12 KiB
UI Phase 4 — Customer Storefront — Report (2026-07-18)
What was built
Home (A5) — HomeScreen.tsx:
- The dead free-text search bar (
?q=silently discarded by C1) replaced with a tappable faux-input (ButtonBase) that routes straight to C1. Decision: the search index has no text column, variant names aren't client-queryable, and the only matchable dataset client-side (5–6 cached category names) is already better served by the category grid directly below — a half-working text field over-promises where trust matters most. The upgrade path is documented in the component's JSDoc and filed as REQ-041. - A compact one-line
TrustStrip(escrow · verified nurses · support) under the greeting. - The patient-record
NudgeCardis now gated on a derived completeness signal (a patient with no recorded conditions) instead of rendering forever, and is dismissible for the session (a module-scoped flag — survives client-side navigation, resets on a hard reload; deliberately not a cookie/localStorage write, since it's ephemeral UI state, not app/auth state). - A
RebookRow— up to 2 "رزرو دوباره با …" cards sourced fromuseBookingList('customer'), deduplicated by nurse, each resolving itsnurseIdvia a per-carduseBookingDetailcall (the list row doesn't carrynurseId) and deep-linking to the nurse's C3 profile. Renders nothing when there's no booking history. - The pre-existing
isErrorretry branch onusePatients()was found already correct in the current code (the audit's finding predates it) — verified, not re-fixed.
Search (C1) — SearchScreen.tsx + useSearchFilters.ts:
- The native
<input type="date">replaced by a Jalali day-chip strip (JalaliDatePickerchipsvariant, extended with optionaltodayLabel/tomorrowLabelprops for «امروز»/«فردا») plus a calendar-icon entry into the full Jalali grid (aPopover) for later dates — intent-only semantics unchanged. - The inline
ToggleButtonGroupgender facet replaced by the sharedGenderToggle, extended with an opt-inallowAnymode (a discriminated-union prop shape — the default booking-context contract is byte-for-byte unchanged for every other caller). - The live-count CTA is now a
StickyActionBar(new shared primitive) — never a tappable "مشاهده ۰ پرستار": at zero results it shows a non-CTA message with a relaxation hint instead of the button. useSearchFiltersnow hydrates every field from the initial URL (searchParamsToFilters), not justcategory_id— a full filter set carried back from a C2 recap chip rehydrates C1 completely. A client-onlyprovince_idquery param (not part ofNurseSearchFilters/the search cache key) carries the province id soCascadingRegionSelect's city dropdown can prefill without a server round trip.
Results (C2) — results/page.tsx:
- A tappable filter-recap chip row (category · region · gender · price) under the count — every chip
performs the same navigation: back to C1 with the entire current query string (C2's URL is already a
superset of everything C1 set, including
province_id/date), so hydration is exact. - The dead single-option sort
<TextField select>replaced with a static "مرتبشده بر اساس امتیاز" caption. - Skeleton twins (
NurseResultCard.Skeleton) were already wired in the current code; updated to match the v2 card anatomy.
NurseResultCard v2 (this phase owns it) — rebuilt as the four-second decision unit:
- Added: a service/variant label (falls back to the category name via a page-supplied
serviceLabelprop until REQ-040'svariantDisplayNamelands), a quiet nurse-gender chip, the completed-visits count («N ویزیت موفق», already-served data that was fetched and never rendered), an optional one-line top-review tag (topReviewTag, REQ-040), and a tappableTrustBadge(now passesnurseId). NurseResultCardSkeletonupdated to the new anatomy (an extra meta-row).
Nurse profile (C3) — nurse/[nurseId]/page.tsx:
- Header now shows the completed-visits count (served, previously unrendered). No gender chip — the
public profile DTO doesn't serve
nurseGender(the client's'female'stub is explicitly a placeholder); filed as REQ-042 rather than rendering it. - New
VerificationSectionrenders the sharedVerificationPanel, fed byuseNurseTrustBadge(nurseId). TrustBadgenow passesnurseId, making the badge tappable everywhere it appears (C2 cards + C3).- The primary CTA is now a
StickyActionBar(price-from beside "درخواست رزرو") — survives the infinite reviews list. - An optional latest-review snippet renders on the services tab when
profile.latestReviewis present. - The reviews tab's fractional
RatingInput(noMath.round) was found already correct in the current code — verified, not re-fixed (phase 1's RatingInput v2 already replaced the rounding).
New shared components (both co-located-tested, both reused by phase 8 per the phase's cross-reference):
VerificationPanel(src/components/VerificationPanel/) — "what Balinyaar verified": one row perTrustBadge.credentialTypes[](i18n off theverificationnamespace'sstep_*codes, never a raw wire value) + the approval date. Renders only what is served — no invented steps, no fake dates.TrustBadge's new opt-innurseIdprop — tappable, opens a bottom-sheet (mobile)/dialog (desktop) renderingVerificationPanel, fed by a lazily-enableduseNurseTrustBadgefetch (only queries once the explainer is actually opened). Implemented as an innerInteractiveTrustBadgesubcomponent so the default (nonurseId) badge — used everywhere else in the app (ProfileSummary, etc.) — calls no query hook at all and needs noQueryClientProviderin its callers' tests.StickyActionBar(src/components/common/StickyActionBar/) — the bottom-pinned action-bar shell shared by C1's live-count CTA and C3's booking CTA. Composes with the shell's existingBottomBarsafe-area handling (a flex sibling below the scrollingmain) rather than reimplementingenv(safe-area-inset-bottom).
Copy: search.empty_suggest_city («شهر نزدیک دیگری مانند مشهد، اصفهان یا شیراز») deleted from both
en.json/fa.json, replaced with empty_suggest_date ("try a different date" — an honest relaxation the
family can actually act on). Reused in the C1 zero-count sticky-bar hint too.
What is now testable (and exactly how)
- Home: log in as the seeded customer (
0912000000x). Tap the search field on Home → lands on/searchwith no?q=in the URL. Stop the API and reload → an error card with «تلاش مجدد», not an eternal spinner. Restart → home recovers; the trust strip shows under the greeting; a customer with a completed booking sees a «رزرو دوباره با …» card that opens the nurse's profile. - C1:
/search— the date filter shows «امروز»/«فردا» + Shamsi day chips (no native browser calendar anywhere); tapping the calendar icon opens the full Jalali grid in a popover. Pick a category + city → the count CTA is pinned at the bottom while scrolling the page. Pick filters matching nothing (e.g. an unusually high min price) → the sticky bar shows the non-CTA "no matches" message, not a tappable button. - C2: run a search → recap chips show category/region/gender/price; tap any chip → C1 opens with every filter pre-filled (category, region incl. province prefill, gender, price, date); browser-back returns to identical results with zero network (cache hit, unchanged from before this phase). The header reads «مرتبشده بر اساس امتیاز» as static text, not a dropdown.
- Cards: a nurse with multiple variants shows distinguishable cards (category/variant label + price);
every card shows the gender chip and «N ویزیت موفق»; tapping the ✓ badge opens the verification
bottom-sheet/dialog (fetches
nurses/{id}/trust_badgelazily on open). - C3: open a profile → header shows completed visits + rating; the
VerificationPanelsection lists the served credential types + approval date (Shamsi); open the reviews tab and scroll deep → «درخواست رزرو» stays pinned at the bottom; a 4.5 average renders as a fractional star row. Tap the CTA → the C4 request form receives the same nurse/variant/gender/date params as before. - Repeat 1–5 on
/en(LTR) and in dark mode; on a mobile viewport confirm both sticky bars sit above theBottomBarand the home-indicator safe area (theBottomBaralready ownsenv(safe-area-inset-bottom);StickyActionBaris a separate flex sibling above it, inside the scrollingmain, so it never overlaps).
What is mocked / waiting on a real service
Nothing new mocked this phase — search stays real (USE_SEARCH_MOCK = false, unchanged) and verification
stays mock-primary (USE_VERIFICATION_MOCK, unchanged) exactly as before; this phase only consumes the
existing useNurseTrustBadge hook (previously built, never wired into a page) and adds no new fetch code
outside existing service hooks. No mock registry entries added/changed.
Contracts
- Consumed:
services/search(b7 + REQ-012, unchanged),services/verification's public trust-badge read (b6, unchanged — this phase is the first to actually calluseNurseTrustBadgefrom a page/component). - Requested (
dev/shared-working-context/frontend/requests/for-backend.md, next free numbers 040–043):- REQ-040 —
variantDisplayName(required) + optionaltopReviewTagonNurseSearchResultDto. - REQ-041 — free-text
qsearch over nurse/variant/category names onGET search/nurses(the Home search bar's upgrade path). - REQ-042 —
nurseGenderonNursePublicProfileDto(the C3 header gender chip, currently omitted). - REQ-043 — public per-step verification detail (step codes + decision dates) so
VerificationPanelcan list identity/Shahkar/license individually instead of folding tocredentialTypes[].
- REQ-040 —
Docs updated
client/CLAUDE.md"Project Structure": the C1/C2/C3 line items (Jalali chips, sticky CTA, recap chips, dossier layout), the newVerificationPanel/StickyActionBarentries, and updatedTrustBadge/GenderToggle/NurseResultCard/JalaliDatePickerlines noting their new modes.
Follow-ups for later phases
- Multi-variant collapse. Collapsing a nurse's several variant rows into one card with a price range +
"N خدمت" disclosure needs the
variantDisplayNameREQ (040) served first — noted as a design follow-up, not built this phase (the category-name fallback keeps rows distinguishable meanwhile). - Public/guest storefront and landing page — deferred to phase 13 (unchanged from the phase brief).
- Save/favorite/share nurses — post-MVP product decision (unchanged from the phase brief).
- Full ICU zero-case copy sweep (ratings/counts pluralization polish across the whole app) — deferred to phase 12 per the phase brief; this phase only ensured the C1 zero-count state is never a tappable CTA.
- Phase 8's public-profile preview should reuse
VerificationPanelandTrustBadge'snurseIdexplainer mode unchanged, per the phase's explicit cross-reference.
Memory
Saved a project-type memory (ui_phase_4_customer_storefront.md, indexed in MEMORY.md) covering: the
NurseResultCard/TrustBadge ownership split with phase 8, the VerificationPanel shared-component
contract, the TrustBadge split-component gotcha (query hooks must live in a conditionally-mounted
subcomponent, not a conditionally-enabled hook call, to avoid forcing QueryClientProvider on every
caller's tests), the C3 profile DTO's missing nurseGender (REQ-042, never render the placeholder stub),
and the StickyActionBar/province_id-carry patterns for future sticky-CTA or region-prefill needs.