6.5 KiB
Frontend Phase 6 — Search & discovery (C1/C2/C3) — report
Date: 2026-07-09 · Track: frontend · Depends on: f4 (catalog/category grid), f5
(TrustBadge), f3 (geo picker), f0 (money util, services pattern) · Consumes: b7 search.md +
b6 trust badge / b5 variant reads · Unlocks: f7 booking request.
What was built
A vertical discovery slice — the trust funnel where a family picks a real, verified nurse.
services/search/ (the domain, copies the f0/auth shape)
types.ts—NurseSearchFilters(the cache key/URL shape),NurseSearchResult(C2 card row),NurseProfile+NurseProfileServiceRow+NurseReviewSnippet(C3), theSearchApiseam. Derived from the b7 contract; the fields b7 doesn't expose are documented inline + filed (REQ-012).keys.ts—searchKeys.results(filters)/searchKeys.profile(id)+canonicalizeSearchFilters(stable key order, absent optionals omitted) — the filter-object-as-query-key caching contract.constants.ts—USE_SEARCH_MOCK(true, primary), stale/gc times, page size, debounce ms.filterParams.ts— the single C1↔C2 URL (de)serializer (snake_case, matching b7 params) so the screen that writes the URL and the screen that reads it never drift.apis/—mockApi.ts(primary; real-shaped verified fixtures inseed.ts, reproduces b7 filter + whole-city geography + rating-sort semantics),clientApi.ts(real b7/b6 mapping scaffold, gaps left blank),index.ts(seam selection byUSE_SEARCH_MOCK).hooks/—useNurseSearch(keepPreviousData, enabled on category+city),useNurseProfile(enabled on id),useDebouncedValue(generic, used by the C1 controller).index.tsre-exports hooks.
Screens (all RTL/i18n/dark-mode, under the customer bottom-tab shell)
- C1
/search— reused f4 category grid (selectable) + f3CascadingRegionSelect(district optional = whole city) + prominent same-gender toggle (خانم/آقا/فرقی ندارد) with a why-line + intent-only date + Toman price range; a live result count drives the "مشاهده N پرستار" CTA into C2. Fast-changing filter state in the colocateduseSearchFilterscontroller (debounced price). - C2
/search/results— result count + rating sort control (one option; other sorts DEFERRED), rating-sortedNurseResultCardlist, all four states (skeleton / empty "relax filters" with concrete suggestions / error-retry / populated), load-more. Filters live in the URL. - C3
/search/nurse/[nurseId]— avatar/name/rating, ✓ تاییدشده (reused TrustBadge) + نظام پرستاری (rendered only wheninoMembership), attribute chips (specialty codes → i18n + years-experience),ServicePriceRowservices list, latest-review snippet (+ "no reviews" empty), loading/not-found/error states, and the "درخواست رزرو" CTA that hands off to/bookings/request.
Shared components (tested)
NurseResultCard— presentational + memoized; avatar, name, reused verified badge, rating + review count, optional distance chip, "from X تومان/unit" viaPriceDisplay.ServicePriceRow— service name +PriceDisplay(money util + i18n unit label); reused by the booking summary in f7+.
Other
- Added
star+tuneicons to the AppIcon registry. Routes:SEARCH_RESULTS,SEARCH_NURSE,BOOKING_REQUEST. i18n:searchfilled +bookingseeded, both locales in sync.
Now testable, and exactly how (§7 of the phase)
Run npm run dev (mock is primary — no backend needed; or point NEXT_PUBLIC_API_URL at a b7 server
and flip USE_SEARCH_MOCK=false, noting the REQ-012 gaps).
- Discovery E2E: Home → tap a category (e.g. مراقبت سالمند) → C1 preselects it → set city (تهران), gender (خانم) → CTA shows a real count → tap → C2 lists only verified nurses, rating-sorted, each with photo/initials, name, ✓ تاییدشده, rating + review count, distance, "from X تومان/ساعت".
- Profile: tap a card → C3 shows badges, attribute chips, services + Persian unit labels, latest
review → "درخواست رزرو" →
/bookings/requestechoing nurse + variant + gender intent. - Empty state: search Mashhad/Isfahan/Shiraz (seeded empty) → "relax your filters" with suggestions.
- Caching (headline): React Query Devtools → filter set A → set B (one fetch) → revert to A → instant, zero new requests. Type in the price field → one debounced request, not one per keystroke.
- i18n/RTL: flip fa↔en — all labels/badges/units/empty copy translate + mirror; dark mode holds.
Mocked behind the seam (how f-next swaps it)
services/search is mock-primary (USE_SEARCH_MOCK = true) because b7's NurseSearchResultDto
row omits the nurse name/avatar/distance, and there is no aggregated public nurse-profile
endpoint (only the b6 trust badge + b5 single-variant read). searchMockApi supplies real-shaped
fixtures so C1/C2/C3 fully demo. The real searchClientApi already maps everything b7/b6 provide and
leaves the missing fields blank; once REQ-012 lands (enrich the search row + add
GET nurses/{id}/profile), the swap is flipping one flag — no hook/component change. (This is a
client-side mock; it is recorded here, not in mocks-registry.md, which tracks backend DI seams.)
Contract consumed / requests filed
- Consumed (not edited):
dev/contracts/domains/search.md(b7) —services/search/types.tsderives from it; b6 trust badge + b5 variant read for the profile scaffold. - Filed:
frontend/requests/for-backend.mdREQ-012 — search-rownurseName/avatarUrl/distanceKm+ an aggregatedGET api/v1/nurses/{id}/profile.
Follow-ups
- f7 booking: the "درخواست رزرو" handoff carries
nurse_id,variant_id,required_gender(the C1 same-gender intent →required_caregiver_gender/b8),city_id,service_category_id,dateas query params to/bookings/request(currently a DEFERRED stub). f7 builds the form + captures the gender into the booking request. - C3 reviews tab: DEFERRED → f13 (only the latest-review snippet ships now).
- DEFERRED (per contract): availability-window hard filter, sorts beyond rating, map/radius discovery.
Gate
npm run check green · npm run test:ci green (165 tests, +8). npm run build compiles and
type-checks clean; the only prerender failure is the pre-existing f5 /nurse/verification
"Missing .env variable!" (needs env set) — unrelated to this phase's routes.