Files
baya-monorepo/client/CLAUDE.md
T
2026-06-18 01:42:14 +03:30

23 KiB

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/
    │   ├── api/
    │   │   ├── client.ts      # clientFetch<T> — throws ApiError on error; use in hooks/client components
    │   │   ├── server.ts      # serverFetch<T> — throws ApiError on error; use in RSCs/Server Actions
    │   │   └── errors.ts      # ApiError class (status, message, code)
    │   ├── query/
    │   │   ├── queryClient.ts # makeQueryClient factory + getQueryClient() SSR-safe singleton
    │   │   └── QueryProvider.tsx  # 'use client' — QueryClientProvider + ReactQueryDevtools
    │   └── cookies/           # Cookie manager — strict server/client separation
    │       ├── constants.ts   # COOKIE_NAMES, CookieOptions, AUTH_*_COOKIE_OPTIONS
    │       ├── server.ts      # getServerCookie, getThemeMode, setServerCookie
    │       ├── client.ts      # getClientCookie, setClientCookie, deleteClientCookie
    │       └── index.ts       # Re-exports constants ONLY (never server/client)
    ├── services/          # Domain services — no top-level barrel; import directly from the file
    │   └── {domain}/
    │       ├── types.ts       # Request/response types for this domain
    │       ├── keys.ts        # React Query key factory
    │       ├── apis/
    │       │   ├── clientApi.ts   # Namespace object wrapping clientFetch calls
    │       │   └── serverApi.ts   # Namespace object wrapping serverFetch calls (only when needed)
    │       └── hooks/
    │           └── 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
    │   ├── 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-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.

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:

import { useTranslations } from 'next-intl';

function MyComponent() {
  const t = useTranslations('nav'); // namespace
  return <span>{t('home')}</span>;  // key
}

Using translations in Server Components:

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.


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 arrivesgetThemeMode() 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:

// 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 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').
  • Each domain does have an index.ts that re-exports its hooks (e.g. src/services/auth/index.ts). Do not export types, keys, or apis/* from this barrel — only hooks.
  • Do not mix clientFetch and serverFetch in the same file — keep clientApi.ts and serverApi.ts separate; Next.js enforces the environment boundary at build time.
  • Do not toast inside hooks for 401/403/5xx — those are already toasted by clientFetch. Only toast in onError for domain-specific 4xx messages.
  • 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.

API Fetch Services

Central fetch primitives live in src/lib/api/:

File Use from Purpose
client.ts hooks, client components clientFetch<T> — throws ApiError on error
server.ts RSCs, Server Actions only serverFetch<T> — throws ApiError on error
errors.ts anywhere ApiError class (status, message, code)

Error contract — clientFetch:

  • 401 — toast "session expired", clear cookies, redirect to login (no throw; page navigates away)
  • 403 — toast "forbidden", throw ApiError
  • 5xx — toast "server error", throw ApiError
  • Other 4xx — throw ApiError, no toast; the calling hook owns the user-facing message
  • Network failure — toast "network error", throw ApiError

Error contract — serverFetch:

  • All errors throw ApiError (no toast — server can't fire browser events)
  • RSC callers decide whether to notFound(), redirect(), or let the error propagate to an error boundary

Domain API calls live in src/services/{domain}/apis/clientApi.ts (or serverApi.ts). Never call raw fetch() directly.


Auth Cookies

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

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
  • Read by serverFetch via getServerCookie(COOKIE_NAMES.ACCESS_TOKEN)
  • Read by clientFetch via getClientCookie(COOKIE_NAMES.ACCESS_TOKEN)

Middleware: validates the exp claim of the access token locally (base64-decode the JWT payload, no signature check) before rendering any private page. An absent or expired token redirects to /{locale}/login.

useIsAuthenticated() hook: SSR-safe — initialises false, sets the real value in useEffect by reading the cookie. This prevents hydration mismatches.


Toast Notifications (notistack)

<SnackbarProvider> wraps all children inside ThemeProvider in src/app/[locale]/layout.tsx.

In React components/hooks — use notistack directly:

import { useSnackbar } from 'notistack'
const { enqueueSnackbar } = useSnackbar()
enqueueSnackbar('Saved!', { variant: 'success' })

Outside React (plain functions, fetch services) — use the event bridge:

import { dispatchToast } from '@/lib/toast'
dispatchToast('Something went wrong', 'error')

dispatchToast fires a window CustomEvent (app:toast). ToastBridge (a zero-UI 'use client' component inside SnackbarProvider) listens and calls enqueueSnackbar.

ToastBridge is already rendered in [locale]/layout.tsx — do not add another instance.


Route Constants

Named path constants live in src/constants/routes.ts:

ROUTES.LOGIN  = '/login'
ROUTES.HOME   = '/'
PUBLIC_PATHS  = [ROUTES.LOGIN, ...]  // paths that bypass middleware auth check

Import from the barrel: import { ROUTES, PUBLIC_PATHS } from '@/constants'.

To add a new public route, append it to PUBLIC_PATHS — the middleware picks it up automatically.


src/lib/cookies/client.ts uses js-cookie internally. The exported API is unchanged:

Function Purpose
getClientCookie(name) Read a cookie by name
setClientCookie(name, value, options?) Write a cookie; options is CookieOptions with maxAge in seconds
deleteClientCookie(name, path?) Delete a cookie
getColorSchemeCookie() Typed helper for the theme cookie

CookieOptions type is defined in src/lib/cookies/constants.tsmaxAge is in seconds (converted to expires: Date internally when calling js-cookie).

  • Do not document.title = title in the render body of any component — it causes ReferenceError: document is not defined during build-time prerendering.