11 KiB
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
- Current product truth (what to test, what's blocking, what's missing) →
mvp/ - Business rules in depth (archived reference, not actively maintained) →
archive/product/. 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/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 viastylis-plugin-rtl - Lucide (
lucide-react) for icons, behind theAppIconname registry - next-intl v4 — locales
fa(default, RTL) anden - 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
- Never add a layout above
[locale].src/app/[locale]/layout.tsxis the root layout — it renders<html>/<body>. A layout above it freezeslang/dir/messages on the default locale for every route. - Respect the server/client boundary. Never import
next/headers,next-intl/server, or@/lib/cookies/serverfrom a client component; never import@/lib/cookies/clientfrom an RSC. - No hard-coded UI strings. Every user-visible string is a key in both
messages/en.jsonandmessages/fa.json. The one exception isapp/global-error.tsx, which cannot use next-intl. - Fetch only through
clientFetch/serverFetch(@/lib/api) — never rawfetch(). Domain calls live insrc/services/{domain}/apis/. - Cookies only through the cookie manager (
@/lib/cookies/*) — neverdocument.cookie,js-cookiedirectly,localStorage, orsessionStoragefor app or auth state. - Colors come from
tokens.css(var(--bal-*)) or MUI palette keys — never a hex or rgb literal insxorstyled. Use the pre-builtAPP_THEME_LTR/APP_THEME_RTL; never callcreateTheme()in a component. - MUI v9 API only.
sx={{ mb: 4 }}, notmb={4}. No v5/v6-only props (useFlexGap,flexWraponStack,storageWindow,InitColorSchemeScript). - RTL-safe. Never
marginLeft,left:, ortextAlign: 'left'for layout flow — use logical or MUI-flipped properties.fais the default locale and it is RTL. - Never add a pre-paint color-scheme script. The no-flash boot is CSS-only; extend the
tokens.cssmedia-query fallback instead. prefers-reduced-motionhas exactly one gate, insrc/app/globals.css. Never add a second, component-local branch.- One layout: a phone.
AppFramecaps every screen at 480px at every viewport. Never add a≥mdbranch that widens a shell, restores a sidebar, or goes multi-column. Wide content scrolls inside its own container. - 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 sharedmatchActivePath, never a hand-rolledpathname.startsWith. - All chrome navigation goes through
@/i18n/navigation— never a rawnext/link, never a manual`/${locale}`prefix. - Icons come from the
AppIconregistry by lowercase name.@mui/icons-materialwas removed — never reintroduce it, and never pass a raw icon component where a name is expected. - Any form with more than one field uses react-hook-form, bound through the
@/components/common/formwrappers. Never callregister/useControllerat a call site. - Never append
ROUTES.HOME('/') toPUBLIC_PATHS— it is matched withstartsWith, so that would silently make every route public. - 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 onuseAdminCapabilities. - The client displays money; it never computes it. IRR digit strings parsed with integer-safe
BigInthelpers, 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. - 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_internalis never modelled in user-app types. Clinical text is never logged, stored inlocalStorage, or put in a query string. - Every shared component has a co-located
*.test.tsx(shared = imported from more than one place). Don't mock MUI — test the rendered DOM. - No dead code.
@typescript-eslint/no-unused-varsis an error, so it fails the gate. Delete it; prefix a deliberately-unused binding with_. Comment the why, never the what. - Every mutation needs an
onErrortoast unless the failure is already surfaced inline. But never toast 401/403/5xx in a hook —clientFetchalready does. - Magic strings become named constants (
src/constants/, or a co-locatedconstants.ts). - Don't reintroduce starter scaffolding or
_TITLE_/_DESCRIPTION_placeholders. - 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.
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 |
| Colors, tokens, dark mode, RTL, fonts, motion | docs/rules/client/theme.md |
The App* library, shells, navigation, icons, constants |
docs/rules/client/components.md |
| Any form | docs/rules/client/forms.md |
| Copy, translations, Persian orthography | docs/rules/client/i18n.md |
Fetching, TanStack Query, services/{domain}, money display, cookies |
docs/rules/client/services.md |
Sessions, refresh, RoleGuard, middleware, security posture |
docs/rules/client/auth.md |
| Tests, ESLint, the type gate | docs/rules/client/testing.md |
| The wire contract — envelope, status codes, enums, pagination | docs/integration/ |
| What is built, what is mocked, what is next | docs/status/ |
| Brand, look and feel, turning a design into a screen | the frontend-designer skill |
| Cross-project rules — naming, gates, code quality | docs/rules/shared/ |