frontend phase 0: app shells, design system & data/contract patterns
Turn the starter into the Balinyaar foundation for the three actor
experiences and lock in the patterns later phases copy.
- Cleanup: remove toastDemo namespace, placeholder home page, and the two
dead icons; fix BottomBar to use usePathname (locale-aware active tab).
- Three actor shells under (private-routes), no layout above [locale]:
customer (customer) group with the 5-tab bottom nav; nurse (/nurse) and
admin (/admin) on the shared sidebar engine. Role model via constants/roles
+ useActorRole (defaults to customer until roles land in f1-b2).
- services/{domain} reference (patients) with a mock behind a config seam,
hierarchical query keys, deliberate staleTime, and mutation invalidation;
shared ApiEnvelope/Paginated wire types + unwrap() in lib/api/types.
- Money (integer-safe IRR/Toman) + Shamsi-date utils; toEnglishDigits helper.
- Shared composites, each tested: OtpInput, PhoneNumberField, StepperHeader,
StatusChip, PlaceholderScreen.
- i18n: seed nav/common/shell/patients in both locales; document namespace
conventions. Update client/CLAUDE.md Project Structure + fix ColorSchemeScript
doc drift. Add phase report, STATUS, and REQ-001 (envelope/casing/pagination).
Gate: npm run check + test:ci green (72 tests); build green with NEXT_PUBLIC_API_URL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+72
-16
@@ -114,18 +114,42 @@ client/
|
||||
│ └── [locale]/
|
||||
│ ├── layout.tsx # ROOT RSC: renders <html lang/dir> + fonts + setRequestLocale + NextIntlClientProvider + ThemeProvider + AuthProvider (seeded via getServerAuthState)
|
||||
│ ├── (private-routes)/
|
||||
│ │ ├── layout.tsx # 'use client' — wraps PrivateLayout
|
||||
│ │ └── page.tsx
|
||||
│ │ ├── layout.tsx # 'use client' — wraps PrivateLayout (auth shell)
|
||||
│ │ ├── (customer)/ # Customer (family) app — mobile-first, bottom-tab nav; no URL segment
|
||||
│ │ │ ├── layout.tsx # 'use client' — wraps CustomerLayout
|
||||
│ │ │ ├── page.tsx # / (home)
|
||||
│ │ │ ├── bookings/page.tsx # /bookings
|
||||
│ │ │ ├── patients/page.tsx # /patients — reference services/{domain} + Query screen
|
||||
│ │ │ ├── wallet/page.tsx # /wallet
|
||||
│ │ │ └── profile/page.tsx # /profile
|
||||
│ │ ├── nurse/ # Nurse app (/nurse/…) — sidebar shell
|
||||
│ │ │ ├── layout.tsx # 'use client' — wraps NurseLayout
|
||||
│ │ │ ├── page.tsx # /nurse (dashboard)
|
||||
│ │ │ ├── verification/page.tsx # /nurse/verification
|
||||
│ │ │ └── visits/page.tsx # /nurse/visits (EVV)
|
||||
│ │ └── admin/ # Admin/backoffice (/admin/…) — desktop sidebar shell
|
||||
│ │ ├── layout.tsx # 'use client' — wraps AdminLayout
|
||||
│ │ ├── page.tsx # /admin (overview)
|
||||
│ │ ├── users/page.tsx # /admin/users
|
||||
│ │ └── notifications/page.tsx # /admin/notifications
|
||||
│ └── (public-routes)/
|
||||
│ └── layout.tsx # 'use client' — wraps PublicLayout
|
||||
├── components/ # Shared UI components (each with .test.tsx if imported >1 place)
|
||||
│ ├── PlaceholderScreen/ # Empty-state scaffold for not-yet-built screens
|
||||
│ ├── OtpInput/ # OTP code input (auto-advance, paste, RTL-safe)
|
||||
│ ├── PhoneNumberField/ # Iranian mobile field (digit-normalizing, LTR-in-RTL)
|
||||
│ ├── StepperHeader/ # Progress header for onboarding/verification flows
|
||||
│ └── StatusChip/ # Semantic status chip (verified/pending/rejected/…) off --bal-* tokens
|
||||
├── i18n/
|
||||
│ ├── routing.ts # defineRouting — locales: ['en', 'fa'], defaultLocale: 'fa'
|
||||
│ └── request.ts # getRequestConfig — loads messages/${locale}.json
|
||||
├── layout/
|
||||
│ ├── PrivateLayout.tsx # 'use client' — authenticated shell; uses useTranslations('nav')
|
||||
│ ├── PrivateLayout.tsx # authenticated wrapper (passthrough today); actor chrome lives in the shells below
|
||||
│ ├── CustomerLayout.tsx # 'use client' — customer shell: TopBar + BottomBar (5-tab); useTranslations('nav')
|
||||
│ ├── NurseLayout.tsx # 'use client' — nurse shell via TopBarAndSideBarLayout; useTranslations('nav')
|
||||
│ ├── AdminLayout.tsx # 'use client' — admin shell via TopBarAndSideBarLayout (persistent sidebar)
|
||||
│ ├── PublicLayout.tsx # unauthenticated shell
|
||||
│ ├── TopBarAndSideBarLayout.tsx # 'use client' — TopBar + SideBar composition
|
||||
│ ├── TopBarAndSideBarLayout.tsx # 'use client' — TopBar + SideBar composition (nurse/admin engine)
|
||||
│ ├── config.ts
|
||||
│ ├── index.ts
|
||||
│ └── components/
|
||||
@@ -139,6 +163,7 @@ client/
|
||||
│ ├── api/
|
||||
│ │ ├── client.ts # clientFetch<T> — throws ApiError on error; use in hooks/client components
|
||||
│ │ ├── server.ts # serverFetch<T> — throws ApiError on error; use in RSCs/Server Actions
|
||||
│ │ ├── types.ts # ApiEnvelope<T> + unwrap(), Paginated<T>, PageParams — shared wire types
|
||||
│ │ └── errors.ts # ApiError class (status, message, code)
|
||||
│ ├── auth/
|
||||
│ │ ├── token.ts # decodeJwtPayload / isTokenAlive — edge-safe, shared with middleware (no next/headers)
|
||||
@@ -152,28 +177,33 @@ client/
|
||||
│ ├── client.ts # getClientCookie, setClientCookie, deleteClientCookie
|
||||
│ └── index.ts # Re-exports constants ONLY (never server/client)
|
||||
├── services/ # Domain services — no top-level barrel; import directly from the file
|
||||
│ ├── auth/ # Reference domain (login/logout/currentUser)
|
||||
│ ├── patients/ # Reference domain for the mock-behind-a-seam pattern (§ services pattern)
|
||||
│ └── {domain}/
|
||||
│ ├── types.ts # Request/response types for this domain
|
||||
│ ├── keys.ts # React Query key factory
|
||||
│ ├── types.ts # Request/response types + the domain's Api interface (the seam)
|
||||
│ ├── keys.ts # React Query key factory (hierarchical)
|
||||
│ ├── constants.ts # Mock toggle + staleTime (when the domain has a mock)
|
||||
│ ├── apis/
|
||||
│ │ ├── clientApi.ts # Namespace object wrapping clientFetch calls
|
||||
│ │ └── serverApi.ts # Namespace object wrapping serverFetch calls (only when needed)
|
||||
│ │ ├── clientApi.ts # Real impl wrapping clientFetch (unwraps ApiEnvelope via unwrap())
|
||||
│ │ ├── mockApi.ts # In-memory impl behind the same interface (until the endpoint lands)
|
||||
│ │ ├── serverApi.ts # serverFetch calls (only when an RSC needs it)
|
||||
│ │ └── index.ts # Selects real vs mock by config — the seam hooks import
|
||||
│ └── hooks/
|
||||
│ └── use{Action}.ts # One hook per file — useQuery or useMutation
|
||||
│ └── use{Action}.ts # One hook per file — useQuery (deliberate staleTime) or useMutation (invalidates)
|
||||
├── context/ # React context providers
|
||||
│ └── auth/ # AuthContext — AuthProvider (server-seeded) + reducer + useAuth
|
||||
├── theme/
|
||||
│ ├── ThemeProvider.tsx # MuiThemeProvider wrapper + ColorSchemeScript + ColorSchemeCookieSync
|
||||
│ ├── ThemeProvider.tsx # MuiThemeProvider wrapper (RTL cache) + ColorSchemeCookieSync
|
||||
│ ├── colors.ts # BRAND, LIGHT_PALETTE, DARK_PALETTE
|
||||
│ ├── light.ts / dark.ts # LIGHT_THEME / DARK_THEME ThemeOptions (consumed by theme.ts)
|
||||
│ ├── direction.ts # getDirection(locale) → 'ltr' | 'rtl'
|
||||
│ ├── theme.ts # APP_THEME_LTR / APP_THEME_RTL (static, created once)
|
||||
│ ├── tokens.css # CSS custom properties — [data-mui-color-scheme] selectors
|
||||
│ ├── typography.ts # TYPOGRAPHY_LTR (Space Grotesk) / TYPOGRAPHY_RTL (Mikhak)
|
||||
│ └── index.ts # Public re-exports (incl. ColorSchemeScript, ThemeProvider, getDirection)
|
||||
├── constants/ # App-wide constants (routes, events, etc.)
|
||||
├── hooks/
|
||||
├── utils/
|
||||
│ └── index.ts # Public re-exports (ThemeProvider, getDirection, APP_THEME_*) — note: no ColorSchemeScript is exported/rendered today (doc drift below)
|
||||
├── constants/ # App-wide constants (routes.ts w/ actor paths, roles.ts, headers.ts)
|
||||
├── hooks/ # incl. auth.ts → useIsAuthenticated / useActorRole (role-aware chrome)
|
||||
├── utils/ # incl. money.ts (IRR/Toman, integer-safe) + date.ts (Shamsi display) + toEnglishDigits
|
||||
└── config.ts
|
||||
```
|
||||
|
||||
@@ -227,8 +257,14 @@ async function MyServerComponent() {
|
||||
```
|
||||
|
||||
**Established namespaces and where they're used:**
|
||||
- `'nav'` — `PrivateLayout.tsx` (sidebar nav items)
|
||||
- `'common'` — `DarkModeButton.tsx` (dark/light mode labels)
|
||||
- `'nav'` — the actor shells (`CustomerLayout`/`NurseLayout`/`AdminLayout`) build their nav from here
|
||||
- `'common'` — `DarkModeButton.tsx` (dark/light labels), shared words (loading, retry, currency_toman, …)
|
||||
- `'shell'` — actor-shell titles + the not-yet-built placeholder body
|
||||
- `'patients'` — the reference services/{domain} demo screen
|
||||
|
||||
**Namespace conventions for the phases to come** (seed each when its feature lands, in both locale
|
||||
files): `auth`, `onboarding`, `verification`, `search`, `booking`, `payment`, `bnpl`, `reviews`,
|
||||
`notifications`, `admin`. Keep top-level keys as namespaces and both files in sync.
|
||||
|
||||
**Never hard-code UI strings in English.** Any user-visible text must have a translation key in both locale files.
|
||||
|
||||
@@ -451,6 +487,26 @@ Central fetch primitives live in `src/lib/api/`:
|
||||
|
||||
**Domain API calls** live in `src/services/{domain}/apis/clientApi.ts` (or `serverApi.ts`). Never call raw `fetch()` directly.
|
||||
|
||||
### The `services/{domain}` reference pattern (copy `auth` / `patients`)
|
||||
|
||||
Every domain follows the same shape: `types.ts` (wire types + the domain's `Api` interface), `keys.ts`
|
||||
(hierarchical React Query key factory), `apis/` (implementations + a selecting `index.ts`), `hooks/`
|
||||
(one hook per file), and a barrel `index.ts` that re-exports **hooks only** (never `types`/`keys`/`apis`).
|
||||
|
||||
- **Caching is deliberate:** set a `staleTime` on reads so revisiting a screen doesn't refetch; mutations
|
||||
**invalidate** the affected list key (`queryClient.invalidateQueries`) or `setQueryData` — never leave the
|
||||
cache stale. See `services/patients/hooks/*`.
|
||||
- **Mock behind a seam:** when the backend endpoint isn't live, implement the domain's `Api` interface
|
||||
twice — a real `clientApi.ts` and an in-memory `mockApi.ts` — and select in `apis/index.ts` by a config
|
||||
flag (`USE_{DOMAIN}_MOCK`). Hooks import the selected `api`; the swap is one line. Record every mock in
|
||||
`dev/shared-working-context/reports/mocks-registry.md`.
|
||||
- **The wire envelope:** the server wraps responses in `ApiEnvelope<T>` (`{ isSuccess, statusCode,
|
||||
message, requestId, data }`, camelCase — see `lib/api/types.ts`). `clientFetch` returns the raw body, so
|
||||
a real `clientApi` reads the payload via `unwrap()`. Types are derived from `dev/contracts/` +
|
||||
`dev/contracts/openapi/swagger.v1.json`, mirroring the wire exactly.
|
||||
- **Money & dates:** format via `@/utils` — `formatIrrToToman`/`formatIrr`/`parseIrr` (IRR strings, integer-safe
|
||||
BigInt) and `formatShamsiDate`/`formatShamsiDateTime` (UTC ISO → Persian calendar). Money is never a float.
|
||||
|
||||
---
|
||||
|
||||
## Auth Cookies & session state
|
||||
|
||||
Reference in New Issue
Block a user