186 lines
9.2 KiB
Markdown
186 lines
9.2 KiB
Markdown
# Client structure
|
|
|
|
The route tree, the server/client boundary, and the page pattern every screen follows.
|
|
|
|
> Last verified: 2026-07-30 against commit `d3ec723`.
|
|
|
|
---
|
|
|
|
## 1. `src/` at a glance
|
|
|
|
| Folder | Holds |
|
|
| --- | --- |
|
|
| `app/` | The App Router tree. Everything under `[locale]/` |
|
|
| `components/` | Shared UI — `common/` primitives plus one folder per domain composite family |
|
|
| `constants/` | App-wide named constants (`routes.ts`, `roles.ts`, `headers.ts`, `policy.ts`) |
|
|
| `context/` | React context providers — `auth/` (AuthContext + reducer) |
|
|
| `hooks/` | Cross-cutting hooks (`auth.ts`, `capabilities.ts`, `layout.ts`, `useAdminListState.ts`) |
|
|
| `i18n/` | next-intl wiring: `routing.ts`, `request.ts`, `navigation.ts` |
|
|
| `layout/` | The one mobile app shell: `AppFrame`, `MobileShell`, the four actor layouts, chrome components |
|
|
| `lib/` | Infrastructure: `api/` (fetch), `auth/` (token/session), `cookies/`, `query/`, `toast/` |
|
|
| `services/` | 22 domain services, one folder each — the data layer |
|
|
| `theme/` | Palette, tokens, typography, the pre-built themes |
|
|
| `utils/` | Money, dates, numbers, CSV, text helpers |
|
|
|
|
Plus `messages/{en,fa}.json`, `middleware.ts`, and `next.config.mjs` (which only wires the next-intl
|
|
plugin and `reactStrictMode`) outside `src/`.
|
|
|
|
This is a **pattern map, not a file listing**. `git ls-files client/src` enumerates files for free and
|
|
never goes stale; what follows is the shape those files have to fit.
|
|
|
|
---
|
|
|
|
## 2. The one absolute rule: no layout above `[locale]`
|
|
|
|
**`src/app/[locale]/layout.tsx` IS the root layout.** It renders `<html>` and `<body>`. There is no
|
|
`src/app/layout.tsx`, and adding one — or any layout above the `[locale]` segment — breaks both locales.
|
|
|
|
**Why**, because this is worth understanding rather than obeying: a layout above `[locale]` is *shared*
|
|
between `/fa` and `/en`. Next.js statically caches it at build time with `defaultLocale` (`fa`) and never
|
|
re-renders it on a client-side locale switch, because the segment it is keyed on doesn't change. Its
|
|
`lang`, `dir`, messages, providers, and fonts therefore **freeze on `fa`/`rtl` for every route, including
|
|
`/en`**. The `[locale]` layout is the lowest boundary keyed on the locale param, so it is the only place
|
|
`<html lang dir>` can reliably track the active locale.
|
|
|
|
### What the root layout owns
|
|
|
|
- The locale, sourced from the **URL param** (`params.locale`), validated against `routing.locales` with a
|
|
fallback to `defaultLocale`. **No header reads.**
|
|
- `<html lang dir>` — `dir` from `getDirection(locale)` — plus `data-mui-color-scheme` from
|
|
`getThemeMode()`.
|
|
- The per-locale font class (Mikhak on `fa` only — see [theme.md](theme.md)).
|
|
- `setRequestLocale(locale)`, so server components deeper in the tree can call `getLocale()` /
|
|
`getTranslations()` reliably. **Never remove it** — without it, deeper RSCs always see `defaultLocale`.
|
|
- `getMessages({ locale })` with the locale passed **explicitly**, so `getRequestConfig` receives it via
|
|
`Promise.resolve(locale)` rather than through the `React.cache` read — which avoids a cache-ordering
|
|
race. **Never call `getMessages()` bare.**
|
|
- The providers: `NextIntlClientProvider`, `AuthProvider` (seeded with `getServerAuthState()`),
|
|
`ThemeProvider`, `NotistackProvider` + `ToastBridge`.
|
|
- `generateStaticParams`, so Next can enumerate locale routes at build time.
|
|
- `generateMetadata` — the `'%s | بالینیار'` / `'%s | Balinyaar'` title template, the default title and
|
|
description, and `metadataBase: new URL(SITE_URL)` so child pages' relative OG/canonical URLs resolve
|
|
absolute. `SITE_URL` comes from `src/config.ts`, never a hard-coded origin.
|
|
|
|
**Never add `notFound()` to the `[locale]` layout.** Unknown locales are handled by middleware; a hard 404
|
|
there breaks the fallback.
|
|
|
|
### The two files that legitimately sit above `[locale]`
|
|
|
|
| File | Why it's allowed |
|
|
| --- | --- |
|
|
| `app/global-error.tsx` | It *replaces* the root layout on a root-level crash, so it renders its own `<html>` — which means it **cannot use next-intl**. It is the one sanctioned static-string exception: keep it minimal and bilingual (fa + en) |
|
|
| `app/robots.ts`, `app/sitemap.ts` | Route handlers, not layouts. They enumerate the public surface across both locales |
|
|
|
|
---
|
|
|
|
## 3. The server/client boundary
|
|
|
|
| Never import | From |
|
|
| --- | --- |
|
|
| `next/headers` | a client component |
|
|
| `next-intl/server` | a client component |
|
|
| `@/lib/cookies/server` | a client component |
|
|
| `@/lib/cookies/client` | an RSC |
|
|
|
|
The build fails on the first three. The fourth fails at runtime, quietly, which is worse.
|
|
|
|
Route-group layouts (`(private-routes)/layout.tsx`, `(public-routes)/layout.tsx`) are `'use client'` — they
|
|
only wrap a layout component and need no server capabilities.
|
|
|
|
Never mix `clientFetch` and `serverFetch` in the same file; keep `clientApi.ts` and `serverApi.ts`
|
|
separate. Next enforces the environment boundary at build time.
|
|
|
|
---
|
|
|
|
## 4. The route tree, by shape
|
|
|
|
Everything lives under `src/app/[locale]/`. Route groups add no URL segment.
|
|
|
|
```
|
|
[locale]/
|
|
├── layout.tsx error.tsx not-found.tsx [...rest]/page.tsx
|
|
├── (private-routes)/ layout.tsx mounts useSessionRoleSync
|
|
│ ├── _chrome/ shared loading skeleton (private, not a route)
|
|
│ ├── select-role/ first-use role picker, own FocusedLayout
|
|
│ ├── (customer)/ the family app — no URL segment
|
|
│ ├── (customer-focused)/ chrome-free counterpart, same URL space (onboarding)
|
|
│ ├── nurse/ the nurse app
|
|
│ ├── admin/ the backoffice
|
|
│ └── partner/ the partner-centre portal — a SEPARATE authz scope
|
|
└── (public-routes)/ login · terms · privacy · welcome
|
|
```
|
|
|
|
| Convention | Meaning |
|
|
| --- | --- |
|
|
| `(parenthesised)` | A route group. Adds no URL segment; exists to attach a layout and a `RoleGuard` |
|
|
| `_`-prefixed folder | Private, **not a route** — `_chrome/`, `admin/_hub/` |
|
|
| `[...rest]/page.tsx` | The catch-all. Calls `notFound()` so any unmatched path under a locale renders `not-found.tsx` — next-intl's recommended 404 pattern |
|
|
| `loading.tsx` | A route-group skeleton shaped like that group's content area. The `MobileShell` chrome is already rendered by the enclosing layout, so a skeleton shapes the content only |
|
|
|
|
Each private group's `layout.tsx` is `'use client'` and wraps `RoleGuard` → that actor's layout:
|
|
`(customer)` → `CustomerLayout`, `nurse` → `NurseLayout`, `admin` → `AdminLayout`, `partner` →
|
|
`PartnerLayout`. `(customer-focused)` and `select-role` wrap `FocusedLayout` instead, for flows a user must
|
|
not be able to tab away from mid-setup.
|
|
|
|
The **partner portal is a separate authorization scope**: a centre admin is not a Balinyaar admin. Its
|
|
`RoleGuard` passes no `expected` role (it isn't an `AppRole`) and each page resolves the caller's *own*
|
|
centre via `useMyPartnerCenter`. See [auth.md](auth.md).
|
|
|
|
**When you add, remove, or rename a route group, a provider, or a top-level `src/` folder, update the
|
|
"Project structure" section in [client/CLAUDE.md](../../../client/CLAUDE.md) and §1 above in the same
|
|
change.**
|
|
|
|
---
|
|
|
|
## 5. The page pattern
|
|
|
|
The root layout owns a title *template*; a route supplies the `%s`. So a route that wants its own tab
|
|
title splits in two:
|
|
|
|
```tsx
|
|
// page.tsx — a thin RSC. No 'use client'.
|
|
import type { Metadata } from 'next';
|
|
import { getTranslations } from 'next-intl/server';
|
|
import HomeScreen from './HomeScreen';
|
|
|
|
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
|
const { locale } = await params;
|
|
const t = await getTranslations({ locale, namespace: 'shell' });
|
|
return { title: t('customer_app') };
|
|
}
|
|
|
|
export default function Page() {
|
|
return <HomeScreen />;
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
// HomeScreen.tsx — 'use client'. All the logic and JSX.
|
|
```
|
|
|
|
Rules that fall out of it:
|
|
|
|
- The screen component is **co-located** with `page.tsx` and named `<PageName>Screen.tsx`, so its existing
|
|
relative imports keep working unchanged.
|
|
- `page.tsx` never renders `<title>` and never touches `document.title`. Assigning `document.title` in a
|
|
render body throws `ReferenceError: document is not defined` during build-time prerendering.
|
|
- A static `metadata` export is fine when the title needs no translation lookup.
|
|
- Page bodies stay **composition + content**. Reusable visuals move to `src/components/`; page-only,
|
|
never-reused composition can stay in the page.
|
|
|
|
Adoption is partial by design: the landing pages (customer home, `/login`, `/search`, `/bookings`,
|
|
`/nurse`, `/admin`, `/partner`) plus `/welcome` use it. The rest still render directly and gain it when
|
|
the page is next substantially touched.
|
|
|
|
`metadataBase` makes the pattern extend to OG: `/welcome` sets `alternates.canonical` + `openGraph` in its
|
|
`generateMetadata` and supplies `og:image` from a co-located `opengraph-image.tsx` (`next/og`'s
|
|
`ImageResponse`).
|
|
|
|
---
|
|
|
|
## 6. This is not a static export
|
|
|
|
The app relies on server components, middleware, and server-side cookies. `next.config.mjs` wires the
|
|
next-intl plugin and `reactStrictMode` and nothing else — don't add `output: 'export'`, and don't assume a
|
|
page can be prerendered without its request context.
|