ui phase 13

This commit is contained in:
hamid
2026-07-20 01:35:15 +03:30
parent d33568bf31
commit 12ce7fa7de
21 changed files with 868 additions and 80 deletions
@@ -1076,3 +1076,40 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a
- **Proposed shape:** see **Need** above; masked phone follows the existing `maskIranMobile` convention
(`"0912•••1234"`, first-4/last-4) — never the full number.
- **Status:** open
## REQ-066 — Public (anonymous), rate-limited nurse-search read for guest browse (tier c) — filed by ui-phase-13 — 2026-07-19
- **Need:** An **unauthenticated** variant of the f6 nurse-search read — proposed
`GET api/v1/public/nurse_search?category_id=&province_id=&city_id=&district_id=&gender=&page=&pageSize=`
(no auth header required) → the same shape `search/nurses` already returns
(`NurseSearchResultDto[]` + pagination), preserving every existing invariant: **verified-only**
results (the `is_searchable` gate f7 already enforces), no customer-context fields (no distance-
from-my-address, no "your last booking with"), and **rate-limited per IP** (this is the one read in
the whole app reachable with zero login, so it is also the one most exposed to scraping/abuse).
- **Why:** ui-phase-13 framed three guest-browse tiers (§3.1 of the phase file) and the product
decision (recorded in `product/notes/open-questions.md`) was to ship (a) landing + (b) static public
pages **now** and defer (c) guest search + public nurse profiles as a REQ-gated follow-up — this
route is what a future phase would need to build a real (not placeholder) guest search results
screen. Nothing in this phase calls it; it is filed so the decision and its cost are on record, not
guessed at when someone picks tier (c) up later.
- **Proposed shape:** identical response shape to the existing (authenticated) `search/nurses`, served
from a route that bypasses the auth middleware entirely (a new, explicitly public controller/route —
not a "make search/nurses optionally-authenticated" flag, which would risk quietly widening what an
authenticated call can already see).
- **Status:** open
## REQ-067 — Public (anonymous), privacy-reviewed nurse-profile read for guest browse (tier c) — filed by ui-phase-13 — 2026-07-19
- **Need:** An **unauthenticated** nurse-profile read, deliberately narrower than the authenticated
`GET nurses/{id}/profile` (REQ-012): `{ nurseId, displayName, avatarUrl, bio, yearsExperience,
averageRating, totalReviews, isVerified, credentialTypes: string[], services: [{ variantId,
displayName, priceIrr, priceUnit, sessionCount? }] }`. **Never** phone, exact coverage
area/district, document/credential numbers, or patient-identifying data from any linked booking —
this is the field set a logged-out stranger may see, which is why it needs an explicit privacy
review before it's built, not an assumption that "the authenticated shape minus a filter" is safe.
- **Why:** same tier-(c) framing as REQ-066 — a guest who found a nurse via public search needs a
profile to land on before the "درخواست رزرو" CTA hands them to `/login`. Filed as a proposal only;
the phase explicitly defers building any guest-facing profile screen against it (§3.4 of
`dev/post-phase/ui/ui-phase-13-public-front-door.md`).
- **Proposed shape:** as above. A privacy sign-off on the exact field list (not just an engineering
guess) should happen before this is implemented — flagging here so it isn't skipped when tier (c)
is picked up.
- **Status:** open
@@ -0,0 +1,162 @@
# UI Phase 13 — Public Front Door — Report (2026-07-19)
## The §3.1 decision (recorded first, per the phase's own gate)
Framed the three guest-browse tiers and decided **ship (a) landing + (b) static public pages now;
defer (c) guest search + public nurse profiles as a REQ-gated follow-up**. Full rationale recorded in
[product/notes/open-questions.md](../../../product/notes/open-questions.md) ("Decided — public
guest-browse depth" section). Tier (c)'s two endpoints are filed as REQ-066/REQ-067 (see Contracts
below) — nothing in this phase calls them; no guest search/profile screen was built.
## What was built
- **The guest front door itself**`client/middleware.ts`: after the existing next-intl 307/308
early-return, an **unauthenticated** exact match on `pathWithoutLocale === '/'` is
`NextResponse.rewrite()`d to `/{locale}/welcome` (never a redirect — the browser URL and SEO
canonical stay `/`). An **authenticated** hit on `/welcome` redirects to `/`. Every other path's
behavior (locale detection, the private-route → `/login?next=` redirect, the `returnUrl` capture,
the copied next-intl hreflang headers) is untouched — confirmed by direct testing, not just code
reading (see "What is now testable").
- **`ROUTES.WELCOME` (`/welcome`)** added to `src/constants/routes.ts` and to `PUBLIC_PATHS`. A
comment on `PUBLIC_PATHS` (and in the middleware) calls out the `startsWith`/`'/'` trap explicitly
for the next agent who touches either file.
- **The landing** at `client/src/app/[locale]/(public-routes)/welcome/`:
- `page.tsx` — thin RSC, `generateMetadata` (description, `alternates.canonical``/{locale}`,
`openGraph`). Deliberately does **not** override `title` — it inherits the root layout's default
(`"بالین‌یار"` / `"Balinyaar"`), which is also how view-source tells it apart from the customer
home's own `shell.customer_app` title when both serve at the same `/{locale}` URL depending on
auth state.
- `WelcomeScreen.tsx` — an **async Server Component**, not a `'use client'` screen (no page in this
app has needed that treatment before; here it's the point — zero query hooks anywhere in the
tree). Sections, in order: hero (`BrandMark` + tagline + subtitle + CTA to `/login`), a static
5-tile category grid (`CategoryTile`, static i18n labels — see the CategoryTile change below), a
3-step how-it-works (step 2 renders the actual `<EscrowNotice />` component, never a paraphrase),
a static trust/verification explainer (identity/license/INO/bank — written from
`product/business/02-nurse-verification.md`, since `VerificationPanel` requires a live
`nurseId`-keyed badge fetch and can't be reused on a zero-data page), a nurse-recruitment CTA
linking to `/login?role=nurse`, and a footer (terms/privacy links, a static contact note, no
duplicate locale switcher — `PublicLayout`'s corner strip already provides one for every public
route including this one).
- `opengraph-image.tsx``next/og`'s `ImageResponse`, a brand-mark composition (teal background,
the logo shape reproduced in plain divs, terracotta accent dot, "Balinyaar" wordmark) at 1200×630.
**Latin-only**`ImageResponse`'s bundled fallback font doesn't cover Persian glyphs, and
embedding a Mikhak font buffer wasn't verified working in the time available; flagged as a
follow-up, not silently skipped.
- **`CategoryTile` gained an `href` prop** (`client/src/components/CategoryTile/CategoryTile.tsx`):
when given, the tile renders via `ButtonBase`'s `component` swap to `AppLink` (the tile itself
*becomes* the anchor) instead of a click handler — the same polymorphic pattern `AppButton` already
uses, so there's no button nested inside a link (invalid HTML) and no new client wrapper component
needed just to make a static tile navigate. Existing `onClick`/`selected` callers are unaffected.
Test coverage added (`CategoryTile.test.tsx`): renders as a link with the given `href`, and no
`button` role is present when `href` is set.
- **SEO/metadata infrastructure:**
- `SITE_URL` constant (`client/src/config.ts`, from `NEXT_PUBLIC_SITE_URL`, falls back to
`http://localhost:3000`) — the root layout's `generateMetadata` now sets
`metadataBase: new URL(SITE_URL)`, so every child page's relative OG/canonical URLs resolve
absolutely.
- `src/app/robots.ts` **replaces** the old contradictory static `public/robots.txt` (deleted):
allows the public surface, disallows every private route root (`nurse`, `admin`, `partner`,
`bookings`, `patients`, `addresses`, `wallet`, `profile`, `support`, `notifications`,
`select-role`, `onboarding`, `search`) for both locales via a wildcard pattern, points at
`sitemap.xml`.
- `src/app/sitemap.ts` — public routes only (`/`, `/login`, `/terms`, `/privacy`) × both locales,
with `hreflang` alternates.
## What is now testable (and exactly how)
**Important caveat first:** the **dev server** (`npm run dev`, Turbopack) in this sandbox did not
reliably invoke middleware for the *literal* root path `/` — verified down to a minimal
`matcher: ['/']` control middleware that unconditionally returned a JSON body and still never fired
for `/` (a sibling `/ping` path with the identical matcher also silently fell back to a stale cached
render instead of hitting the handler). This reproduced even after full `.next` wipes and process
restarts, and is **not specific to this phase's logic** — a **production build** (`npm run build` +
`npm run start`) exhibits none of it and confirms the intended behavior end-to-end (below). Treat this
as a known dev-server quirk in this Next 16.2.9/Turbopack build, not a defect in the shipped code; a
human should re-confirm with `npm run dev` on their own machine before assuming it's universal.
Verified against a **production** server (`NEXT_PUBLIC_API_URL=... npm run build && npm run start`):
1. `GET /` (no cookies) → `307``Location: /fa` (next-intl's own redirect, untouched).
2. `GET /fa` (no cookies) → `200`, response header `x-middleware-rewrite: /fa/welcome`; body's
`<title>` is the bare default `"بالین‌یار"` (not the customer app's `"اپلیکیشن خانواده | بالین‌یار"`)
and contains the hero copy/CTA — the landing, not the customer home.
3. `GET /fa/welcome` directly → `200`, same landing content.
4. `GET /en` (no cookies) → `200`, `lang="en" dir="ltr"`, no Mikhak reference, title `"Balinyaar"`.
5. With a **valid, unexpired `access_token` cookie** (forged payload for testing — `isTokenAlive` only
checks `exp`, never the signature, by design): `GET /fa``200` with `<title>` =
`"اپلیکیشن خانواده | بالین‌یار"` — the **customer home**, confirming an authenticated `/` is
untouched. `GET /fa/welcome``307``/fa/` (redirected away from the marketing page). `GET
/fa/bookings` → `200` (private route now reachable).
6. Without the cookie: `GET /fa/bookings``307``/fa/login?next=%2Fbookings` — the private-route
gate and `returnUrl` capture are unchanged.
7. `GET /robots.txt` → the new allow/disallow rules + `Sitemap: .../sitemap.xml`.
8. `GET /sitemap.xml` → the 4 public paths × 2 locales with `hreflang` alternates.
9. `GET /fa` view-source → `og:title`, `og:description`, `og:image` (an **absolute** URL),
`og:image:width/height` (1200×630), `canonical``/fa`.
10. `GET` the `opengraph-image` route directly → `200`, `Content-Type: image/png`; verified the bytes
are a real 1200×630 PNG (`PNG image data, 1200 x 630, 8-bit/color RGBA`).
`npm run check` (type + lint + `lint:copy`) is green. `npm run test:ci` — all 115 suites / 527 tests
pass, including the extended `CategoryTile.test.tsx`.
Not independently verified in this session (no running backend, no browser): the actual OTP
login→`RoleRouter` round trip after tapping the hero CTA (the login screen itself is unchanged by
this phase); visual dark-mode/RTL inspection of the landing (code follows the same token/RTL
conventions as every other screen, but wasn't eyeballed in a real browser).
## What is mocked / waiting on a real service
Nothing new. Tiers (a)+(b) are 100% static content — no service calls, no mock flags, no
`services/{domain}` seam touched.
## Contracts
- **Consumed:** none (no data fetching on this page).
- **Filed (proposals only, not built against):**
- **REQ-066** — public (anonymous), rate-limited nurse-search read, for a future tier-(c) guest
search screen.
- **REQ-067** — public (anonymous), privacy-reviewed nurse-profile read, for a future tier-(c)
guest profile screen.
- Both in
[for-backend.md](../frontend/requests/for-backend.md), status `open`; each explicitly says the
frontend has not built anything against it.
## Docs updated
- `product/notes/open-questions.md` — the §3.1 decision + rationale (and the docs HTML was
regenerated: `cd product && node build-docs.mjs`).
- `client/CLAUDE.md`:
- Project Structure — `middleware.ts`'s description (the rewrite + the bare-root matcher-entry
gotcha), `src/app/robots.ts`/`sitemap.ts`, the whole `(public-routes)/welcome/` subtree, and
`CategoryTile`'s new `href` mode.
- Per-page metadata section — `metadataBase`/`SITE_URL` and the `opengraph-image.tsx` convention.
- i18n section — the `welcome` namespace description.
- Route Constants section — the `PUBLIC_PATHS`/`'/'` trap, spelled out again at the point a future
agent is most likely to be editing.
## Follow-ups for later phases
- **Tier (c)** (guest search + public nurse profiles) — REQ-066/067 need a backend phase plus an
explicit privacy sign-off on the nurse-profile field list before any guest-facing search/profile
screen is built. Do not build against a guessed shape.
- **OG image Persian variant** — currently Latin-only by deliberate scope cut (see `opengraph-image.tsx`'s
own comment). A follow-up could embed a Mikhak font buffer (`fs.readFile` the existing
`src/app/fonts/Mikhak-Bold.woff2`) and verify Satori/`next/og` renders Persian glyphs correctly in a
real running server before shipping it — not verified in this session.
- **Dev-server root-path quirk** — if a future phase also needs middleware to act on the literal `/`
segment, re-confirm on a real (non-sandboxed) `npm run dev` whether the matcher gotcha reproduces
there too; if so, the `matcher: ['/', '/((?!_next|_vercel|api|.*\\..*).*)']` fix already applied here
covers it, but worth a second look outside this session's environment.
- **Footer legal links** — phase 3 had already shipped `/terms`/`/privacy` by the time this phase ran,
so the footer links to both (no graceful-omission gap to report).
- **Verification-explainer reuse** — phase 4/8 had already shipped, but their `VerificationPanel`
needs a live `nurseId` badge query and can't be reused as-is on a zero-fetch landing; the trust
section here is independently written static copy sourced from the same product doc. A future pass
could extract a shared *static* label set (`step_identity_kyc`, `step_moh_competency_license`, …
already exist in the `verification` namespace) if keeping the two copies in sync becomes a problem.
## Save memory note
See `MEMORY.md``ui_phase_13_public_front_door.md` for the durable cross-session note (the
rewrite-not-redirect mechanic, the `PUBLIC_PATHS`/`'/'` trap, the dev-server root-path quirk, tier (c)
status).