# Balinyaar Client

The web frontend of **Balinyaar**, a trust-first home-nursing marketplace in Iran. Families search for,
book, pay for and review home nursing; nurses list configurable services and run their day from the same
app. Four actors share one mobile shell: family, nurse, admin, partner centre.

> Last verified: 2026-07-30 against commit `d3ec723`.

- Repo-wide context and the backend → root [CLAUDE.md](../CLAUDE.md)
- Current product truth (what to test, what's blocking, what's missing) → [`mvp/`](../mvp/README.md)
- Business rules in depth (archived reference, not actively maintained) → [`archive/product/`](../archive/product/index.md).
  **Read the relevant doc before designing a feature** — don't infer a business rule from code.
- Visual/design work → the **frontend-designer** skill. It is the *design* contract and defers to this file
  and [`archive/docs/rules/client/`](../archive/docs/rules/client/) for engineering rules.

---

## Stack

- **Next.js 16** — App Router, Turbopack, React Server Components. **Not a static export.**
- **React 19** + **TypeScript** (`strict`)
- **MUI v9** (`@mui/material`) with **Emotion** underneath; RTL via `stylis-plugin-rtl`
- **Lucide** (`lucide-react`) for icons, behind the `AppIcon` name registry
- **next-intl v4** — locales `fa` (default, RTL) and `en`
- **TanStack Query v5** for server state; a small **AuthContext** (`src/context/auth/`) for session state
- **react-hook-form v7** for every form with more than one field
- **notistack** for toasts; **js-cookie** (wrapped) for client cookies
- **Jest** + **Testing Library**; **ESLint 9** (flat config) + **Prettier**

## Commands

| Task | Command |
| --- | --- |
| Dev server | `npm run dev` |
| Production build | `npm run build` |
| **Type + lint + copy (the gate)** | `npm run check` |
| Type-check only | `npm run type` |
| Lint only / autofix | `npm run lint` · `npm run lint:fix` |
| Persian copy lint | `npm run lint:copy` |
| Format | `npm run format` |
| Test (watch / CI) | `npm test` · `npm run test:ci` |

## Quality gates

```
npm run check      # tsc --noEmit  →  eslint .  →  scripts/check-copy.mjs
npm run test:ci    # also required when you touched a component with a co-located *.test.tsx
```

Both must be green, and `en.json`/`fa.json` must be in sync, before work is done. There is **no
`next lint`** — it was removed in Next 16 and calling it silently does nothing.

---

## Hard rules

1. **Never add a layout above `[locale]`.** `src/app/[locale]/layout.tsx` **is** the root layout — it renders
   `<html>`/`<body>`. A layout above it freezes `lang`/`dir`/messages on the default locale for every route.
2. **Respect the server/client boundary.** Never import `next/headers`, `next-intl/server`, or
   `@/lib/cookies/server` from a client component; never import `@/lib/cookies/client` from an RSC.
3. **No hard-coded UI strings.** Every user-visible string is a key in **both** `messages/en.json` and
   `messages/fa.json`. The one exception is `app/global-error.tsx`, which cannot use next-intl.
4. **Fetch only through `clientFetch`/`serverFetch`** (`@/lib/api`) — never raw `fetch()`. Domain calls live in
   `src/services/{domain}/apis/`.
5. **Cookies only through the cookie manager** (`@/lib/cookies/*`) — never `document.cookie`, `js-cookie`
   directly, `localStorage`, or `sessionStorage` for app or auth state.
6. **Colors come from `tokens.css` (`var(--bal-*)`) or MUI palette keys** — never a hex or rgb literal in `sx`
   or `styled`. Use the pre-built `APP_THEME_LTR`/`APP_THEME_RTL`; **never call `createTheme()` in a
   component.**
7. **MUI v9 API only.** `sx={{ mb: 4 }}`, not `mb={4}`. No v5/v6-only props (`useFlexGap`, `flexWrap` on
   `Stack`, `storageWindow`, `InitColorSchemeScript`).
8. **RTL-safe.** Never `marginLeft`, `left:`, or `textAlign: 'left'` for layout flow — use logical or
   MUI-flipped properties. `fa` is the default locale and it is RTL.
9. **Never add a pre-paint color-scheme script.** The no-flash boot is CSS-only; extend the `tokens.css`
   media-query fallback instead.
10. **`prefers-reduced-motion` has exactly one gate**, in `src/app/globals.css`. Never add a second,
    component-local branch.
11. **One layout: a phone.** `AppFrame` caps every screen at 480px at every viewport. Never add a `≥md` branch
    that widens a shell, restores a sidebar, or goes multi-column. Wide content scrolls inside its own
    container.
12. **Navigation is the bottom bar; there is no drawer.** Add a destination by adding a tab or a hub row, never
    by forking `MobileShell`. Active state comes from the shared `matchActivePath`, never a hand-rolled
    `pathname.startsWith`.
13. **All chrome navigation goes through `@/i18n/navigation`** — never a raw `next/link`, never a manual
    `` `/${locale}` `` prefix.
14. **Icons come from the `AppIcon` registry by lowercase name.** `@mui/icons-material` was removed — never
    reintroduce it, and never pass a raw icon component where a name is expected.
15. **Any form with more than one field uses react-hook-form**, bound through the
    `@/components/common/form` wrappers. Never call `register`/`useController` at a call site.
16. **Never append `ROUTES.HOME` (`'/'`) to `PUBLIC_PATHS`** — it is matched with `startsWith`, so that would
    silently make every route public.
17. **The middleware auth check is UX-only, not a security boundary** — it does not verify the JWT signature.
    Never gate real authorization on it, on `isTokenAlive`, or on `useAdminCapabilities`.
