another step for constructing base project
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
# Balinyaar Client — Claude Code Guidelines
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
client/
|
||||
├── messages/ # Translation files (add keys to BOTH files)
|
||||
│ ├── en.json
|
||||
│ └── fa.json
|
||||
├── middleware.ts # next-intl routing middleware (locale detection + redirect)
|
||||
├── 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
|
||||
│ ├── (private-routes)/
|
||||
│ │ ├── layout.tsx # 'use client' — wraps PrivateLayout
|
||||
│ │ └── page.tsx
|
||||
│ └── (public-routes)/
|
||||
│ └── layout.tsx # 'use client' — wraps PublicLayout
|
||||
├── components/ # Shared UI components (each with .test.tsx if imported >1 place)
|
||||
├── i18n/
|
||||
│ ├── routing.ts # defineRouting — locales: ['en', 'fa'], defaultLocale: 'fa'
|
||||
│ └── request.ts # getRequestConfig — loads messages/${locale}.json
|
||||
├── layout/
|
||||
│ ├── PrivateLayout.tsx # 'use client' — authenticated shell; uses useTranslations('nav')
|
||||
│ ├── PublicLayout.tsx # unauthenticated shell
|
||||
│ ├── TopBarAndSideBarLayout.tsx # 'use client' — TopBar + SideBar composition
|
||||
│ ├── config.ts
|
||||
│ ├── index.ts
|
||||
│ └── components/
|
||||
│ ├── TopBar.tsx
|
||||
│ ├── SideBar.tsx
|
||||
│ ├── SideBarNavList.tsx
|
||||
│ ├── SideBarNavItem.tsx
|
||||
│ ├── DarkModeButton.tsx # 'use client' — only subscriber to useColorScheme()
|
||||
│ └── index.tsx
|
||||
├── lib/
|
||||
│ └── cookies/ # Cookie manager — strict server/client separation
|
||||
│ ├── constants.ts # COOKIE_NAMES, CookieOptions, COLOR_SCHEME_COOKIE_OPTIONS
|
||||
│ ├── server.ts # getServerCookie, getThemeMode, setServerCookie
|
||||
│ ├── client.ts # getClientCookie, setClientCookie, deleteClientCookie
|
||||
│ └── index.ts # Re-exports constants ONLY (never server/client)
|
||||
├── 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
|
||||
│ ├── 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
|
||||
├── constants/ # App-wide constants (routes, events, etc.)
|
||||
├── hooks/
|
||||
├── utils/
|
||||
└── config.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server / Client Component Boundaries
|
||||
|
||||
**Root layout** (`src/app/layout.tsx`) is a **lean Server Component** (RSC). It:
|
||||
- Reads locale from the **`x-next-intl-locale` request header** 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.
|
||||
|
||||
**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:
|
||||
- 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.
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## i18n (next-intl v4)
|
||||
|
||||
**Adding translations:**
|
||||
1. Add the key to `messages/en.json` AND `messages/fa.json`. Both files must always be in sync.
|
||||
2. Top-level keys are namespaces: `"nav"`, `"common"`, etc.
|
||||
|
||||
**Using translations in client components:**
|
||||
```tsx
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
function MyComponent() {
|
||||
const t = useTranslations('nav'); // namespace
|
||||
return <span>{t('home')}</span>; // key
|
||||
}
|
||||
```
|
||||
|
||||
**Using translations in Server Components:**
|
||||
```tsx
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
async function MyServerComponent() {
|
||||
const t = await getTranslations('nav');
|
||||
return <span>{t('home')}</span>;
|
||||
}
|
||||
```
|
||||
|
||||
**Established namespaces and where they're used:**
|
||||
- `'nav'` — `PrivateLayout.tsx` (sidebar nav items)
|
||||
- `'common'` — `DarkModeButton.tsx` (dark/light mode labels)
|
||||
|
||||
**Never hard-code UI strings in English.** Any user-visible text must have a translation key in both locale files.
|
||||
|
||||
---
|
||||
|
||||
## Cookie Manager
|
||||
|
||||
The cookie manager in `src/lib/cookies/` is split into three files to prevent cross-environment bundling:
|
||||
|
||||
| File | Use from | Purpose |
|
||||
|------|----------|---------|
|
||||
| `constants.ts` | anywhere | `COOKIE_NAMES`, `CookieOptions`, `COLOR_SCHEME_COOKIE_OPTIONS` |
|
||||
| `server.ts` | Server Components, Server Actions, Route Handlers only | `getServerCookie`, `getThemeMode`, `setServerCookie` |
|
||||
| `client.ts` | client components / `useEffect` only | `getClientCookie`, `setClientCookie`, `deleteClientCookie` |
|
||||
| `index.ts` | anywhere | Re-exports `constants.ts` only — safe barrel |
|
||||
|
||||
**Rules:**
|
||||
- Import constants via the barrel: `import { COOKIE_NAMES } from '@/lib/cookies'`
|
||||
- Import server utils directly: `import { getThemeMode } from '@/lib/cookies/server'`
|
||||
- Import client utils directly: `import { setClientCookie } from '@/lib/cookies/client'`
|
||||
- Never import `server.ts` in a client component; never import `client.ts` in an RSC.
|
||||
- `COOKIE_NAMES.COLOR_SCHEME = 'color-scheme'` — the single source of truth for the theme cookie name. Do not redeclare it anywhere.
|
||||
|
||||
---
|
||||
|
||||
## Constants
|
||||
|
||||
**Rule: every magic string or configurable value must be a named constant — never inline.**
|
||||
|
||||
A value is "magic" if its meaning isn't obvious from the literal alone: cookie names, event names, localStorage keys, route paths, query-param names, numeric timeouts, API endpoint slugs.
|
||||
|
||||
Where to define:
|
||||
- **Cookie names / options**: `src/lib/cookies/constants.ts`
|
||||
- **Feature-scope constants**: co-locate in a `constants.ts` next to that feature's files
|
||||
- **App-wide constants** (used across multiple features): `src/constants/` — one file per concern (`routes.ts`, `events.ts`, etc.)
|
||||
|
||||
Rules:
|
||||
1. Import the constant; never copy-paste the string value.
|
||||
2. When renaming, update the constant definition — the rest of the codebase follows automatically.
|
||||
|
||||
---
|
||||
|
||||
## Theme System
|
||||
|
||||
### How it works (end-to-end, no-flash)
|
||||
|
||||
1. **Request arrives** → `getThemeMode()` reads `'color-scheme'` cookie → returns `{ colorScheme, defaultMode }`
|
||||
2. **Root layout** sets `data-mui-color-scheme={colorScheme}` on `<html>` server-side
|
||||
3. **`<ColorSchemeScript />`** in `<head>` runs before any paint:
|
||||
- Reads the same cookie, sets `data-mui-color-scheme` (handles edge cases where server attr might differ)
|
||||
- Patches `Storage.prototype` — routes MUI's `localStorage` writes for key `'mode'` to our cookie; reads return `null` so MUI always trusts the `defaultMode` prop
|
||||
4. **`<MuiThemeProvider defaultMode={defaultMode}>`** mounts — uses the server-derived mode, not localStorage
|
||||
5. **`ColorSchemeCookieSync`** in ThemeProvider writes the cookie via `useColorScheme().colorScheme` on mount (safety net for first-visit system mode)
|
||||
|
||||
### Critical MUI v9 rules
|
||||
|
||||
**`colorSchemeSelector` must be the explicit attribute name:**
|
||||
```ts
|
||||
// theme.ts
|
||||
cssVariables: {
|
||||
colorSchemeSelector: 'data-mui-color-scheme', // CORRECT
|
||||
// colorSchemeSelector: 'data', // WRONG — produces boolean data-dark/data-light
|
||||
},
|
||||
```
|
||||
The shorthand `'data'` in MUI v9 generates `[data-%s]` → `data-dark=""` / `data-light=""` (boolean attributes). Our `tokens.css` uses `[data-mui-color-scheme="dark"]` which never matches boolean attributes. Always use the explicit attribute name.
|
||||
|
||||
**Never use `storageWindow={null}`:**
|
||||
In MUI v9's `localStorageManager`, the check is `if (!storageWindow && typeof window !== 'undefined')` — `null` is falsy, so it silently overrides to `window`. This prop is a no-op in browsers. The `Storage.prototype` patch in `ColorSchemeScript` is the correct intercept.
|
||||
|
||||
**Never use MUI's `InitColorSchemeScript`:**
|
||||
It reads from localStorage which diverges from our cookie (especially in 'system' mode). Use `ColorSchemeScript` from `@/theme` instead.
|
||||
|
||||
**MUI v9 localStorage key defaults (different from v5/v6):**
|
||||
- Mode key: `'mode'` (was `'mui-mode'`)
|
||||
- Color scheme key: `'color-scheme'` (was `'mui-color-scheme'`)
|
||||
- HTML attribute: `'data-color-scheme'` (was `'data-mui-color-scheme'`)
|
||||
|
||||
We override all of these via `colorSchemeSelector: 'data-mui-color-scheme'` in the theme and the Storage.prototype patch.
|
||||
|
||||
### Color tokens
|
||||
|
||||
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)`.
|
||||
|
||||
### 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`.
|
||||
|
||||
### Toggle components
|
||||
|
||||
`DarkModeToggleButton` and `DarkModeFormSwitch` in `src/layout/components/DarkModeButton.tsx` are the **only** components that subscribe to `useColorScheme()`. When the user toggles:
|
||||
1. `setMode('dark')` is called
|
||||
2. `Storage.prototype.setItem` intercept fires → writes `'color-scheme'='dark'` cookie synchronously
|
||||
3. MUI sets `data-mui-color-scheme="dark"` on `<html>`
|
||||
4. CSS variables resolve → browser repaints. No React re-render above the button.
|
||||
|
||||
Use `colorScheme` (not `mode`) for the `isDark` check — `mode` can be `'system'` even when dark is active.
|
||||
|
||||
---
|
||||
|
||||
## Direction (RTL / LTR)
|
||||
|
||||
Derived from locale via `getDirection(locale)` in `src/theme/direction.ts`:
|
||||
- RTL locales: `fa`, `ar`, `he`, `ur`
|
||||
- All others: `ltr`
|
||||
|
||||
`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.
|
||||
|
||||
**Default locale is `fa` (RTL).** The middleware redirects bare `/` to `/fa/`. English is explicitly accessed at `/en/`.
|
||||
|
||||
---
|
||||
|
||||
## Fonts
|
||||
|
||||
Two fonts are loaded on every request (both CSS variables are always defined on `<html>`):
|
||||
|
||||
| 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` |
|
||||
|
||||
**Typography exports:**
|
||||
- `TYPOGRAPHY_LTR` — Space Grotesk headings, system font body (used by `APP_THEME_LTR`)
|
||||
- `TYPOGRAPHY_RTL` — Mikhak for all text including body (used by `APP_THEME_RTL`, ensures full Persian glyph coverage)
|
||||
- `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`.
|
||||
|
||||
---
|
||||
|
||||
## Unit Testing
|
||||
|
||||
**Rule: every shared component must have a co-located test file.**
|
||||
|
||||
A component is "shared" if it is imported from more than one place (page, layout, or other component).
|
||||
|
||||
Coverage baseline for shared components:
|
||||
1. It renders without crashing.
|
||||
2. Every documented prop produces the correct HTML attribute or CSS class.
|
||||
3. User interactions (click, change) call the expected callbacks.
|
||||
|
||||
Test location: `src/components/ComponentName/ComponentName.test.tsx` next to the component.
|
||||
Test wrapper: wrap with `<ThemeProvider>` if the component uses MUI theming.
|
||||
Do NOT mock MUI components — test against the rendered DOM.
|
||||
|
||||
Enforcement: before removing or renaming a shared component, check whether `src/**/*.test.{ts,tsx}` files import it. If so, update or delete those tests too.
|
||||
|
||||
---
|
||||
|
||||
## Anti-patterns (do not do these)
|
||||
|
||||
- **Do not** read `localStorage` or `document.cookie` in render functions — use `useEffect` or server-side `cookies()` from `next/headers`.
|
||||
- **Do not** call `createTheme()` inside a component or hook — use `APP_THEME_LTR` / `APP_THEME_RTL`.
|
||||
- **Do not** use `storageWindow={null}` on `MuiThemeProvider` — it is silently ignored in MUI v9.
|
||||
- **Do not** use `InitColorSchemeScript` from MUI — use `ColorSchemeScript` from `@/theme`.
|
||||
- **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** 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** import `@/lib/cookies/server` in client components or `@/lib/cookies/client` in RSCs.
|
||||
- **Do not** `document.title = title` in the render body of any component — it causes `ReferenceError: document is not defined` during build-time prerendering.
|
||||
Reference in New Issue
Block a user