# Customer storefront — home, search, results, nurse public profile

## Current state

The storefront is four client-rendered screens, all behind auth: CustomerHomePage (client/src/app/[locale]/(private-routes)/(customer)/page.tsx — greeting+avatar, free-text search bar, data-driven CategoryTile grid, two NudgeCard prompts), SearchPage/C1 (search/page.tsx — a vertical filter form: category grid, CascadingRegionSelect province/city/district, 3-way gender ToggleButtonGroup, native date input, debounced Toman price range, and a live-count CTA driven by useSearchFilters + useNurseSearch), SearchResultsPage/C2 (search/results/page.tsx — URL-is-the-filter-state list of NurseResultCard with load-more, one-option sort select, skeleton/empty/error states), and NurseProfilePage/C3 (search/nurse/[nurseId]/page.tsx — avatar header, rating, TrustBadge + INO chip, attribute Chips, MUI Tabs for services (ServicePriceRow list) and reviews (infinite published-review list with RatingInput stars), and a bottom "درخواست رزرو" AppButton). Chrome comes from CustomerLayout (client/src/layout/CustomerLayout.tsx): a fixed default-MUI AppBar (TopBar with a centered static title "اپلیکیشن خانواده", support icon, notification bell, dark-mode toggle) plus a 5-tab MUI BottomNavigation BottomBar, content constrained to 800px.

Styling is disciplined but minimal: everything in this area resolves through the --bal-* CSS variables in client/src/theme/tokens.css (deep teal light + lifted-teal dark schemes, both defined), Mikhak is loaded for fa via next/font, MUI v9 CSS-vars theme with an RTL Emotion cache, and I found zero hard-coded hexes in the storefront pages or their components. Visually, however, it is a bare utility app, not a storefront: cards are 1px-border Papers with no elevation/warmth, the terracotta accent is used nowhere in the storefront (only checkout/BNPL screens use --bal-secondary), all icons are stock @mui/icons-material, the "logo" is the starter kit's multicolor cartoon-pencil SVG, and there is no hero, no value props, no how-it-works, and no public/guest-accessible page other than /login — the entire marketing face of the marketplace requires an account and a completed patient record to even see.

## Problems (21)

- **[high]** `client/src/app/[locale]/(public-routes)` — There is no public storefront at all. The only unauthenticated route is /login; home, search, results, and nurse profiles are all wrapped in RoleGuard(customer) via (customer)/layout.tsx, and the home page additionally redirects to onboarding until a patient record exists. A family evaluating the service cannot see a single nurse, price, or trust signal before registering — fatal for acquisition in a trust-first marketplace.
  - evidence: (public-routes)/ contains only layout.tsx and login/page.tsx; (customer)/layout.tsx wraps children in <RoleGuard expected={APP_ROLES.CUSTOMER}>
- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/page.tsx` — The home free-text search bar is a dead affordance: HomeSearchBar pushes `?q=<query>` to /search, but SearchFilterScreen reads only `category_id` and silently discards `q`. The placeholder promises «جستجوی خدمت یا پرستار…» and the input does nothing.
  - evidence: page.tsx L130 pushes `?q=`; search/page.tsx L49-50 reads only params.get('category_id')
- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/page.tsx` — If the patients query errors, the customer home hangs on a bare spinner forever — the gate `if (data == null || isEmpty) return <AppLoading />` has no isError/retry branch, so a transient API failure bricks the app's front door.
  - evidence: L66-68: `if (data == null || isEmpty) { return <AppLoading />; }` — usePatients() error never handled
- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/search/page.tsx` — The visit-date filter is a native `<input type="date">`, which renders a Gregorian calendar in browser chrome. The default locale is fa and every date the app displays is Shamsi (formatShamsiDate); Iranian families plan by the Jalali calendar, so this field is unusable-in-practice localization breakage on the main discovery flow.
  - evidence: L104-110: `<TextField type="date" … slotProps={{ inputLabel: { shrink: true } }} />`
- **[high]** `client/src/components/NurseResultCard/NurseResultCard.tsx` — The result unit is the variant, but the card never names the service/variant — a nurse offering three variants appears as three near-identical cards (same avatar, name, rating) differing only in price, with no explanation. NurseSearchResult also carries no variant display name, so the card cannot label it even if it wanted to.
  - evidence: Card renders only name/badge/rating/distance/price; services/search/types.ts NurseSearchResult has no variant displayName field
- **[high]** `client/src/components/common/AppIcon/icons/PencilIcon.tsx` — The brand 'logo' icon is the starter kit's emoji-style multicolor pencil SVG with hard-coded fills (#EA596E, #FFCC4D, #D99E82…) that ignore the color prop; it is rendered as the BrandMark on the auth splash and the sidebar logo. A cartoon pencil as the mark of a healthcare-trust brand actively undermines the product.
  - evidence: PencilIcon paths carry fill="#EA596E" etc.; AppIcon/config.ts maps `logo: PencilIcon`; components/auth/BrandMark.tsx renders <AppIcon icon="logo" size={56}/>
- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/search/results/page.tsx` — The populated results state has no filter recap and no way to edit filters — the header is only a count plus a fake sort control; `backToFilters` is rendered exclusively inside the EmptyState, so users must use browser-back to change city/gender/price.
  - evidence: L62-70 header renders count + sort only; backToFilters referenced only in EmptyState (L88, L131)
- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/search/results/page.tsx` — The sort dropdown is a non-functional control: a TextField select with a single MenuItem, hard-coded value="rating", and no onChange — it looks interactive but does nothing, which erodes perceived quality.
  - evidence: L67-69: `<TextField select … value="rating">` with one <MenuItem> and no onChange
- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/search/nurse/[nurseId]/page.tsx` — The primary CTA «درخواست رزرو» sits at the very bottom of the page flow — after the full infinite reviews list when that tab is open — and is not sticky; and when no variant_id was carried it silently falls back to services[0] while the ServicePriceRow list offers no way to pick a specific service to book.
  - evidence: L62-64: `const variantId = carriedVariant ?? String(profile.services[0]?.variantId ?? '')`; CTA is the last child of the page Stack (L92-101); ServicePriceRow has no onClick/select affordance
- **[medium]** `client/src/services/search/types.ts` — Core trust data is fetched but never rendered: totalCompletedBookings and nurseGender exist on both NurseSearchResult and NurseProfile, yet neither the result card nor the profile shows completed-visit count or confirms the nurse's gender — exactly the cues a family choosing an in-home caregiver checks first.
  - evidence: types.ts L67 `totalCompletedBookings`, L73/L117 `nurseGender` — no usage in NurseResultCard.tsx or nurse/[nurseId]/page.tsx
- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/page.tsx` — The «تکمیل پروندهٔ بیمار» NudgeCard renders unconditionally forever — unlike the profile nudge (gated on hasCustomerProfile), there is no completeness check, so a customer with fully-filled patient records sees a permanent stale prompt occupying prime home real estate.
  - evidence: L96-102 renders NudgeCard with no condition; L103 gates only the profile nudge
- **[medium]** `client/src/components/RatingInput/RatingInput.tsx` — Rating stars are colored with --bal-warning, a token documented in tokens.css as a toast/alert *background* meant to sit under cream text. As a foreground fill it renders dark mustard (#8a6418) in light mode and muddy olive (#97701f, ~3.7:1) on the dark #0f1c19 background — ratings, a primary trust signal, look dim rather than gold in both schemes.
  - evidence: RatingInput.tsx L53 and NurseResultCard.tsx L87 use `var(--bal-warning)`; tokens.css L50 comment: 'Feedback — toast / alert backgrounds'
- **[medium]** `client/src/layout/components/TopBar.tsx` — The shell is stock starter chrome: a default-elevation fixed MUI AppBar with a nowrap centered Typography title, and CustomerLayout titles it with the system label «اپلیکیشن خانواده» instead of the brand «بالین یار» — the brand name never appears anywhere in the logged-in storefront.
  - evidence: TopBar.tsx L25-38 (centered whiteSpace:'nowrap' title, commented-out boxShadow toggle); CustomerLayout.tsx L46 `title={tShell('customer_app')}`
- **[medium]** `client/src/components/common/AppButton/AppButton.tsx` — Starter-grade AppButton defaults to `margin: 1` on all sides, so every storefront call site must fight it with `sx={{ m: 0 }}` (10+ occurrences across home/search/results/profile alone); passing any sx also silently drops the default, making spacing inconsistent by construction.
  - evidence: AppButton.tsx L9-11 DEFAULT_SX_VALUES = { margin: 1 }; e.g. results/page.tsx L83/L100, nurse/[nurseId]/page.tsx L56/L98 all set `sx={{ m: 0 }}`
- **[medium]** `client/src/components/common/AppIcon/config.ts` — The entire icon vocabulary is stock @mui/icons-material (ElderlyOutlined, ChildCareOutlined, VolunteerActivismOutlined…), giving category tiles and chrome the generic Material-dashboard look the brand explicitly wants to avoid; category icons are the emotional face of the home grid and read cold/clinical.
  - evidence: config.ts L4-98: every icon imported from @mui/icons-material; KNOWN_CATEGORY_ICONS in CategoryTile.tsx resolve to these
- **[medium]** `client/src/layout/components/BottomBar.tsx` — The bottom navigation has no iOS safe-area handling — no env(safe-area-inset-bottom) padding — so on notch/home-indicator phones (the primary device class for this mobile-first app) the 5 tab targets sit under the system gesture bar.
  - evidence: L50-62: Paper + BottomNavigation with only `borderTop`, no safe-area padding
- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/search/page.tsx` — The live-count CTA «مشاهده N پرستار» — the best affordance on the screen — is the last element of a long scrolling form instead of a sticky bottom bar, so the count is invisible while adjusting the upper filters where it would guide relaxation/tightening in real time.
  - evidence: L130-140: AppButton rendered as final Stack child, no position:sticky
- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/search/page.tsx` — The gender facet re-implements an inline ToggleButtonGroup instead of extending the shared GenderToggle component, leaving two divergent gender-control implementations (different padding/fontWeight, no error affordance) to keep visually in sync.
  - evidence: search/page.tsx L86-100 inline group vs components/GenderToggle/GenderToggle.tsx
- **[low]** `client/messages/fa.json` — The empty-results suggestion «شهر نزدیک دیگری مانند مشهد، اصفهان یا شیراز را امتحان کنید» hard-codes city names and suggests an impossible relaxation — the patient lives where they live; a family cannot 'try Shiraz'. Reads as filler copy and dents credibility.
  - evidence: search.empty_suggest_city key
- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/search/nurse/[nurseId]/page.tsx` — The verified badge is a static chip everywhere it appears — nothing lets a customer discover *what* was verified (identity, license, INO, background check), even though the verification pipeline is the product's core differentiator; on results it is also repeated identically on every card (all rows are verified by invariant), so it stops carrying information.
  - evidence: TrustBadge rendered with no onClick/link (page.tsx L141, NurseResultCard.tsx L82); TrustBadge.tsx has no interactive affordance
- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/search/results/page.tsx` — First paint of both search screens under Suspense is a bare centered CircularProgress (AppLoading) rather than the page's own skeleton layout, causing a spinner→skeleton→content double transition.
  - evidence: results/page.tsx L22 and search/page.tsx L35 `<Suspense fallback={<AppLoading />}>` while in-page skeletons exist at L72-77

## Opportunities (10)

- **Public landing + guest browse (the real storefront)** (impact: high, effort: large) — Create a public marketing home under (public-routes): hero with the existing tagline «مراقبت مطمئن در خانه», the category grid as entry points, a 3-step how-it-works (search verified nurses → escrow-protected payment → pay-out only after confirmed check-out), a verification-pipeline trust strip, and sample verified-nurse cards. Let guests run search and view nurse profiles read-only, deferring the login gate to the «درخواست رزرو» tap (intent is highest there). Nearly all pieces already exist as components — this is mostly routing + a hero section, and it is the single biggest acquisition lever the product has.
- **Nurse profile as a trust dossier** (impact: high, effort: medium) — Redesign C3 around the question 'would I let this person into my mother's home?': a header card with photo, gender chip, years of experience, completed-visits count (data already fetched), and rating; an expandable 'verification checklist' section listing each passed check (identity ✓, nursing license ✓, INO membership ✓) fed by the badge state instead of one flat chip; a rating-distribution bar + review tag summary above the review list; per-service rows that are tappable to book that exact variant; and a sticky bottom CTA bar showing price-from + «درخواست رزرو». This screen is where the marketplace either earns or loses the booking.
- **Jalali date selection** (impact: high, effort: small) — Replace the native Gregorian type="date" with a Shamsi-native control: a horizontal chip strip («امروز», «فردا», day+Shamsi-date chips for the next 7 days) plus an optional full Jalali picker. Small build (the Shamsi format util already exists) and removes the most jarring localization break in the funnel.
- **Results page upgrade: recap chips, variant labels, honest sorting** (impact: high, effort: medium) — Add a tappable filter-recap chip row (category · city/district · gender · price) that deep-links back to C1 with state preserved; show the service/variant name on each card (requires adding displayName to the search row — already filed as a backend gap pattern); collapse multiple variants of one nurse into a single card with a price range and 'N خدمت' disclosure; and either implement price/distance sort or remove the single-option dropdown. Also surface totalCompletedBookings («۱۲۴ ویزیت موفق») on cards — a stronger differentiator than the uniform verified chip.
- **Warmth pass: brand mark, custom icons, terracotta accents** (impact: high, effort: medium) — Replace the pencil logo with a real Balinyaar mark and put the brand (not «اپلیکیشن خانواده») in the top bar; commission/adopt a single warm stroke-icon set for the 5-6 category icons and bottom-nav tabs; introduce the terracotta accent sparingly in the storefront — e.g. the home greeting card background tint, the star rating fill (a proper warm gold/terracotta token instead of --bal-warning), and the profile CTA. Today the storefront is monochrome teal borders on white and reads like an internal tool.
- **Sticky conversion bars** (impact: medium, effort: small) — Make the C1 live-count CTA and the C3 booking CTA sticky bottom bars (above the BottomBar, with safe-area padding). The live count becomes a real-time feedback instrument while filtering; the profile CTA stays reachable regardless of review-list depth. One shared SlotBar component covers both.
- **Home that sells and remembers** (impact: medium, effort: medium) — Wire the free-text search to actually filter (variant/category name match) or remove the field; add a compact trust strip (پرداخت امن اسکرو · پرستاران تاییدشده · پشتیبانی ۲۴ ساعته); add a 'rebook' shortcut card sourced from the bookings cache («رزرو دوباره با خانم …» — repeat care is the dominant pattern in home nursing); and make nudges completeness-aware and dismissible.
- **Tappable verification explainer** (impact: medium, effort: small) — Make every TrustBadge open a bottom sheet that narrates the verification pipeline ('این پرستار این مراحل را گذرانده است: …' with check dates). Turns a static chip into the product's trust story at exactly the moment of doubt, at trivial cost.
- **Save/favorite and share nurses** (impact: medium, effort: medium) — Add a heart action on result cards and the profile plus a share-profile link. Families deliberate and compare with relatives before booking a caregiver; today there is no way to shortlist or send a profile to a sibling, forcing screenshot workflows.
- **Nurse photo emphasis and richer media** (impact: medium, effort: large) — Today avatars are initial-letter fallbacks in teal discs. Prioritize real photos in the verification flow and give them prominence (larger profile photo, photo-forward result cards); longer-term, a short recorded self-introduction. In a market with low institutional trust, seeing the person is the strongest trust cue available.

## Keep (do not regress)

- Token discipline: zero hard-coded hexes in storefront pages/components — every color resolves through the --bal-* variables in client/src/theme/tokens.css, with complete light and dark scheme definitions, so a restyle can happen at the token layer.
- All four data states are consistently implemented everywhere: skeleton grids sized to match real tiles (home/C1), error states with working retry, product-aware empty states, and a dedicated ProfileSkeleton on C3 — no blank screens.
- RTL correctness: no physical marginLeft/left/textAlign:'left' anywhere in the audited area; logical properties used where needed (marginInlineStart:'auto' in ReviewCard), paired LTR/RTL themes with stylis-plugin-rtl, and Mikhak loaded for fa with full-body coverage.
- Search architecture: the filter object is the URL is the React Query cache key (deep-linkable, back/forward-safe results), the C1 CTA shows a live result count, price inputs are debounced, and 'empty district = whole city' is an explicit, honestly-labeled choice in CascadingRegionSelect.
- Trust honesty by construction: the C1 subtitle states only verified nurses appear, TrustBadge has three visually distinct states (verified/unverified/expired) driven by server state and semantic tokens, and the UI never re-derives or fakes verification.
- Money handling: PriceDisplay renders exclusively through the IRR→Toman money util with fa digit grouping, unit labels from i18n keys, and totals only ever computed as price × sessionCount (BigInt-safe) — never a parsed float.
- Accessibility groundwork: CategoryTile is a real ButtonBase with aria-pressed, NurseResultCard has Enter/Space key handling and a focus-visible outline, RatingInput exposes radiogroup/radio semantics, and the home search form uses role='search'.
- The gender facet is treated with the care the domain demands: prominent placement, humane hint copy explaining same-gender preference, never defaulted, and carried explicitly into the booking request.
- Persian copy quality: the fa strings are natural, specific, and warm (e.g. the patient-record nudge «…تا پرستار آماده حاضر شود») — not machine-translated placeholder text.
