# 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) - Business rules (what to build) → [`product/`](../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 [`docs/rules/client/`](../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 ``/`
`. 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`](../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 , 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 — , 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'` `