18. **The client displays money; it never computes it.** IRR digit strings parsed with integer-safe `BigInt`
    helpers, never a float. Never compute a rate, an aggregate, a payout date, or a holiday shift. A
    server-frozen deadline is rendered, never recomputed. A signed balance is never clamped.
19. **Never leak clinical data.** The customer never fires the care-instructions query; the nurse's care-record
     access is append-only; access-denied is gated *before* any clinical fetch; `is_internal` is never modelled
     in user-app types. Clinical text is never logged, stored in `localStorage`, or put in a query string.
20. **Every shared component has a co-located `*.test.tsx`** (shared = imported from more than one place).
    Don't mock MUI — test the rendered DOM.
21. **No dead code.** `@typescript-eslint/no-unused-vars` is an **error**, so it fails the gate. Delete it;
    prefix a deliberately-unused binding with `_`. Comment the *why*, never the *what*.
22. **Every mutation needs an `onError` toast** unless the failure is already surfaced inline. But never toast
    401/403/5xx in a hook — `clientFetch` already does.
23. **Magic strings become named constants** (`src/constants/`, or a co-located `constants.ts`).
24. **Don't reintroduce starter scaffolding** or `_TITLE_`/`_DESCRIPTION_` placeholders.
25. **When you change the structure, update "Project structure" below in the same change.**

---

## Project structure

The canonical map of the frontend's architecture. Expanded, with the reasoning, in
[`docs/rules/client/structure.md`](../archive/docs/rules/client/structure.md).

```
client/
├── messages/{en,fa}.json   translations — add every key to BOTH
├── middleware.ts           i18n routing → guest front door → auth gate
├── next.config.mjs         next-intl plugin + reactStrictMode, nothing else
└── src/
    ├── app/                the App Router tree
    │   ├── global-error.tsx  above [locale]: renders its own <html>, cannot use next-intl
    │   ├── robots.ts · sitemap.ts   the public surface, both locales
    │   ├── fonts/            Mikhak woff2 (next/font/local resolves relative to the caller)
    │   └── [locale]/
    │       ├── layout.tsx    THE ROOT LAYOUT — <html lang dir>, fonts, setRequestLocale,
    │       │                 providers, generateMetadata + metadataBase
    │       ├── error.tsx · not-found.tsx · [...rest]/page.tsx
    │       ├── (private-routes)/    layout mounts useSessionRoleSync
    │       │   ├── _chrome/         shared content skeleton (private, not a route)
    │       │   ├── select-role/     first-use role picker, in FocusedLayout
    │       │   ├── (customer)/      the family app — no URL segment
    │       │   ├── (customer-focused)/  chrome-free, same URL space (onboarding)
    │       │   ├── nurse/           the nurse app
    │       │   ├── admin/           the backoffice (+ _hub/ shared group-root body)
    │       │   └── partner/         the partner-centre portal — a SEPARATE authz scope
    │       └── (public-routes)/     login · terms · privacy · welcome
    ├── components/         common/ primitives + one folder per domain composite family
    ├── constants/          routes.ts · roles.ts · headers.ts · policy.ts
    ├── context/auth/       AuthContext (provider + reducer + useAuth)
    ├── hooks/              auth.ts · capabilities.ts · layout.ts · useAdminListState.ts
    ├── i18n/               routing.ts · request.ts · navigation.ts
    ├── layout/             AppFrame · MobileShell · the 4 actor layouts · chrome · config.ts
    ├── lib/                api/ · auth/ · cookies/ · query/ · toast/
    ├── services/           22 domain services — the data layer
    ├── theme/              colors.ts · tokens.css · typography.ts · theme.ts · ThemeProvider.tsx
    ├── utils/              money · date · number · text · toCsv · navigation
    └── config.ts           API_URL · SITE_URL · NESHAN_WEB_KEY · IS_DEBUG
```

Route groups (parenthesised) add no URL segment; `_`-prefixed folders are private, not routes. Each private
group's layout is `'use client'` and wraps `RoleGuard` → that actor's layout.

**Every screen is a thin RSC `page.tsx` (exporting `generateMetadata`) plus a co-located `'use client'`
`<PageName>Screen.tsx`.** `page.tsx` never renders `<title>` and never touches `document.title`.

---

## Where to read more

Open **one** of these for the area you are touching.

| Working on… | Read |
| --- | --- |
| Routes, layouts, the RSC boundary, page metadata | [docs/rules/client/structure.md](../archive/docs/rules/client/structure.md) |
| Colors, tokens, dark mode, RTL, fonts, motion | [docs/rules/client/theme.md](../archive/docs/rules/client/theme.md) |
| The `App*` library, shells, navigation, icons, constants | [docs/rules/client/components.md](../archive/docs/rules/client/components.md) |
| Any form | [docs/rules/client/forms.md](../archive/docs/rules/client/forms.md) |
| Copy, translations, Persian orthography | [docs/rules/client/i18n.md](../archive/docs/rules/client/i18n.md) |
| Fetching, TanStack Query, `services/{domain}`, money display, cookies | [docs/rules/client/services.md](../archive/docs/rules/client/services.md) |
| Sessions, refresh, `RoleGuard`, middleware, security posture | [docs/rules/client/auth.md](../archive/docs/rules/client/auth.md) |
| Tests, ESLint, the type gate | [docs/rules/client/testing.md](../archive/docs/rules/client/testing.md) |
| The wire contract — envelope, status codes, enums, pagination | [docs/integration/](../archive/docs/integration/index.md) |
| What is built, what is mocked, what is next | [docs/status/](../archive/docs/status/index.md) |
| Brand, look and feel, turning a design into a screen | the **frontend-designer** skill |
| Cross-project rules — naming, gates, code quality | [docs/rules/shared/](../archive/docs/rules/shared/) |
