Files
baya-monorepo/client/CLAUDE.md
T
2026-06-21 00:05:07 +03:30

30 KiB

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.
  • Product/domain rules (what to build) → 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 typetsc --noEmit. Config in tsconfig.json: strict on, noEmit, @/*src/*.
  • npm run linteslint . 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

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/
    │   ├── globals.css
    │   ├── fonts/                              # Local font files (woff2) — Mikhak for fa
    │   └── [locale]/
    │       ├── layout.tsx                      # ROOT RSC: renders <html lang/dir> + fonts + 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/
    │   ├── 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 (incl. ColorSchemeScript, ThemeProvider, getDirection)
    ├── constants/         # App-wide constants (routes, events, etc.)
    ├── hooks/
    ├── utils/
    └── config.ts

Server / Client Component Boundaries

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.

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.
  • 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.


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).

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.

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.

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/.


Fonts

Fonts are loaded per locale — the Persian face is never shipped to English pages:

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)
  • 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:

  • 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.

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 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/[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').
  • 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.
  • 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

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 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 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)

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.

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:

<NotistackProvider>{children}</NotistackProvider>

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.