another step just a little remaining

This commit is contained in:
hamid
2026-06-21 00:05:07 +03:30
parent da42f15a32
commit 3fd147cf80
35 changed files with 4620 additions and 4537 deletions
+125 -31
View File
@@ -1,5 +1,92 @@
# Balinyaar Client — Claude Code Guidelines
The web frontend of **Balinyaar**, a trust-first home-nursing marketplace in Iran. This file is the
**engineering contract** for everything under `client/`: providers, routing, data fetching, theming,
i18n, cookies, and the rules every change must follow.
- Repo-wide context and the backend → root [CLAUDE.md](../CLAUDE.md).
- Product/domain rules (what to build) → [`product/`](../product/) — read the relevant doc before
designing a feature; don't infer business rules from code.
- Visual/design work (brand palette, tokens, component look-and-feel) → the **frontend-designer**
skill. It is the *design* contract and defers to this file for *engineering* rules. Don't restate
this file there.
## Stack
- **Next.js 16** — App Router, Turbopack, React Server Components. **Not a static export** — the app
relies on server components, middleware, and server-side cookies. (`next.config.mjs` only wires the
next-intl plugin + `reactStrictMode`.)
- **React 19** + **TypeScript** (`strict`).
- **MUI v9** (`@mui/material`) for components and theming; **Emotion** underneath (RTL via
`stylis-plugin-rtl`).
- **next-intl v4** for i18n — locales `fa` (default, RTL) and `en`.
- **TanStack Query v5** for server state; a small **AppStore** (React context + reducer, `src/store/`)
for client state.
- **notistack** for toasts; **js-cookie** (wrapped) for client cookies.
- **Jest** + **Testing Library** for unit tests.
- Quality gates: **tsc**, **ESLint 9** (flat config), **Prettier**.
## Commands
| Task | Command |
| --- | --- |
| Dev server | `npm run dev` |
| Production build | `npm run build` |
| Type-check | `npm run type` |
| Lint | `npm run lint` |
| Lint + autofix | `npm run lint:fix` |
| **Type + lint (the gate)** | `npm run check` |
| Format (Prettier) | `npm run format` |
| Test (watch) | `npm test` |
| Test (CI, once) | `npm run test:ci` |
**Always run `npm run check` before declaring work done.** Run `npm run test:ci` as well when you
touch a component that has a co-located `*.test.tsx`.
## Quality gates: lint & type (how they work)
Both gates are plain CLI tools. **There is no `next lint`** — it was removed in Next 16; calling it
silently does nothing.
- `npm run type``tsc --noEmit`. Config in `tsconfig.json`: `strict` on, `noEmit`, `@/*``src/*`.
- `npm run lint``eslint .` driven by **flat config** in `eslint.config.mjs`. That config spreads
`eslint-config-next` (core-web-vitals + typescript + react + react-hooks + jsx-a11y + import) and
applies `eslint-config-prettier` last so ESLint never fights Prettier on formatting.
- `npm run check` runs type then lint. Keep it green.
Rules for this project:
- **This project is flat-config only.** Do not add `.eslintrc*` files — put any rule changes in
`eslint.config.mjs`.
- **ESLint owns correctness, Prettier owns formatting.** Don't add stylistic ESLint rules.
- **Prefer fixing code over silencing the linter.** When a disable is genuinely correct — e.g. a
deliberate browser-only read after mount that trips `react-hooks/set-state-in-effect` — use a
scoped `// eslint-disable-next-line <rule>` with a one-line reason, never a file-wide disable.
- **Pin to ESLint 9.** ESLint 10 currently crashes with this Next 16 toolchain
(`scopeManager.addGlobals is not a function`). `import/no-cycle` is also disabled — its TS resolver
has an interface mismatch here (see the note in `eslint.config.mjs`).
## Golden rules (the short list)
A change is "done" only if it respects all of these — each has a full section below.
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.
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`.
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`, `localStorage`, or `sessionStorage` for app/auth state.
6. **Colors come from `tokens.css`** (`var(--…)`), never hard-coded in `sx`. Use the pre-built
`APP_THEME_LTR`/`APP_THEME_RTL`; never call `createTheme()` in a component.
7. **MUI v9 API only.** Use `sx={{ mb: 4 }}`, not `mb={4}` as a direct prop. No MUI-v5/v6-only props
(`useFlexGap`, `flexWrap` on `Stack`, `storageWindow`, `InitColorSchemeScript`, …).
8. **Shared components get a co-located `*.test.tsx`.** (A component imported from >1 place.)
9. **Magic strings become named constants** (`src/constants/` or a co-located `constants.ts`).
10. **`npm run check` is green** and translations stay in sync before you finish.
## Project Structure
```
@@ -11,11 +98,10 @@ client/
├── next.config.mjs # createNextIntlPlugin wires i18n into Next.js
└── src/
├── app/
│ ├── layout.tsx # Root RSC: reads locale + cookie → sets HTML attrs
│ ├── globals.css
│ ├── fonts/ # Local font files (woff2) — Mikhak for fa
│ └── [locale]/
│ ├── layout.tsx # RSC: setRequestLocale + NextIntlClientProvider + ThemeProvider + AppStoreProvider
│ ├── layout.tsx # ROOT RSC: renders <html lang/dir> + fonts + setRequestLocale + NextIntlClientProvider + ThemeProvider + AppStoreProvider
│ ├── (private-routes)/
│ │ ├── layout.tsx # 'use client' — wraps PrivateLayout
│ │ └── page.tsx
@@ -62,14 +148,14 @@ client/
│ └── use{Action}.ts # One hook per file — useQuery or useMutation
├── store/ # AppStore (Redux-like client state)
├── theme/
│ ├── ColorSchemeScript.tsx # Inline <script> in <head> — sets attr + patches Storage
│ ├── ThemeProvider.tsx # MuiThemeProvider wrapper + ColorSchemeCookieSync
│ ├── colors.ts # LIGHT_PALETTE, DARK_PALETTE, BRAND
│ ├── ThemeProvider.tsx # MuiThemeProvider wrapper + ColorSchemeScript + ColorSchemeCookieSync
│ ├── colors.ts # BRAND, LIGHT_PALETTE, DARK_PALETTE
│ ├── light.ts / dark.ts # LIGHT_THEME / DARK_THEME ThemeOptions (consumed by theme.ts)
│ ├── direction.ts # getDirection(locale) → 'ltr' | 'rtl'
│ ├── theme.ts # APP_THEME_LTR / APP_THEME_RTL (static, created once)
│ ├── tokens.css # CSS custom properties — [data-mui-color-scheme] selectors
│ ├── typography.ts # TYPOGRAPHY_LTR (Space Grotesk) / TYPOGRAPHY_RTL (Mikhak)
│ └── index.ts # Public re-exports
│ └── index.ts # Public re-exports (incl. ColorSchemeScript, ThemeProvider, getDirection)
├── constants/ # App-wide constants (routes, events, etc.)
├── hooks/
├── utils/
@@ -80,21 +166,19 @@ client/
## Server / Client Component Boundaries
**Root layout** (`src/app/layout.tsx`) is a **lean Server Component** (RSC). It:
- Reads locale from the **`x-app-locale` request header** (`HEADER_NAMES.LOCALE` from `@/constants`) set by the middleware (with fallback to `defaultLocale` for build-time pre-rendering where no request context exists).
- Calls `getThemeMode()` from `@/lib/cookies/server` (only `colorScheme` is used).
- Renders `<html>` with `data-mui-color-scheme`, `lang`, and `dir` from the locale/cookie.
- Does **NOT** wrap the tree with `NextIntlClientProvider`, `ThemeProvider`, or `AppStoreProvider` — those live in `[locale]/layout.tsx` where the locale is reliably sourced from URL params.
**There is NO `src/app/layout.tsx`.** `src/app/[locale]/layout.tsx` is the application's **root layout** — it renders `<html>` and `<body>`. This is intentional and load-bearing (see below); do not re-introduce a layout above the `[locale]` segment.
**WHY providers are NOT in root layout**: `getThemeMode()` and `headers()` are both called inside `try/catch` blocks that swallow `DYNAMIC_SERVER_USAGE`. Next.js therefore never observes a dynamic API error propagating from the root layout and treats it as a statically renderable component. At build time the locale falls back to `defaultLocale` ('fa'), the static HTML is cached, and all subsequent requests — including `/en` — are served the pre-rendered 'fa' messages. Moving providers to `[locale]/layout.tsx` sidesteps this entirely: that layout always has the correct locale from URL params.
**Locale layout** (`src/app/[locale]/layout.tsx`) is an RSC that owns all i18n and theme context. It:
**Root / locale layout** (`src/app/[locale]/layout.tsx`) is an RSC that owns the document shell, all i18n, and theme context. It:
- Sources the locale from the **URL param** (`params.locale`), validated against `routing.locales` (falls back to `defaultLocale`). No header reads.
- Renders `<html lang dir>` (`dir` from `getDirection(locale)`) plus `data-mui-color-scheme` from `getThemeMode()`.
- Loads the Mikhak font and attaches its CSS-variable class to `<html>` **only for `fa`** (see Fonts).
- Calls `setRequestLocale(locale)` so server components deeper in the tree can call `getLocale()` / `getTranslations()` reliably.
- Calls `getMessages({ locale })` with the locale passed **explicitly** so `getRequestConfig` receives it via `Promise.resolve(locale)` (not through the React.cache read), avoiding any cache-ordering race.
- Calls `getThemeMode()` for `defaultMode` needed by `ThemeProvider`.
- Wraps children with `NextIntlClientProvider`, `AppStoreProvider`, and `ThemeProvider`.
- Exports `generateStaticParams` so Next.js can enumerate locale routes at build time.
**WHY `<html>` MUST live in `[locale]/layout.tsx` and not a layout above it**: a layout above the `[locale]` segment 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 (the segment doesn't change). Its `lang`/`dir`/messages 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 where `<html lang dir>` reliably tracks the active locale.
**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** import from `next/headers`, `next-intl/server`, or `@/lib/cookies/server` in a client component. The build will fail.
@@ -213,6 +297,8 @@ We override all of these via `colorSchemeSelector: 'data-mui-color-scheme'` in t
All theme-aware colors live in `src/theme/tokens.css` under `[data-mui-color-scheme]` selectors. Do not add color values to inline `sx` props or component styles — add a CSS variable to `tokens.css` and reference it via `var(--my-token)`.
This includes feedback colors: `--bal-success`, `--bal-error`, `--bal-warning`, `--bal-info` (each with a `*-contrast` text token). These drive the toast variants (see Toast Notifications) and are the place to source any success/error/warning/info color — the MUI palette does **not** define semantic colors, so prefer these tokens over MUI's defaults for brand consistency.
### Pre-built theme objects
`APP_THEME_LTR` and `APP_THEME_RTL` are created once at module load. Never call `createTheme()` inside a component or hook — pass the appropriate pre-built theme to `MuiThemeProvider`.
@@ -237,7 +323,7 @@ Derived from locale via `getDirection(locale)` in `src/theme/direction.ts`:
`ThemeProvider` accepts a `dir` prop and selects the matching pre-built theme (`APP_THEME_RTL` for RTL). The RTL Emotion cache uses `stylis-plugin-rtl` to mirror all generated CSS.
The root layout sets `dir={dir}` on `<html>` and passes `dir` to `ThemeProvider`. Changing locale → new request → new `dir` without any client-side state.
`src/app/[locale]/layout.tsx` sets `dir={dir}` on `<html>` and passes `dir` to `ThemeProvider`. Because that layout is keyed on the `[locale]` URL param, changing locale re-renders it with a fresh `dir` — on both hard and soft navigation, no client-side state. **Do not** move the `<html dir>` render to a layout above `[locale]`; such a layout is shared across locales, gets statically cached with the default locale, and `dir` freezes on 'rtl' for `/en`.
**Default locale is `fa` (RTL).** The middleware redirects bare `/` to `/fa/`. English is explicitly accessed at `/en/`.
@@ -245,12 +331,12 @@ The root layout sets `dir={dir}` on `<html>` and passes `dir` to `ThemeProvider`
## Fonts
Two fonts are loaded on every request (both CSS variables are always defined on `<html>`):
Fonts are loaded **per locale** — the Persian face is never shipped to English pages:
| Locale | Font | CSS variable | Source |
|--------|------|--------------|--------|
| `fa` (RTL) | **Mikhak** | `--font-mikhak` | `next/font/local` — woff2 files in `src/app/fonts/` |
| `en` (LTR) | **Space Grotesk** | `--font-space-grotesk` | `next/font/google` |
| Locale | Font | CSS variable | Source | Loaded when |
|--------|------|--------------|--------|-------------|
| `fa` (RTL) | **Mikhak** | `--font-mikhak` | `next/font/local` — woff2 files in `src/app/fonts/` | only on `fa` routes |
| `en` (LTR) | **Space Grotesk** | `--font-space-grotesk` | (not currently wired — falls back to the system stack) | — |
**Typography exports:**
- `TYPOGRAPHY_LTR` — Space Grotesk headings, system font body (used by `APP_THEME_LTR`)
@@ -258,10 +344,10 @@ Two fonts are loaded on every request (both CSS variables are always defined on
- `TYPOGRAPHY` — alias for `TYPOGRAPHY_LTR` (deprecated, prefer the explicit exports)
**Rules:**
- Both font variables are loaded unconditionally so the CSS stacks can always reference them.
- Font files live in `src/app/fonts/` (not `public/`). next/font/local resolves paths relative to the calling file at build time.
- Never load fonts inside components — all font loading lives in `src/app/layout.tsx`.
- To add a new font, add woff2 files to `src/app/fonts/`, declare via `localFont` in layout.tsx, and update `BRAND_FONT_VARIABLE_*` constants in `typography.ts`.
- Mikhak is declared with `preload: false`, and its `.variable` class is attached to `<html>` **only when `locale === 'fa'`**. Both are required: a `next/font` loader called in the root layout would otherwise preload on every route (including `/en`), and `preload: false` ensures the woff2 only downloads when Persian text actually renders.
- Font files live in `src/app/fonts/` (not `public/`). next/font/local resolves paths relative to the calling file (`src/app/[locale]/layout.tsx`) at build time.
- Never load fonts inside components — all font loading lives in `src/app/[locale]/layout.tsx`.
- To add a new font, add woff2 files to `src/app/fonts/`, declare via `localFont`/`localFont`-equivalent in `src/app/[locale]/layout.tsx`, attach its `.variable` class conditionally on the matching locale, and update `BRAND_FONT_VARIABLE_*` constants in `typography.ts`.
---
@@ -293,12 +379,12 @@ Enforcement: before removing or renaming a shared component, check whether `src/
- **Do not** set `colorSchemeSelector: 'data'` — use `'data-mui-color-scheme'`.
- **Do not** check `mode === 'dark'` for "is dark active" — use `colorScheme === 'dark'`.
- **Do not** hard-code UI strings — add translation keys to both `messages/en.json` and `messages/fa.json`.
- **Do not** put `NextIntlClientProvider`, `ThemeProvider`, or `AppStoreProvider` in the root layout — both `headers()` and `getThemeMode()` swallow `DYNAMIC_SERVER_USAGE`, so Next.js statically caches the root layout at build time with `defaultLocale` ('fa'). All `/en` requests then receive the cached 'fa' messages. These providers belong in `[locale]/layout.tsx` where locale is sourced from URL params.
- **Do not** add a `src/app/layout.tsx` or any layout above the `[locale]` segment. Such a layout is shared across locales, gets statically cached at build time with `defaultLocale` ('fa'), and never re-renders on a locale switch — so `<html lang/dir>`, messages, providers, and fonts placed there freeze on 'fa'/'rtl' for `/en`. `src/app/[locale]/layout.tsx` is the root layout (it renders `<html>`/`<body>`) precisely because it is the lowest boundary keyed on the locale param.
- **Do not** call `getMessages()` without passing `{ locale }` explicitly — `getMessages({ locale })` passes the locale directly to `getRequestConfig` via `Promise.resolve(locale)`, bypassing potential React.cache ordering issues.
- **Do not** remove `setRequestLocale(locale)` from `src/app/[locale]/layout.tsx` — without it, `getLocale()` called by deeper server components always returns `defaultLocale`.
- **Do not** add `notFound()` to `src/app/[locale]/layout.tsx` — unknown locale URLs are handled by middleware (redirect to defaultLocale); a hard 404 here breaks fallback behavior.
- **Do not** import `TYPOGRAPHY` — use `TYPOGRAPHY_LTR` or `TYPOGRAPHY_RTL` explicitly.
- **Do not** load fonts inside components or pages — all next/font declarations belong in `src/app/layout.tsx`.
- **Do not** load fonts inside components or pages — all next/font declarations belong in `src/app/[locale]/layout.tsx`, with the `.variable` class attached conditionally per locale (Mikhak only for `fa`).
- **Do not** import `@/lib/cookies/server` in client components or `@/lib/cookies/client` in RSCs.
- **Do not** call `fetch()` directly in components or services — use `serverFetch` (RSC/Server Actions) or `clientFetch` (hooks/Client Components) from `@/lib/api`.
- **Do not** create a top-level barrel at `src/services/index.ts` — imports should make the domain origin clear (e.g. `import { useLogin } from '@/services/auth'`, not `import { useLogin } from '@/services'`).
@@ -308,7 +394,8 @@ Enforcement: before removing or renaming a shared component, check whether `src/
- **Do not** call `js-cookie` (`Cookies.*`) directly — use the central client cookie manager (`@/lib/cookies/client`).
- **Do not** read or write `document.cookie` directly — use the central client cookie manager.
- **Do not** store auth tokens in `sessionStorage` or `localStorage` — use cookies via `@/lib/cookies/client`.
- **Do not** pass `flexWrap` or `useFlexGap` as direct props to MUI `Stack` — these are not valid Stack props in MUI v9 and cause a TypeScript overload error. Use `sx={{ flexWrap: 'wrap' }}` instead. `useFlexGap` was a MUI v5 opt-in and does not exist in v9.
- **Do not** use mui old api which cause errors
---
## API Fetch Services
@@ -340,14 +427,14 @@ Central fetch primitives live in `src/lib/api/`:
| Cookie | Constant | TTL | Set by |
|--------|----------|-----|--------|
| `access_token` | `COOKIE_NAMES.ACCESS_TOKEN` | 15 min | `loginUser()` in `src/services/auth.ts` |
| `refresh_token` | `COOKIE_NAMES.REFRESH_TOKEN` | 7 days | `loginUser()` in `src/services/auth.ts` |
| `access_token` | `COOKIE_NAMES.ACCESS_TOKEN` | 15 min | `useLogin()` in `src/services/auth/hooks/useLogin.ts` |
| `refresh_token` | `COOKIE_NAMES.REFRESH_TOKEN` | 7 days | `useLogin()` in `src/services/auth/hooks/useLogin.ts` |
Both are regular (non-httpOnly) cookies so they are readable by both server and client.
**Lifecycle:**
- Written after a successful login via `setClientCookie` with `AUTH_ACCESS_COOKIE_OPTIONS` / `AUTH_REFRESH_COOKIE_OPTIONS`
- Deleted by `logoutUser()` and automatically by `clientFetch` on 401
- Deleted by `useLogout()` (`src/services/auth/hooks/useLogout.ts`) / `useEventLogout()` (`src/hooks/auth.ts`), and automatically by `clientFetch` on 401
- Read by `serverFetch` via `getServerCookie(COOKIE_NAMES.ACCESS_TOKEN)`
- Read by `clientFetch` via `getClientCookie(COOKIE_NAMES.ACCESS_TOKEN)`
@@ -377,6 +464,13 @@ dispatchToast('Something went wrong', 'error')
`ToastBridge` is already rendered in `[locale]/layout.tsx` — do not add another instance.
**Toast colors follow the theme.** `NotistackProvider` maps every notistack variant to a `styled(MaterialDesignContent)` whose `backgroundColor`/`color` come from the `--bal-{success,error,warning,info}` (+ `*-contrast`) tokens in `tokens.css`. Because those tokens are defined on `<html>`, they cascade into notistack's Portal and switch with the color scheme automatically. Never hard-code a toast color — adjust the tokens instead.
**Direction is inherited, not passed.** notistack's Portal mounts under `<body>`, so it inherits `dir` from `<html dir>` (set per-locale in the root layout). Do **not** pass a `dir` prop to `SnackbarProvider` — it is not a valid prop (TS error) and is unnecessary:
```tsx
<NotistackProvider>{children}</NotistackProvider>
```
---
## Route Constants