some ui phase improvement planning
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
# UI Phase 13 — Public front door (optional)
|
||||
|
||||
> **Mission (this phase is OPTIONAL — the app is complete without it):** today the entire anonymous web
|
||||
> surface of a trust-first marketplace is `/login`. A family evaluating Balinyaar cannot see a single
|
||||
> service, trust signal, or explanation of escrow before creating an account; there is no marketing page,
|
||||
> no per-page metadata, and the `robots.txt` is starter junk. This phase builds the public front door —
|
||||
> a landing at `/` for unauthenticated visitors, honest how-it-works and trust sections, and a real
|
||||
> SEO/metadata surface — and it starts by **framing the guest-browse product decision** (how deep the
|
||||
> anonymous experience goes) so scope is decided, not drifted into.
|
||||
>
|
||||
> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) · a product
|
||||
> decision on guest browse (§3.1) · **Unlocks:** marketing, SEO, and an acquisition funnel that starts
|
||||
> before login
|
||||
> **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
|
||||
|
||||
Balinyaar's product *is* trust — verified nurses, escrow-held payments, payout only after a confirmed
|
||||
check-out — yet none of that story exists outside an authenticated session. Diagnosed current state
|
||||
(all verified in code):
|
||||
|
||||
1. **There is no public surface.** `client/src/app/[locale]/(public-routes)/` contains exactly two
|
||||
files: `layout.tsx` and `login/page.tsx`. Home, search, results, and nurse profiles are all wrapped
|
||||
in `RoleGuard(customer)` via `(customer)/layout.tsx` ([audit/customer-storefront.md](audit/customer-storefront.md), problem #1).
|
||||
2. **The middleware sends every guest to `/login`.** `client/middleware.ts:23-30` checks
|
||||
`PUBLIC_PATHS.some((p) => pathWithoutLocale.startsWith(p))` and `client/src/constants/routes.ts:169`
|
||||
defines `PUBLIC_PATHS: string[] = [ROUTES.LOGIN]` — so an unauthenticated hit on `/` 307s to
|
||||
`/{locale}/login`. Note the **`startsWith` matching**: `'/'` can never be added to `PUBLIC_PATHS`
|
||||
(every path starts with `/`, which would silently un-gate the whole app).
|
||||
3. **`/` belongs to the customer app.** The `(customer)` route group has no URL segment, so
|
||||
`(customer)/page.tsx` *is* the root route. A second `page.tsx` cannot also resolve to `/` (Next
|
||||
parallel-page collision), so the landing must be served by a **middleware rewrite**, not a sibling page.
|
||||
4. **One static `<title>` for ~60 routes.** The only metadata export in the app is
|
||||
`src/app/[locale]/layout.tsx:54-58` (`'Balinyaar | بالینیار'` + placeholder description). No
|
||||
`generateMetadata`, no OpenGraph, anywhere ([audit/cross-cutting-ux.md](audit/cross-cutting-ux.md)).
|
||||
5. **`client/public/robots.txt` exists but is contradictory starter content** — two `User-agent: *`
|
||||
blocks, `Disallow: /private/` (a path that does not exist in this app) followed by `Allow: /`. There
|
||||
is no sitemap.
|
||||
|
||||
**What already exists (do not rebuild):**
|
||||
|
||||
- The **brand mark, theme pass, and de-startered public shell** from
|
||||
[Phase 0](ui-phase-0-design-language.md) and [Phase 2](ui-phase-2-shells-and-navigation.md); the
|
||||
primitives kit + per-route metadata groundwork from [Phase 1](ui-phase-1-primitives-and-states.md).
|
||||
- `CategoryTile` (`client/src/components/CategoryTile/CategoryTile.tsx`) — takes `label` + `iconKey`
|
||||
props with a safe icon fallback (`KNOWN_CATEGORY_ICONS`: `elderly`, `post_surgery`, `infant`,
|
||||
`chronic`, `companionship`); it works with **static i18n labels**, no API needed.
|
||||
- `EscrowNotice` (`client/src/components/EscrowNotice/EscrowNotice.tsx`) — the **product-mandated
|
||||
verbatim escrow copy** («مبلغ بهصورت امانی نزد بالینیار میماند…»), and `TrustBadge`'s three
|
||||
honest states.
|
||||
- The brand tagline is already a key: `common.brand_tagline` = «مراقبت مطمئن در خانه»
|
||||
(`client/messages/fa.json:54`).
|
||||
- The `[locale]` root layout already renders honest `lang`/`dir` per locale and loads Mikhak fa-only
|
||||
(`src/app/[locale]/layout.tsx`) — this phase builds **on top of** it, never above it.
|
||||
- The auth machinery: middleware gate, `RoleRouter`/`resolveRoleDestination`
|
||||
(`client/src/services/auth/routing.ts` — a customer resolves to `ROUTES.HOME` = `/`), `RoleGuard` on
|
||||
the four shells (refinement phase 2). None of it changes semantics in this phase.
|
||||
- If [Phase 3](ui-phase-3-auth-and-first-run.md) has already run: the terms/privacy pages and the
|
||||
login `returnUrl` capture. If [Phase 4](ui-phase-4-customer-storefront.md) has run: the
|
||||
verification-explainer content (whatever component name its report gives it). Reuse both; see §3.2.
|
||||
|
||||
## 2. Required reading (do this first)
|
||||
|
||||
- [audit/customer-storefront.md](audit/customer-storefront.md) — problem #1 (no public storefront) and
|
||||
the "Public landing + guest browse" opportunity this phase implements; the **Keep** list you must not
|
||||
regress (token discipline, four data states, trust honesty, money handling).
|
||||
- [audit/cross-cutting-ux.md](audit/cross-cutting-ux.md) — the metadata/404/route-chrome findings and
|
||||
the "Public landing + public nurse profiles" opportunity; its **Keep** list (RTL habits, Mikhak
|
||||
fa-only loading, locale/dir wiring reasoning).
|
||||
- Code: `client/middleware.ts` (the whole file — the i18n 307/308 early-return, the `PUBLIC_PATHS`
|
||||
check, the locale header), `client/src/constants/routes.ts` (`ROUTES`, `PUBLIC_PATHS`),
|
||||
`client/src/app/[locale]/layout.tsx` (root metadata + the "why no layout above [locale]" comment),
|
||||
`client/src/app/[locale]/(public-routes)/layout.tsx` (client layout wrapping `PublicLayout` — RSC
|
||||
children still render server-side), `(customer)/page.tsx` (the authenticated home that owns `/`),
|
||||
`client/src/services/auth/routing.ts` + `client/src/components/auth/RoleRouter.tsx`,
|
||||
`client/src/components/CategoryTile/CategoryTile.tsx`, `client/src/components/EscrowNotice/EscrowNotice.tsx`.
|
||||
- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) —
|
||||
the design contract (invoke the skill; §7 non-negotiables all apply here).
|
||||
- Product: [../../../product/overview/platform-summary.md](../../../product/overview/platform-summary.md)
|
||||
(the four ground truths — everything the landing claims must trace to them) and
|
||||
[../../../product/business/index.md](../../../product/business/index.md) (verification + escrow rules,
|
||||
so marketing copy is honest). [../../../product/notes/open-questions.md](../../../product/notes/open-questions.md)
|
||||
is where §3.1's decision gets recorded.
|
||||
- The REQ tracker: [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
|
||||
— REQ-001…038 existed before this chain; earlier UI phases may have appended more. **Check the file
|
||||
for the next free number before filing.**
|
||||
|
||||
## 3. Scope — build this
|
||||
|
||||
### 3.1 The product decision — do this FIRST, and write it down
|
||||
|
||||
Frame guest-browse depth as three tiers and get an explicit decision before building:
|
||||
|
||||
- **(a) Landing only** — a marketing page at `/`; zero data, zero new endpoints.
|
||||
- **(b) Landing + static public pages** — (a) plus category/how-it-works content pages; still zero
|
||||
data (the category grid is static i18n content, not the catalog API).
|
||||
- **(c) Guest search + public nurse profiles** — read-only anonymous variants of the phase-4 search
|
||||
results and nurse-profile screens. **Requires public read endpoints that do not exist** (all search
|
||||
and profile reads sit behind the auth middleware and cookie-bearing `clientFetch`), so tier (c) is
|
||||
REQ-gated backend work plus a privacy review of which nurse fields may be exposed logged-out.
|
||||
|
||||
**Recommendation this phase encodes: ship (a)+(b) now; frame (c) as a REQ-gated follow-up** (file the
|
||||
REQs as proposals, §3.4 — do not build guest search against endpoints that don't exist). Record the
|
||||
decision (chosen tier, rationale, what tier (c) would need) in this phase's report **and** append it to
|
||||
[../../../product/notes/open-questions.md](../../../product/notes/open-questions.md) (edit the `.md`;
|
||||
the `.html` view is generated — `cd product && node build-docs.mjs`).
|
||||
|
||||
### 3.2 Public landing at `/` for unauthenticated visitors
|
||||
|
||||
**Routing mechanics (get this exactly right):**
|
||||
|
||||
- Create the landing as an **RSC** at `client/src/app/[locale]/(public-routes)/welcome/page.tsx`
|
||||
(the client `(public-routes)/layout.tsx` is fine — RSC children of a client layout still render on
|
||||
the server).
|
||||
- In `client/middleware.ts`, after the i18n 307/308 early-return: if the request is **unauthenticated
|
||||
and `pathWithoutLocale === '/'`**, `NextResponse.rewrite()` to `/{locale}/welcome` — the visitor sees
|
||||
the landing **at the URL `/`** (good for SEO canonical). Do **not** redirect.
|
||||
- Add `/welcome` (a named constant in `ROUTES`) to `PUBLIC_PATHS`. **Never add `'/'`** — the
|
||||
`startsWith` match would make every route public (§1.2).
|
||||
- An **authenticated** user hitting `/welcome` directly → redirect to `/` (a signed-in customer lands
|
||||
on their home, exactly as today). Authenticated `/` is untouched — `(customer)/page.tsx` keeps
|
||||
serving it, `RoleGuard`/`RoleRouter` behavior unchanged.
|
||||
- Preserve the middleware's existing behavior for every other path: the locale-header injection, the
|
||||
next-intl `Link: alternate` hreflang headers it copies through, and the redirect-to-login for private
|
||||
paths (including the `returnUrl` capture if Phase 3 has added it — test that flow after your change).
|
||||
|
||||
**Landing sections (mobile-first, in order):**
|
||||
|
||||
1. **Hero** — brand mark (phase 0), the existing tagline `common.brand_tagline`
|
||||
(«مراقبت مطمئن در خانه»), one supporting sentence, and a primary CTA to `/login` (label e.g.
|
||||
«شروع کنید»). No carousel, no stock photography of fake nurses.
|
||||
2. **Category grid** — reuse `CategoryTile` with **static i18n labels** keyed to the five
|
||||
`KNOWN_CATEGORY_ICONS` keys (elderly / post_surgery / infant / chronic / companionship). Each tile
|
||||
links to `/login` (tier a+b: intent capture, not guest search). This section needs a small client
|
||||
wrapper only if tiles navigate via router — prefer plain `AppLink`-wrapped tiles to keep the page RSC.
|
||||
3. **How it works — 3 steps:** «جستجوی پرستار تاییدشده» → «پرداخت امن امانی» → «مراقبت با خیال راحت».
|
||||
Step 2's supporting text reuses the `EscrowNotice` verbatim copy (same i18n key or the component
|
||||
itself) — never a paraphrase.
|
||||
4. **Verification/trust explainer** — the "what we verify" story (identity, nursing license, INO
|
||||
membership). If Phase 4 has shipped its verification-explainer content, reuse those keys/components;
|
||||
if this phase runs before Phase 4, write the section from
|
||||
[../../../product/business/index.md](../../../product/business/index.md)'s pipeline and note in the
|
||||
report that Phase 4 should fold its explainer into the same keys.
|
||||
5. **Nurse recruitment** — «پرستار هستید؟ به بالینیار بپیوندید» with a secondary CTA to `/login` with
|
||||
the nurse intent the login screen already supports (the A1/B1 role switch from f1/phase 3).
|
||||
6. **Footer** — links to the terms/privacy pages if Phase 3 has shipped them (omit gracefully and note
|
||||
it in the report if not), a contact affordance, and the locale switcher from the phase-2 public shell.
|
||||
|
||||
### 3.3 SEO & metadata for the public surface
|
||||
|
||||
- `generateMetadata` on the landing (and login + any phase-3 public pages if present): **localized**
|
||||
title/description via `getTranslations`, a title template `%s | بالینیار`, and
|
||||
`alternates.canonical` pointing at `/{locale}` for the landing (the rewrite means `/welcome` and `/`
|
||||
serve the same content — canonicalize on `/`).
|
||||
- **OpenGraph:** `og:title`/`og:description` per locale + one static OG image (1200×630) at
|
||||
`client/public/og/balinyaar-og.png`, built from the phase-0 brand mark on the brand teal/cream. Set
|
||||
`metadataBase` from an env constant (e.g. `NEXT_PUBLIC_SITE_URL`) — never a hard-coded origin.
|
||||
- **Replace** the contradictory `client/public/robots.txt` with an app-router `src/app/robots.ts`
|
||||
(delete the static file), allowing the public routes and disallowing the private roots
|
||||
(`/*/nurse`, `/*/admin`, `/*/partner`, `/*/bookings`, …); add `src/app/sitemap.ts` listing **public
|
||||
routes only** (`/fa`, `/en`, login, terms/privacy when they exist) with locale alternates.
|
||||
- `lang`/`dir` per locale is already correct in `src/app/[locale]/layout.tsx` — **do not regress it,
|
||||
and NEVER add a layout (or robots/sitemap-driven layout tricks) above `[locale]`** (golden rule #1;
|
||||
`robots.ts`/`sitemap.ts` are metadata routes, not layouts — they are safe at `src/app/`).
|
||||
|
||||
### 3.4 Tier (c): guest search + public nurse profiles (DEFERRED unless explicitly approved in §3.1)
|
||||
|
||||
If — and only if — the §3.1 decision approves tier (c): build guest search results and a public nurse
|
||||
profile as **read-only variants of the phase-4 screens** (no booking CTA past login, no address/PII),
|
||||
behind new public endpoints. Either way, **file the REQs now as proposals** in
|
||||
[for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) (next free numbers —
|
||||
check the tracker, ≥ REQ-039):
|
||||
|
||||
- **Public search read** — anonymous, rate-limited variant of the nurse search (verified-only invariant
|
||||
preserved; no customer-context fields).
|
||||
- **Public nurse profile read** — privacy-reviewed field set (display name, photo, verified badge
|
||||
state, rating aggregate, service/price rows; **never** phone, exact areas, or document data).
|
||||
|
||||
Everything else about tier (c) — routes, guest-to-login handoff at the «درخواست رزرو» tap — is
|
||||
**(DEFERRED → a follow-up phase once the REQs are delivered)**.
|
||||
|
||||
### 3.5 Performance sanity
|
||||
|
||||
The landing is the one page where first paint is the product. **RSC-first, zero client data fetching
|
||||
above the fold** (the whole page needs no API at tier a+b), no new client-side libraries, images through
|
||||
`AppImage`/`next/image` with explicit dimensions, and the per-locale font strategy untouched (Mikhak is
|
||||
already fa-only, `preload: false` — do not "optimize" it into loading for `/en`). Any interactive island
|
||||
(e.g. a locale switcher) stays a leaf client component.
|
||||
|
||||
## 4. Mocks & seams in this phase
|
||||
|
||||
**None.** Tiers (a)+(b) are static content — no service calls, no new `services/{domain}` seams, no mock
|
||||
flags. The REQ posture: 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 pre-date this chain; check the tracker for the next free number). This phase files the two
|
||||
tier-(c) proposals in §3.4 and builds nothing against them.
|
||||
|
||||
## 5. Critical rules you must not get wrong
|
||||
|
||||
- **This phase is optional and must stay reversible.** If it is skipped or aborted mid-way, the app must
|
||||
behave exactly as today (guests → `/login`). Keep the middleware change small and additive.
|
||||
- **Auth boundaries untouched.** Private routes stay private; `RoleGuard`/`RoleRouter`/
|
||||
`resolveRoleDestination` semantics unchanged; the middleware remains the auth gate. The only new
|
||||
public surface is what §3.2/§3.3 name.
|
||||
- **The `PUBLIC_PATHS` `startsWith` trap:** never add `'/'` — root goes public via the exact-match
|
||||
rewrite branch, not the list (`client/middleware.ts:23`, `routes.ts:169`).
|
||||
- **Do not break the login flow:** the phase-3 `returnUrl` capture (if shipped) and next-intl locale
|
||||
detection/normalization (the 307/308 early-return and the copied `Link` hreflang headers) must survive
|
||||
the middleware edit.
|
||||
- **No marketing claims the product can't honor.** Escrow copy is the `EscrowNotice` verbatim string;
|
||||
"verified" copy describes the real pipeline; **no invented numbers** («۵۰۰۰ پرستار», fake ratings,
|
||||
fake testimonials). Trust-first means the landing is honest-first.
|
||||
- **Copy discipline:** every string in **both** `messages/en.json` and `messages/fa.json`; new strings
|
||||
use the ZWNJ brand spelling «بالینیار» (phase 12 canonicalizes the legacy spaced form — don't add
|
||||
more of it); formal شما register.
|
||||
- **Design contract:** tokens not hexes (`--bal-*` / palette keys), terracotta stays the single sparing
|
||||
accent, RTL logical props only, dark mode verified, MUI v9 API only, icons via the `AppIcon` registry,
|
||||
`App*` wrappers before raw MUI. Any new **shared** component gets a co-located `*.test.tsx`.
|
||||
- **Never add a layout above `[locale]`** — `lang`/`dir` would freeze on the default locale (the root
|
||||
layout's comment explains why). `robots.ts`/`sitemap.ts` at `src/app/` are fine; a layout is not.
|
||||
- **Fetch/cookies rules untouched** — no raw `fetch()`, no cookie reads outside `@/lib/cookies/*`
|
||||
(at tier a+b you should need neither).
|
||||
|
||||
## 6. Definition of Done
|
||||
|
||||
On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
|
||||
|
||||
- [ ] The §3.1 decision is written down (report + `product/notes/open-questions.md`) **before** the
|
||||
landing was built, and the built scope matches it.
|
||||
- [ ] `npm run check` green; `npm run test:ci` green for any touched/added shared components;
|
||||
`en.json`/`fa.json` in sync.
|
||||
- [ ] An **unauthenticated** visit to `/` renders the landing at the URL `/` (rewrite, not redirect),
|
||||
in both locales; an **authenticated** customer at `/` still gets the customer home; `/welcome`
|
||||
while authenticated redirects to `/`.
|
||||
- [ ] Every other unauthenticated private path still redirects to `/login` (spot-check `/bookings`,
|
||||
`/nurse`), and login → role routing works exactly as before (including `returnUrl` if phase 3 ran).
|
||||
- [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and on mobile + desktop
|
||||
widths for the landing (it is the page strangers judge the product by).
|
||||
- [ ] View-source of the landing shows the localized `<title>`/description, OG tags with an absolute
|
||||
image URL, and correct `lang`/`dir`; `/robots.txt` and `/sitemap.xml` serve the §3.3 content
|
||||
(starter `robots.txt` deleted).
|
||||
- [ ] The landing performs no client-side data fetching (Network tab: no `/api/v1/*` calls while
|
||||
logged out) and no marketing string contradicts `EscrowNotice`/`TrustBadge` semantics.
|
||||
- [ ] Tier-(c) REQs filed as proposals in the tracker with the next free numbers.
|
||||
|
||||
## 7. How to test (what a human can verify after this phase)
|
||||
|
||||
1. In a private/incognito window, open `http://localhost:3000` → normalized to `/fa`, the **landing**
|
||||
renders (URL stays `/fa`, no `/login` redirect, no `/welcome` in the address bar): hero + tagline,
|
||||
5 category tiles, 3-step how-it-works with the escrow sentence, trust explainer, nurse CTA, footer.
|
||||
2. Switch to `/en` → LTR landing, English copy, system font (Network tab: no Mikhak woff2). Toggle dark
|
||||
mode → all sections stay token-correct.
|
||||
3. Still logged out, visit `/fa/bookings` → redirected to `/fa/login` exactly as before this phase.
|
||||
4. Tap the hero CTA → `/login`; complete the seeded customer OTP login → land on `/` and see the
|
||||
**customer home** (not the landing). Manually revisit `/fa/welcome` while logged in → redirected to `/fa`.
|
||||
5. Tap «پرستار هستید؟…» while logged out → login screen in its nurse-intent mode (B1 switch preselected).
|
||||
6. View source on the logged-out landing: localized `<title>` (`… | بالینیار` on `/fa`), meta
|
||||
description, `og:image` absolute URL, `lang="fa" dir="rtl"` (and `lang="en" dir="ltr"` on `/en`).
|
||||
7. Open `http://localhost:3000/robots.txt` → the new rules (private roots disallowed) and the sitemap
|
||||
reference; `http://localhost:3000/sitemap.xml` → public routes only, both locales.
|
||||
8. Confirm in the report/tracker: the §3.1 decision recorded, the two tier-(c) REQs filed, and (if
|
||||
phase 3 hadn't run) the noted footer-legal-links gap.
|
||||
|
||||
## 8. Hand off & document (close the phase)
|
||||
|
||||
- Update `client/CLAUDE.md` **Project Structure**: the new `(public-routes)/welcome` route (and the
|
||||
middleware's rewrite behavior in the routing/middleware description), plus `robots.ts`/`sitemap.ts`.
|
||||
- Append the §3.1 decision to [../../../product/notes/open-questions.md](../../../product/notes/open-questions.md)
|
||||
and regenerate the docs HTML (`cd product && node build-docs.mjs`).
|
||||
- Write the frontend report at `dev/shared-working-context/reports/ui-phase-13-report.md`: the tier
|
||||
decision + rationale, the middleware change (with the `PUBLIC_PATHS` trap called out for future
|
||||
agents), sections shipped, REQ numbers filed, and any graceful omissions (footer legal links,
|
||||
verification-explainer reuse status).
|
||||
- List the REQs filed (tier-(c) public search read + public nurse profile read) with their final numbers.
|
||||
- Save a memory note per operating-rules §8: the public front door exists, `/` now forks by auth via a
|
||||
middleware **rewrite** (never `'/'` in `PUBLIC_PATHS`), tier (c) is REQ-gated and unbuilt.
|
||||
Reference in New Issue
Block a user