Files
baya-monorepo/archive/docs/rules/client/structure.md
T
2026-08-02 20:01:31 +03:30

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.