# UI Phase 4 — Customer storefront > **Mission:** turn home → search → results → nurse profile from functional screens into a **storefront that > answers "would I let this person into my mother's home?"**. The data layer already fetches the trust signals > (gender, completed visits, verification badge, ratings) — the UI drops most of them on the floor, ships a dead > search bar, a fake sort control, a Gregorian date picker in a Shamsi product, and a primary CTA buried under an > infinite review list. This phase fixes the funnel's honesty defects and builds the reusable trust-presentation > components (`VerificationPanel`, the tappable `TrustBadge` explainer) that phase 8's public-profile preview reuses. > > **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) · > **Unlocks:** [Phase 5 (booking funnel)](ui-phase-5-booking-lifecycle.md) + [Phase 8](ui-phase-8-nurse-business-and-verification.md) (reuses the trust components built here) > **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.** ## 1. Context — where this sits The storefront is the four customer discovery screens: `client/src/app/[locale]/(private-routes)/(customer)/page.tsx` (A5 home), `search/page.tsx` (C1 filters), `search/results/page.tsx` (C2 results), and `search/nurse/[nurseId]/page.tsx` (C3 profile). Phases 0–2 already de-startered the theme, primitives, and chrome — this phase redesigns the route tree itself. Diagnosed defects (all verified in code, full evidence in [audit/customer-storefront.md](audit/customer-storefront.md)): 1. **The home search bar is a dead affordance.** `HomeSearchBar` pushes `?q=` (page.tsx L130) but `SearchFilterScreen` reads only `category_id` (search/page.tsx L49–50) and silently discards `q`. The placeholder promises «جستجوی خدمت یا پرستار…» and the input does nothing. 2. **A failed patients query bricks the home forever.** The gate `if (data == null || isEmpty) return ` (page.tsx L66–68) has no `isError`/retry branch — a transient API failure leaves the app's front door on a spinner. 3. **The patient-record nudge renders unconditionally forever** (page.tsx L96–102), unlike the profile nudge which is gated on `hasCustomerProfile` (L103). 4. **The C1 visit-date filter is a native Gregorian `type="date"`** (search/page.tsx L104–110) in a product where every displayed date is Shamsi; the gender facet re-implements an inline `ToggleButtonGroup` (L86–100) instead of the shared `GenderToggle`; the live-count CTA is the last child of a long scrolling form (L130–140). 5. **C2 ships a fake sort** — a `TextField select` with one hard-coded `MenuItem`, `value="rating"`, no `onChange` (results/page.tsx L67–69) — and no filter recap: `backToFilters` renders only inside the EmptyState (L58/L88), so editing filters from a populated list means browser-back. 6. **`NurseResultCard` is information-thin.** The result row IS a bookable variant, but the card never names the service — three variants of one nurse render as identical cards differing only in price. `nurseGender` and `totalCompletedBookings` are **served by the real backend today** (REQ-012 delivered them onto the index row — see `services/search/apis/clientApi.ts` `NurseSearchResultDto`) and never rendered. The variant display name is a genuine wire gap: neither `NurseSearchResult` (types.ts) nor the DTO carries it. 7. **C3 doesn't read as a dossier.** The «درخواست رزرو» CTA is the last page child (nurse/[nurseId]/page.tsx L92–101) — below the infinite reviews list when that tab is open — and not sticky. `totalCompletedBookings` is fetched and never shown; `TrustBadge` is a static chip with no affordance to learn *what* was verified; and the profile DTO does **not** serve gender (`clientApi.ts` stubs `nurseGender: 'female'` with an "unused" comment — never render it). 8. **The empty-results copy is nonsense**: `search.empty_suggest_city` = «شهر نزدیک دیگری مانند مشهد، اصفهان یا شیراز را امتحان کنید» (fa.json L361) — a family cannot "try Shiraz"; the patient lives where they live. **What already exists (do not rebuild):** - The search architecture: **the filter object IS the URL IS the React Query cache key** (`services/search/keys.ts` + `filterParams.ts`), `keepPreviousData`, debounced Toman price inputs, live count on C1. - The **real** search backend (`USE_SEARCH_MOCK = false` since refinement-phase-4): the index row serves `nurseName`/`avatarUrl`/`distanceKm`/`nurseGender`/`totalCompletedBookings`; `GET nurses/{id}/profile` serves bio/years/services/latestReview. - The public trust-badge read: `useNurseTrustBadge(nurseId)` → `GET nurses/{id}/trust_badge` → `{ isVerified, approvedAt, credentialTypes[] }` (`services/verification`) — real data for the explainer. - Phase 0's theme/brand/icon registry; phase 1's primitives (EmptyState/ErrorState kit, PageHeader, ``, **Jalali date picker**, skeleton twins pattern, RatingInput v2 with honest fractional stars); phase 2's shells, safe-area-aware `BottomBar`, and contextual customer header. Consume them; never fork local variants. - All four data states on C1/C2/C3, `ProfileSkeleton`, the honest «کل شهر» district semantics, `PriceDisplay` money rules, the gender facet's humane hint copy. ## 2. Required reading (do this first) - [audit/customer-storefront.md](audit/customer-storefront.md) — the 21 problems + keep-list for this route tree. - [audit/feature-components.md](audit/feature-components.md) — `NurseResultCard`/`TrustBadge` findings + the component-layer keep-list (presentational purity, caller-owned i18n, token discipline). - The four pages under `client/src/app/[locale]/(private-routes)/(customer)/` listed above, plus `search/useSearchFilters.ts` and `services/search/{types.ts,filterParams.ts,apis/clientApi.ts}` — know exactly what the wire serves before deciding what needs a REQ. - `client/src/components/{NurseResultCard,TrustBadge,GenderToggle,ServicePriceRow,PriceDisplay}/` — the components this phase owns or extends. - `client/src/services/verification/types.ts` (`TrustBadge` wire type, `publicBadgeState`) and `hooks/useNurseTrustBadge.ts` — the explainer's data source. - [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) — invoke it; and skim `client/CLAUDE.md` "Golden rules". - Product rules: [../../../product/overview/platform-summary.md](../../../product/overview/platform-summary.md) (the four ground truths), [../../../product/business/04-search-and-matching.md](../../../product/business/04-search-and-matching.md) (verified-only, same-gender, variant-is-the-unit), and [../../../product/business/02-nurse-verification.md](../../../product/business/02-nurse-verification.md) (what the pipeline actually verifies — the explainer must narrate this truthfully). ## 3. Scope — build this ### 3.1 Customer home — a front door that works - **Fix the dead search bar — decision: convert it into an honest search entry point.** Replace the free-text `TextField` with a tappable search affordance (a faux-input `ButtonBase` styled as the search field) that routes to C1 (`/search`), optionally focusing the category grid. *Why not client-side text match:* the index has no text column, variant names are not queryable client-side, and the only matchable dataset (5–6 cached category names) is already better served by the category grid directly below — a half-working text field over-promises exactly where trust matters. File the **text-search REQ** (`q` over nurse/variant/category names on `search/nurses`) so the bar can upgrade to a real typeahead when the backend serves it; note the upgrade path in the component's JSDoc. - **Add the missing `isError` branch**: `usePatients()` failure renders the phase-1 `ErrorState` with retry — never an eternal spinner. Keep the onboarding redirect logic (`isEmpty` → onboarding) exactly as is. - **Compact trust strip** under the greeting: three icon+label items — «پرداخت امن امانی» · «پرستاران تاییدشده» · «پشتیبانی» — quiet, one row, tokens only. This is ambient reassurance, not a hero; keep it to one line on mobile. - **Gate the patient-record NudgeCard on actual record incompleteness.** Derive a completeness signal from the cached patients data (e.g. a patient missing conditions/age — inspect `services/patients` types for what's derivable). If no honest signal is derivable client-side, show the nudge only when a patient list exists but a patient was never opened/completed — and make it dismissible (session-scoped) rather than permanent. Never a forever-nudge. - **Rebook shortcut row**: source recent bookings from the existing `useBookingList('customer')` cache (`services/bookings`) and render up to 2 «رزرو دوباره با …» cards deep-linking to the nurse's C3 profile. Repeat care is the dominant pattern in home nursing. Render nothing (no empty state) when there are no past bookings. ### 3.2 Search (C1) — Persian-native filters, visible feedback - **Replace the native `type="date"`** with the phase-1 Jalali controls: a horizontal day-chip strip — «امروز»، «فردا»، then day-name + Shamsi date chips for the next 7 days — plus an entry to the full Jalali picker for later dates. The value stays the same ISO string `dateIntent` the flow already carries (intent-only, never a hard filter — preserve that semantic and its hint copy). - **Make the live-count CTA a sticky bottom bar**: «مشاهده N پرستار» pinned above the `BottomBar`, safe-area aware (compose with phase 2's safe-area handling — don't reimplement `env(safe-area-inset-bottom)` locally). The count is the screen's best feedback instrument; it must be visible while adjusting the upper filters. - **Reuse the shared `GenderToggle`** instead of the divergent inline `ToggleButtonGroup`. `GenderToggle` is male/female-only by design (required, never defaulted — booking context); extend it with an **opt-in `allowAny` prop** (adds the «فرقی ندارد» option) rather than forking, keep the booking-context behavior unchanged, and update its co-located test for both modes. - **Never render «مشاهده ۰ پرستار» as a tappable CTA.** When the live count is 0, the sticky bar shows a non-CTA message («پرستاری با این فیلترها یافت نشد») with the same relaxation hints as 3.6. Full ICU zero-case catalog sweep is (DEFERRED → [phase 12](ui-phase-12-copy-motion-and-polish.md)) — just don't ship the zero-CTA here. ### 3.3 Results (C2) — honest header, editable filters - **Tappable filter-recap chip row** under the count: category · region (city/district or «کل شهر») · gender · price range — each chip deep-links back to C1 **with state preserved**. The filters already live in the URL (do-not-regress); C1 must initialize from the carried params, not just `category_id` — extend `useSearchFilters` to hydrate from a full `searchParamsToFilters` read. - **Kill the fake sort.** Replace the single-option `TextField select` with a static caption «مرتب‌شده بر اساس امتیاز» (the contract's only MVP sort — `SearchSort = 'rating'`). Real sort options only if the API grows them (DEFERRED — do not file a REQ for sorts the product hasn't asked for). - **Adopt skeleton twins**: replace the generic `Skeleton height={112}` rows with `NurseResultCard.Skeleton` (built in 3.4, following phase 1's twin pattern) so loading matches the card anatomy exactly. ### 3.4 `NurseResultCard` v2 — the decision card (this phase owns it) Rebuild the card as the four-second decision unit, staying **presentational + memoized** with its co-located test: - **Service/variant display name** — the row IS a variant; the card must name what is being bought. This field is missing from the wire (`NurseSearchResultDto` has no display name — verified): **file the REQ** to denormalize `variantDisplayName` onto the index row. Until it lands, render the row's category name (available via the cached catalog reference data, passed in by the page — the card stays data-agnostic) so multi-variant nurses are at least distinguishable by price row; switch to the served name when the REQ lands. - **Nurse gender indicator** — load-bearing for same-gender matching; `nurseGender` is served today and never rendered. A quiet chip/glyph («خانم»/«آقا» styled per the design language), never color-only. - **Completed-visits count** — `totalCompletedBookings` is served today; render «N ویزیت موفق» (fa digits). This carries more information than the uniform verified chip (every row is verified by invariant). - **Optional top review tag** — one line, e.g. «منظم و دقیق», only if served. The index row carries no review tag (verified): file it as an optional field on the same index-row REQ; the card renders it conditionally. - **`NurseResultCard.Skeleton`** static twin matching the new anatomy (avatar disc, name+badge row, meta row, price row). - Keep: `memo`, stable `onSelect`, keyboard/focus a11y, `PriceDisplay` for money, `TrustBadge` for the badge. ### 3.5 Nurse profile (C3) — the trust dossier + the shared trust components - **Header identity card**: large avatar, name, gender chip, years of experience, completed visits («N ویزیت موفق» — fetched today, never rendered), rating + count. The profile DTO does **not** serve gender (the client stub is a placeholder — never render it): **file the REQ** to add `nurseGender` to `NursePublicProfileDto`; until it lands, omit the gender chip on C3 (the C2 card and the carried `required_gender` param cover the matching flow honestly). - **`VerificationPanel` — new shared component** (`src/components/VerificationPanel/`, barrel + test): "what Balinyaar verified" as a check-listed panel, fed by `useNurseTrustBadge(nurseId)` — verified state, `approvedAt` (Shamsi), and one row per `credentialTypes[]` entry (stable codes → i18n labels, never raw wire values). Render only what is served — no invented steps, no fake dates. - **Tappable `TrustBadge` explainer**: give `TrustBadge` an opt-in `onClick`/expand affordance that opens a bottom-sheet (mobile) / dialog (desktop) narrating the verification story — «این پرستار این مراحل را گذرانده است…» — rendering the same `VerificationPanel` inside. The default (non-interactive) badge everywhere else is unchanged; update `TrustBadge.test.tsx`. Wire it up on C2 cards and C3. **File the REQ** for public per-step verification detail (step codes + decided dates) so the panel can list identity/Shahkar/license individually with dates; until then the panel shows the credential-type rows + the approval date — honest, served data only. These two are **REUSED by [phase 8](ui-phase-8-nurse-business-and-verification.md)'s public-profile preview — build them shared**, presentational, caller-fed. - **Sticky «درخواست رزرو» bottom CTA** (same sticky-bar treatment as 3.2, price-from beside the button) so the CTA survives the infinite reviews list. Keep the carried variant/gender/date param handoff to f7 exactly as is. - **Reviews tab polish**: aggregate header uses phase 1's RatingInput v2 (fractional — kill the `Math.round` overstatement at L260 by consuming the v2 API), review cards get the card-kit treatment. Rating-distribution bars are (DEFERRED → phase 12) unless trivially derivable from served data. - Render the already-fetched `latestReview` snippet on the services tab if it improves the dossier — optional. ### 3.6 Empty-results copy — honest relaxation Replace `empty_suggest_city` in **both** catalogs with honest relaxation suggestions the family can actually act on: widen to the whole city («منطقه را خالی کنید تا کل شهر جست‌وجو شود» — keep/merge with the existing district line), try other dates, remove the gender filter. Delete the «مشهد/اصفهان/شیراز» line. Reuse the same relaxation vocabulary in the C1 zero-count bar (3.2). Final wording sweep is phase 12's; this phase removes the nonsense and lands plausible copy in both `en.json`/`fa.json`. **(DEFERRED)**: public/guest storefront and landing page (→ [phase 13](ui-phase-13-public-front-door.md)); save/favorite/share nurses (post-MVP, product decision); collapsing multi-variant nurses into one card (needs the variant-name REQ served first — note it in the report as a follow-up); C1/C2 Suspense-fallback skeletons beyond what phase 1's route-level `loading.tsx` already covers. ## 4. Mocks & seams in this phase **None introduced.** Search and reviews are real (`USE_SEARCH_MOCK = false`); verification stays mock-primary but the public `trust_badge` read flows through the existing `services/verification` seam either way — this phase only consumes hooks. Backend gaps become REQ entries appended to [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) — **REQ-001…038 are taken; check the tracker for the next free number at execution time** (parallel UI phases also file). Expected filings from this phase: 1. **Free-text search** — `q` param on `GET search/nurses` matching nurse/variant/category names (the home bar's upgrade path, 3.1). 2. **Index-row enrichment** — `variantDisplayName` (required) + optional `topReviewTag` on `NurseSearchResultDto` (3.4). 3. **`nurseGender` on `NursePublicProfileDto`** (3.5 header chip). 4. **Public verification-step detail** — per-step passed checks + decision dates on (or beside) the `trust_badge` payload (3.5 explainer depth). The UI stays mock-tolerant: every new render is conditional on the field being served; nothing blocks on a REQ. ## 5. Critical rules you must not get wrong - **Verified-only invariant.** Every returned row is verified by the search-index invariant; the UI **never re-filters or re-checks** verification and never fakes a badge state. The explainer *adds* narration to served truth — `TrustBadge`'s three honest states (verified/unverified/expired) and their server-driven derivation stay exactly as they are. - **Never render placeholder data as truth.** The C3 profile's stubbed `nurseGender: 'female'` must not reach the screen; no invented verification steps or dates in the panel. - **The filter-object-IS-the-URL-IS-the-query-key architecture stays.** Recap chips, C1 hydration, and the sticky CTA all read/write the same `filterParams` (de)serializer; deep-linking and back/forward cache hits must keep working. - **Money rules**: amounts render only through `PriceDisplay`/the money util (BigInt IRR, Toman at the boundary); totals only ever price × sessionCount; never parse money to a float. - **Same-gender facet care**: never defaulted, hint copy preserved, the chosen value carried into booking as before. - **Design contract**: i18n keys in **both** catalogs; tokens not hexes (`--bal-*` / palette keys only); RTL logical props (`marginInlineStart`, `start`/`end`) — verify at `/fa` first; dark mode via tokens; MUI v9 API only; shared components (`VerificationPanel`, extended `TrustBadge`/`GenderToggle`/`NurseResultCard`) keep/get co-located tests; `clientFetch`/cookies/services rules untouched — this phase adds **no** fetch code outside existing hooks. - **Ownership**: phase 0 owns `theme/`/`AppIcon`/`AppButton`; phase 1 owns the shared primitives (Jalali picker, RatingInput, state views, skeleton-twin pattern); phase 2 owns `layout/`. If a foundation gap blocks you (e.g. a missing icon or sticky-bar primitive), extend the foundation file **minimally** and note it in your report — never fork a local variant. ## 6. Definition of Done On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md): - [ ] `npm run check` green; `npm run test:ci` green — including updated/new tests for `NurseResultCard` (+ its Skeleton), `TrustBadge` (interactive mode), `GenderToggle` (`allowAny`), and `VerificationPanel`. - [ ] `en.json`/`fa.json` in sync; «مشهد/اصفهان/شیراز» is gone from both. - [ ] The home search affordance routes to C1; a failed patients query shows retry (kill the API to prove it); the record nudge no longer renders for a complete record; a customer with past bookings sees the rebook row. - [ ] C1: Jalali day-chips replace the Gregorian input; the count CTA is sticky and never tappable at count 0; the gender facet is the shared `GenderToggle`. - [ ] C2: recap chips deep-link back to C1 with **all** filters preserved (deep-link a full URL to prove hydration); no interactive-looking dead sort; skeleton twins during load. - [ ] Cards show variant/category label, gender, completed visits; C3 has the sticky CTA, completed visits in the header, `VerificationPanel`, and tappable badges opening the explainer; no gender chip on C3 until the REQ lands. - [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and mobile + desktop for home, C1, C2, C3 (sticky bars must clear the BottomBar and the home-indicator safe area on mobile). - [ ] REQs filed in the tracker with the next free numbers; no edits outside `client/` and the tracker/report files. ## 7. How to test (what a human can verify after this phase) 1. **Home**: log in as the seeded customer → the search bar tap opens C1 (no dead `?q=`). Stop the API and reload → an error card with «تلاش مجدد», not an eternal spinner. Restart → home recovers; trust strip visible under the greeting; a customer with a completed booking sees «رزرو دوباره با …». 2. **C1**: the date filter shows «امروز/فردا» + Shamsi day chips (no Gregorian browser calendar anywhere). Pick filters → the count CTA stays pinned at the bottom while scrolling; set filters matching nothing → the bar shows the no-results message and is not tappable. 3. **C2**: run a search → recap chips show category/region/gender/price; tap one → C1 opens with every filter pre-filled; browser-back returns to identical results with zero network (cache hit). The header reads «مرتب‌شده بر اساس امتیاز» as text, not a dropdown. 4. **Cards**: a nurse with multiple variants shows distinguishable cards (service label + price); every card shows gender and «N ویزیت موفق»; tapping the ✓ badge opens the verification bottom-sheet. 5. **C3**: open a profile → header shows completed visits + rating; `VerificationPanel` lists the served credential types + approval date (Shamsi); open the reviews tab and scroll deep → «درخواست رزرو» stays pinned; a 4.5 average renders as a fractional star row, not five full stars. Tap the CTA → the C4 request form receives the same nurse/variant/gender/date params as before. 6. Repeat 1–5 on `/en` (LTR) and in dark mode; on a mobile viewport confirm both sticky bars sit above the BottomBar. ## 8. Hand off & document (close the phase) - Update `client/CLAUDE.md` "Project Structure": the C1/C2/C3 line items (Jalali chips, sticky CTA, recap chips, dossier layout) and the new `VerificationPanel` component entry; note `TrustBadge`'s new interactive mode and `GenderToggle`'s `allowAny` on their lines. - Write the report at `dev/shared-working-context/reports/ui-phase-4-report.md`: what changed per screen, the exact REQ numbers filed (text search, index-row enrichment, profile gender, public step detail), the home-search-bar decision + rationale, the multi-variant-collapse follow-up, and any foundation files you extended. - Save a memory note per operating-rules §8: phase 4 owns `NurseResultCard`; `VerificationPanel` + the `TrustBadge` explainer are the shared trust components phase 8 consumes; the C3 profile DTO still lacks gender (placeholder field — never render it) until its REQ lands.