178 lines
11 KiB
Markdown
178 lines
11 KiB
Markdown
# 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
|
|
`<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`](../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](../docs/rules/client/structure.md) |
|
|
| Colors, tokens, dark mode, RTL, fonts, motion | [docs/rules/client/theme.md](../docs/rules/client/theme.md) |
|
|
| The `App*` library, shells, navigation, icons, constants | [docs/rules/client/components.md](../docs/rules/client/components.md) |
|
|
| Any form | [docs/rules/client/forms.md](../docs/rules/client/forms.md) |
|
|
| Copy, translations, Persian orthography | [docs/rules/client/i18n.md](../docs/rules/client/i18n.md) |
|
|
| Fetching, TanStack Query, `services/{domain}`, money display, cookies | [docs/rules/client/services.md](../docs/rules/client/services.md) |
|
|
| Sessions, refresh, `RoleGuard`, middleware, security posture | [docs/rules/client/auth.md](../docs/rules/client/auth.md) |
|
|
| Tests, ESLint, the type gate | [docs/rules/client/testing.md](../docs/rules/client/testing.md) |
|
|
| The wire contract — envelope, status codes, enums, pagination | [docs/integration/](../docs/integration/index.md) |
|
|
| What is built, what is mocked, what is next | [docs/status/](../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/](../docs/rules/shared/) |
|