another step just a little remaining
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"rules": {
|
||||
"import/no-cycle": "error"
|
||||
}
|
||||
}
|
||||
+8
-62
@@ -1,66 +1,12 @@
|
||||
# AGENTS.md — Balinyaar Web Client
|
||||
|
||||
Agent-oriented guide to the frontend. For human setup/run instructions see [README.md](README.md).
|
||||
The canonical agent guide for the frontend is **[CLAUDE.md](CLAUDE.md)** (same folder). It is the
|
||||
engineering contract: stack, commands, lint/type gates, routing, providers, data fetching, theming,
|
||||
i18n, cookies, and the rules every change must follow.
|
||||
|
||||
## Stack
|
||||
- Repo-wide context → [../CLAUDE.md](../CLAUDE.md)
|
||||
- Human setup/run instructions → [README.md](README.md)
|
||||
- UI/design work → the **frontend-designer** skill
|
||||
|
||||
- **Next.js** with the **App Router** (`src/app/`), statically exported (`output: 'export'` in `next.config.mjs` → builds to `out/`)
|
||||
- **React** + **TypeScript** (`strict` mode)
|
||||
- **Material UI (MUI)** for components and theming (Emotion under the hood)
|
||||
- **Jest** + **Testing Library** for unit tests
|
||||
- ESLint (`eslint-config-next`) + Prettier
|
||||
|
||||
## Commands
|
||||
|
||||
| Task | Command |
|
||||
| ------------ | ------------------ |
|
||||
| Dev server | `npm run dev` |
|
||||
| Build | `npm run build` |
|
||||
| Lint | `npm run lint` |
|
||||
| Type-check | `npm run type` |
|
||||
| Format | `npm run format` |
|
||||
| Test (watch) | `npm test` |
|
||||
| Test (CI) | `npm run test:ci` |
|
||||
|
||||
Always run `npm run lint` and `npm run type` after a change. Run `npm run test:ci` when you touch a component that has a `*.test.tsx`.
|
||||
|
||||
## Directory map
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ Routes (App Router). Each folder = a route; page.tsx = the page.
|
||||
│ ├── layout.tsx Root layout: store + theme providers, global <metadata>
|
||||
│ ├── page.tsx "/" entry (delegates to home)
|
||||
│ ├── home/ about/ Content pages
|
||||
│ ├── auth/login|signup Auth pages (LoginForm is currently a stub)
|
||||
│ └── me/ Authenticated user page
|
||||
├── components/
|
||||
│ ├── common/ Reusable App* primitives (see below) + ErrorBoundary
|
||||
│ └── UserInfo/ Logged-in user summary
|
||||
├── hooks/ useIsAuthenticated, useIsMobile, event hooks, useWindowSize
|
||||
├── layout/ PublicLayout / PrivateLayout + TopBar, SideBar, BottomBar, config
|
||||
├── store/ Global state: React context + reducer (AppStore, AppReducer)
|
||||
├── theme/ ThemeProvider, light/dark palettes, colors, MUI-for-Next bridge
|
||||
└── utils/ storage (local/session), navigation, environment, text, types
|
||||
```
|
||||
|
||||
## Conventions (follow these)
|
||||
|
||||
- **Imports**: use the `@/*` alias for anything under `src/` (e.g. `import { AppButton } from '@/components'`). The alias is defined in `tsconfig.json`.
|
||||
- **Barrel files**: most folders export through an `index.ts(x)`. Add new public exports there (e.g. a new common component is re-exported from `src/components/common/index.tsx`, which `src/components/index.tsx` re-exports).
|
||||
- **Common components** live in `src/components/common/<Name>/` as a folder containing `<Name>.tsx`, `index.tsx` (re-export), and an optional `<Name>.test.tsx`. Existing ones: `AppAlert`, `AppButton`, `AppIcon`, `AppIconButton`, `AppImage`, `AppLink`, `AppLoading`. Prefer reusing these over raw MUI where one exists.
|
||||
- **Icons**: reference icons by string name through `AppIcon`. The name→icon map is in `src/components/common/AppIcon/config.ts`; add new icons there (custom SVGs go in `AppIcon/icons/`).
|
||||
- **Navigation**: sidebar/bottom-bar items are arrays of `LinkToPage` defined inside `PublicLayout.tsx` / `PrivateLayout.tsx`. Add a route → add an entry there to surface it in the nav.
|
||||
- **Two layouts**: `PrivateLayout` (after auth) and `PublicLayout` (before auth); `CurrentLayout` picks between them based on auth state. The app name lives in the `TITLE_PRIVATE` / `TITLE_PUBLIC` constants in those files.
|
||||
- **Auth is a stub.** `src/hooks/auth.ts` and `src/app/auth/login/LoginForm.tsx` fake login by writing a placeholder token to session storage (`// TODO: AUTH:`). When implementing real auth, replace those spots and point requests at `NEXT_PUBLIC_API_URL` (the `server` project's JWT endpoints).
|
||||
- **Theming**: use the theme/palette via MUI's `sx` / `useTheme`; don't hardcode colors. Light/dark values are in `src/theme/light.ts` and `dark.ts`; dark-mode toggle flows through the store.
|
||||
- **Client vs server components**: files needing hooks/browser APIs start with `'use client';` (see `LoginForm.tsx`). Pages that only render markup can stay server components.
|
||||
|
||||
## Environment
|
||||
|
||||
Browser-exposed config comes from `NEXT_PUBLIC_*` variables (copy `.env.sample` → `.env`). The ones actually read in code: `NEXT_PUBLIC_ENV`, `NEXT_PUBLIC_DEBUG`, `NEXT_PUBLIC_PUBLIC_URL` (see `src/config.ts`) and `NEXT_PUBLIC_VERSION` (see `src/utils/environment.ts`). `NEXT_PUBLIC_API_URL` is the backend base URL.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Static export (`output: 'export'`) means **no server-side runtime** — no API routes, no SSR-only features, `images.unoptimized` is on.
|
||||
- Don't reintroduce the removed demo/showcase route (`src/app/dev`) or `Demo*` components — they were template content.
|
||||
`CLAUDE.md` is the single source of truth; this file is just a pointer so the convention is
|
||||
discoverable under the `AGENTS.md` name too.
|
||||
|
||||
+125
-31
@@ -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
|
||||
|
||||
+36
-18
@@ -1,8 +1,14 @@
|
||||
# Balinyaar — Web Client
|
||||
|
||||
Frontend for the Balinyaar application, built with **Next.js (App Router)**, **React**, **TypeScript**, and **Material UI (MUI)**.
|
||||
Frontend for **Balinyaar**, a trust-first home-nursing marketplace in Iran. Built with
|
||||
**Next.js 16 (App Router)**, **React 19**, **TypeScript**, and **Material UI (MUI) v9**.
|
||||
|
||||
It ships with a small library of reusable `App*` components, a theming system (light/dark), a lightweight global store, and public/private layouts wired for an authentication flow.
|
||||
It ships with internationalization (`fa`/`en`, RTL-first), a no-flash light/dark theme system, a small
|
||||
library of reusable `App*` components, TanStack Query for server state, and public/private layout
|
||||
shells wired for cookie-based authentication.
|
||||
|
||||
> **AI agents:** read [CLAUDE.md](CLAUDE.md) — it is the engineering contract (architecture, providers,
|
||||
> data fetching, theming, i18n, cookies, and the rules every change must follow).
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -38,39 +44,51 @@ It ships with a small library of reusable `App*` components, a theming system (l
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000).
|
||||
Open [http://localhost:3000](http://localhost:3000). The root path redirects to `/fa` (default locale).
|
||||
|
||||
## Available scripts
|
||||
|
||||
| Script | Description |
|
||||
| ------------------ | ------------------------------------------------------- |
|
||||
| `npm run dev` | Start the development server with hot reload |
|
||||
| `npm run build` | Production build (static export to `out/`) |
|
||||
| `npm run start` | Serve a production build |
|
||||
| `npm run lint` | Run ESLint (`eslint-config-next`) |
|
||||
| `npm run dev` | Start the development server (Turbopack) with hot reload |
|
||||
| `npm run build` | Production build |
|
||||
| `npm run start` | Serve a production build |
|
||||
| `npm run lint` | Lint with ESLint (flat config, `eslint .`) |
|
||||
| `npm run lint:fix` | Lint and auto-fix |
|
||||
| `npm run type` | Type-check with `tsc --noEmit` |
|
||||
| `npm run check` | Type-check **and** lint (the quality gate) |
|
||||
| `npm run format` | Format the codebase with Prettier |
|
||||
| `npm test` | Run Jest in watch mode |
|
||||
| `npm run test:ci` | Run Jest once (CI) |
|
||||
| `npm run type` | Type-check with `tsc` |
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ Next.js App Router routes (home, about, auth, me) + root layout
|
||||
├── components/ Reusable UI: common App* components (AppButton, AppIcon, ...) + UserInfo
|
||||
├── hooks/ Custom hooks (auth, layout, events, window size)
|
||||
├── layout/ PublicLayout / PrivateLayout + TopBar, SideBar, BottomBar
|
||||
├── store/ Global app store (React context + reducer)
|
||||
├── theme/ MUI theme provider, light/dark palettes, colors
|
||||
└── utils/ Helpers (storage, navigation, env, text, types)
|
||||
├── app/[locale]/ App Router routes, grouped into (public-routes) and (private-routes).
|
||||
│ [locale]/layout.tsx is the ROOT layout (renders <html>/<body>).
|
||||
├── components/ Reusable UI: common App* components (AppButton, AppIcon, …) + UserInfo
|
||||
├── constants/ App-wide named constants (routes, headers, …)
|
||||
├── hooks/ Custom hooks (auth, layout/mobile, events, window size)
|
||||
├── i18n/ next-intl routing + request config
|
||||
├── layout/ PublicLayout / PrivateLayout + TopBar, SideBar, BottomBar
|
||||
├── lib/ api (client/server fetch), cookies, query (TanStack), toast
|
||||
├── services/ Domain services: {domain}/{types,keys,apis,hooks}
|
||||
├── store/ AppStore (React context + reducer) for client state
|
||||
├── theme/ ThemeProvider, palettes, tokens.css, typography, direction
|
||||
└── utils/ Helpers (storage, navigation, env, text, types)
|
||||
```
|
||||
|
||||
The `@/*` import alias maps to `src/*` (see `tsconfig.json`).
|
||||
Translation files live in [`messages/`](messages) (`en.json` / `fa.json`) and must stay in sync.
|
||||
|
||||
> For a deeper, agent-oriented map of conventions and where to make changes, see [AGENTS.md](AGENTS.md).
|
||||
> For the full agent-oriented map of conventions and where to make changes, see [CLAUDE.md](CLAUDE.md).
|
||||
|
||||
## Notes
|
||||
|
||||
- `next.config.mjs` uses `output: 'export'`, producing a static site in `out/`.
|
||||
- Authentication in `src/hooks/auth.ts` and `src/app/auth/login/LoginForm.tsx` is currently a stub (look for `// TODO: AUTH:`); wire it to the backend's JWT endpoints when implementing real auth.
|
||||
- This is a server-rendered Next.js app (App Router + middleware + server-side cookies) — **not** a
|
||||
static export.
|
||||
- Internationalization is locale-prefixed (`/fa`, `/en`); `fa` is the default and is RTL.
|
||||
- Authentication is cookie-based (`access_token` / `refresh_token`), managed through
|
||||
`src/lib/cookies/` and the `src/services/auth/` hooks. The API base URL comes from
|
||||
`NEXT_PUBLIC_API_URL`.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Flat ESLint config (ESLint 10 / Next.js 16).
|
||||
//
|
||||
// `next lint` was removed in Next 16, so linting runs through the ESLint CLI
|
||||
// directly: `npm run lint`. The eslint-config-next package (v16) ships a
|
||||
// ready-made flat-config array that wires up the Next core-web-vitals rules,
|
||||
// the TypeScript rules, react / react-hooks / jsx-a11y / import plugins and the
|
||||
// TypeScript parser — so we just spread it and layer our project rules on top.
|
||||
|
||||
import next from 'eslint-config-next';
|
||||
import prettier from 'eslint-config-prettier/flat';
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
const config = [
|
||||
// Things ESLint should never look at.
|
||||
{
|
||||
ignores: ['.next/**', 'out/**', 'coverage/**', 'node_modules/**', 'next-env.d.ts'],
|
||||
},
|
||||
|
||||
// Next.js + TypeScript + React + import/a11y rule sets.
|
||||
...next,
|
||||
|
||||
// Turn off every stylistic rule that would fight Prettier. Keep this last
|
||||
// among the rule-providing entries so it wins. Formatting is owned by
|
||||
// Prettier (`npm run format`), never by ESLint.
|
||||
prettier,
|
||||
|
||||
// Project-specific rule overrides go here, e.g.:
|
||||
// { rules: { 'react/jsx-key': 'error' } }
|
||||
//
|
||||
// NOTE: `import/no-cycle` is intentionally NOT enabled. On this toolchain the
|
||||
// eslint-plugin-import TypeScript resolver bundled by eslint-config-next 16
|
||||
// throws "invalid interface loaded as resolver" for that rule, and it cannot
|
||||
// follow the `@/*` path alias to trace cycles anyway. Re-add it once the
|
||||
// import-resolver-typescript interface is compatible.
|
||||
];
|
||||
|
||||
export default config;
|
||||
@@ -8,5 +8,17 @@
|
||||
"light_mode": "Light mode",
|
||||
"direction_ltr": "Switch to LTR",
|
||||
"direction_rtl": "Switch to RTL"
|
||||
},
|
||||
"toastDemo": {
|
||||
"title": "Toast Notifications Demo",
|
||||
"subtitle": "Click a button to trigger each toast type",
|
||||
"success_btn": "Success",
|
||||
"error_btn": "Error",
|
||||
"warning_btn": "Warning",
|
||||
"info_btn": "Info",
|
||||
"success_msg": "Profile saved successfully!",
|
||||
"error_msg": "Failed to load data. Please try again.",
|
||||
"warning_msg": "Your session will expire in 5 minutes.",
|
||||
"info_msg": "A new version of the app is available."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,17 @@
|
||||
"light_mode": "حالت روشن",
|
||||
"direction_ltr": "تغییر به چپبهراست",
|
||||
"direction_rtl": "تغییر به راستبهچپ"
|
||||
},
|
||||
"toastDemo": {
|
||||
"title": "نمایش اعلانهای Toast",
|
||||
"subtitle": "روی هر دکمه کلیک کنید تا نوع مربوطه نمایش داده شود",
|
||||
"success_btn": "موفقیت",
|
||||
"error_btn": "خطا",
|
||||
"warning_btn": "هشدار",
|
||||
"info_btn": "اطلاعات",
|
||||
"success_msg": "پروفایل با موفقیت ذخیره شد!",
|
||||
"error_msg": "بارگذاری اطلاعات ناموفق بود. لطفاً دوباره تلاش کنید.",
|
||||
"warning_msg": "جلسه شما تا ۵ دقیقه دیگر منقضی میشود.",
|
||||
"info_msg": "نسخه جدیدی از برنامه در دسترس است."
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2240
-2994
File diff suppressed because it is too large
Load Diff
+8
-4
@@ -7,11 +7,13 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"format": "prettier ./ --write",
|
||||
"lint": "next lint",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"start": "next start",
|
||||
"test": "jest --watch",
|
||||
"test:ci": "jest --ci",
|
||||
"type": "tsc"
|
||||
"type": "tsc --noEmit",
|
||||
"check": "npm run type && npm run lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.14.0",
|
||||
@@ -35,6 +37,7 @@
|
||||
"stylis-plugin-rtl": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -43,8 +46,9 @@
|
||||
"@types/node": "^25.9.3",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"eslint": "latest",
|
||||
"eslint-config-next": "latest",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "^16.2.9",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"jest": "latest",
|
||||
"jest-environment-jsdom": "latest",
|
||||
"next-router-mock": "latest",
|
||||
|
||||
@@ -2,14 +2,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { PrivateLayout } from '@/layout';
|
||||
|
||||
/*
|
||||
* Wraps all private (authenticated) routes with the application shell:
|
||||
* TopBar, SideBar, and main content area.
|
||||
*
|
||||
* Authentication enforcement belongs in middleware — this layout only
|
||||
* applies the visual structure. Add a middleware matcher for this group
|
||||
* when real session-based auth is introduced.
|
||||
*/
|
||||
|
||||
export default function PrivateRouteLayout({ children }: { children: ReactNode }) {
|
||||
return <PrivateLayout>{children}</PrivateLayout>;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Box from '@mui/material/Box'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
export default function HomePage() {
|
||||
return null;
|
||||
const t = useTranslations('toastDemo')
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 4 }}>
|
||||
<Typography>Balin yaar</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,61 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import localFont from 'next/font/local';
|
||||
import { setRequestLocale, getMessages } from 'next-intl/server';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getThemeMode } from '@/lib/cookies/server';
|
||||
import { AppStoreProvider } from '@/store';
|
||||
import { ThemeProvider, getDirection } from '@/theme';
|
||||
import { BRAND } from '@/theme/colors';
|
||||
import { NotistackProvider } from '@/lib/toast';
|
||||
import { QueryProvider } from '@/lib/query/QueryProvider';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import '../globals.css';
|
||||
import '@/theme/tokens.css';
|
||||
|
||||
/*
|
||||
* This layout is the correct place for NextIntlClientProvider because it
|
||||
* receives the locale directly from URL params — no header reads, no caching
|
||||
* surprises. setRequestLocale(locale) is called first so that any server
|
||||
* component deeper in the tree that calls getLocale() / getTranslations()
|
||||
* gets the right locale from React.cache instead of falling through to the
|
||||
* header fallback.
|
||||
* This is the application's ROOT layout — it renders <html> and <body>.
|
||||
*
|
||||
* getMessages({ locale }) passes the locale explicitly so the config
|
||||
* callback in src/i18n/request.ts receives it via requestLocale directly
|
||||
* (Promise.resolve(locale)) rather than reading it from the cache — an
|
||||
* extra layer of defense against cache-ordering races.
|
||||
* Why <html> lives here and NOT in a layout above the [locale] segment:
|
||||
* `lang` and `dir` must track the active locale, and the only layout that
|
||||
* re-renders when the locale changes is the one keyed on the [locale] param.
|
||||
* A layout placed above [locale] is shared between /fa and /en, so it is
|
||||
* statically cached with the defaultLocale and never re-renders on a locale
|
||||
* switch — leaving `dir`/`lang` frozen on the default ('fa'/'rtl'). Sourcing
|
||||
* the locale from URL params here means no header reads, no caching surprises.
|
||||
*
|
||||
* setRequestLocale(locale) is called first so that any server component
|
||||
* deeper in the tree that calls getLocale() / getTranslations() gets the
|
||||
* right locale from React.cache instead of falling through to the header
|
||||
* fallback. getMessages({ locale }) passes the locale explicitly so the
|
||||
* config callback in src/i18n/request.ts receives it via requestLocale
|
||||
* directly (Promise.resolve(locale)) rather than reading it from the cache.
|
||||
*/
|
||||
|
||||
// FA brand font — Mikhak, a free Persian typeface.
|
||||
// preload: false + conditional className (below) ensure the woff2 files are
|
||||
// only fetched on Persian routes, never on /en.
|
||||
const mikhak = localFont({
|
||||
src: [
|
||||
{ path: '../fonts/Mikhak-Regular.woff2', weight: '400', style: 'normal' },
|
||||
{ path: '../fonts/Mikhak-Medium.woff2', weight: '500', style: 'normal' },
|
||||
{ path: '../fonts/Mikhak-Bold.woff2', weight: '700', style: 'normal' },
|
||||
],
|
||||
display: 'swap',
|
||||
variable: '--font-mikhak',
|
||||
preload: false,
|
||||
});
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: BRAND.teal,
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Balinyaar | بالینیار',
|
||||
description: 'Balinyaar web application',
|
||||
manifest: '/site.webmanifest',
|
||||
};
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params,
|
||||
@@ -37,21 +72,34 @@ export default async function LocaleLayout({
|
||||
setRequestLocale(safeLocale);
|
||||
|
||||
const messages = await getMessages({ locale: safeLocale });
|
||||
const { defaultMode } = await getThemeMode();
|
||||
const { colorScheme, defaultMode } = await getThemeMode();
|
||||
const dir = getDirection(safeLocale);
|
||||
|
||||
// Only attach the Mikhak font variable on RTL (Persian) routes so the
|
||||
// Persian typeface is not loaded for English pages.
|
||||
const fontClassName = safeLocale === 'fa' ? mikhak.variable : undefined;
|
||||
|
||||
return (
|
||||
<NextIntlClientProvider locale={safeLocale} messages={messages}>
|
||||
<AppStoreProvider>
|
||||
<ThemeProvider dir={dir} defaultMode={defaultMode}>
|
||||
<QueryProvider>
|
||||
<NotistackProvider>
|
||||
{children}
|
||||
</NotistackProvider>
|
||||
</QueryProvider>
|
||||
</ThemeProvider>
|
||||
</AppStoreProvider>
|
||||
</NextIntlClientProvider>
|
||||
<html
|
||||
lang={safeLocale}
|
||||
dir={dir}
|
||||
className={fontClassName}
|
||||
data-mui-color-scheme={colorScheme}
|
||||
>
|
||||
<body>
|
||||
<NextIntlClientProvider locale={safeLocale} messages={messages}>
|
||||
<AppStoreProvider>
|
||||
<ThemeProvider dir={dir} defaultMode={defaultMode}>
|
||||
<QueryProvider>
|
||||
<NotistackProvider>
|
||||
{children}
|
||||
</NotistackProvider>
|
||||
</QueryProvider>
|
||||
</ThemeProvider>
|
||||
</AppStoreProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Metadata, Viewport } from 'next';
|
||||
import localFont from 'next/font/local';
|
||||
import { headers } from 'next/headers';
|
||||
import { getThemeMode } from '@/lib/cookies/server';
|
||||
import { getDirection } from '@/theme';
|
||||
import { BRAND } from '@/theme/colors';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import { HEADER_NAMES } from '@/constants';
|
||||
import './globals.css';
|
||||
import '@/theme/tokens.css';
|
||||
|
||||
// FA brand font — Mikhak, a free Persian typeface
|
||||
const mikhak = localFont({
|
||||
src: [
|
||||
{ path: './fonts/Mikhak-Regular.woff2', weight: '400', style: 'normal' },
|
||||
{ path: './fonts/Mikhak-Medium.woff2', weight: '500', style: 'normal' },
|
||||
{ path: './fonts/Mikhak-Bold.woff2', weight: '700', style: 'normal' },
|
||||
],
|
||||
display: 'swap',
|
||||
variable: '--font-mikhak',
|
||||
});
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: BRAND.teal,
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Balinyaar | بالینیار',
|
||||
description: 'Balinyaar web application',
|
||||
manifest: '/site.webmanifest',
|
||||
};
|
||||
|
||||
export default async function RootLayout({ children }: { children: ReactNode }) {
|
||||
|
||||
let locale: string = routing.defaultLocale;
|
||||
try {
|
||||
const hdrs = await headers();
|
||||
const headerLocale = hdrs.get(HEADER_NAMES.LOCALE);
|
||||
if (headerLocale && routing.locales.includes(headerLocale as (typeof routing.locales)[number])) {
|
||||
locale = headerLocale;
|
||||
}
|
||||
} catch {
|
||||
// No request context (build-time prerendering) — use defaultLocale
|
||||
}
|
||||
|
||||
const dir = getDirection(locale);
|
||||
const { colorScheme } = await getThemeMode();
|
||||
|
||||
return (
|
||||
<html
|
||||
lang={locale}
|
||||
dir={dir}
|
||||
className={`${mikhak.variable}`}
|
||||
data-mui-color-scheme={colorScheme}
|
||||
>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
// See: https://github.com/mui-org/material-ui/blob/6b18675c7e6204b77f4c469e113f62ee8be39178/examples/nextjs-with-typescript/src/Link.tsx
|
||||
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||
import { AnchorHTMLAttributes, forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
@@ -15,6 +15,9 @@ export function useIsAuthenticated() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// SSR-safe: read the browser-only cookie once after mount so the server-rendered
|
||||
// `false` reconciles on the client without a hydration mismatch. Deliberate setState.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setIsAuthenticated(Boolean(getClientCookie(COOKIE_NAMES.ACCESS_TOKEN)));
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ function useIsMobileForNextJs() {
|
||||
const [onMobileDelayed, setOnMobileDelayed] = useState(SERVER_SIDE_MOBILE_FIRST);
|
||||
|
||||
useEffect(() => {
|
||||
setOnMobileDelayed(onMobile); // Next.js don't allow to use useOnMobileXxx() directly, so we need to use this workaround
|
||||
// Defer the media-query result to after mount so SSR renders the mobile-first value
|
||||
// and the client reconciles without a hydration mismatch. Deliberate setState.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setOnMobileDelayed(onMobile);
|
||||
}, [onMobile]);
|
||||
|
||||
return onMobileDelayed;
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
'use client';
|
||||
import { FunctionComponent, PropsWithChildren, useEffect } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { LinkToPage } from '@/utils';
|
||||
import { COOKIE_NAMES } from '@/lib/cookies';
|
||||
import { getClientCookie } from '@/lib/cookies/client';
|
||||
import { useAppStore } from '@/store';
|
||||
import TopBarAndSideBarLayout from './TopBarAndSideBarLayout';
|
||||
|
||||
/**
|
||||
* Renders "Private Layout" composition
|
||||
* @layout PrivateLayout
|
||||
*/
|
||||
const PrivateLayout: FunctionComponent<PropsWithChildren> = ({ children }) => {
|
||||
const t = useTranslations('nav');
|
||||
const [, dispatch] = useAppStore();
|
||||
const [,dispatch] = useAppStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (getClientCookie(COOKIE_NAMES.ACCESS_TOKEN)) {
|
||||
@@ -21,16 +13,8 @@ const PrivateLayout: FunctionComponent<PropsWithChildren> = ({ children }) => {
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
const sidebarItems: Array<LinkToPage> = [
|
||||
{ title: t('home'), path: '/', icon: 'home' },
|
||||
{ title: '404', path: '/wrong-url', icon: 'error' },
|
||||
];
|
||||
|
||||
return (
|
||||
<TopBarAndSideBarLayout sidebarItems={sidebarItems} title="Balinyaar" variant="sidebarPersistentOnDesktop">
|
||||
{children}
|
||||
{/* <Stack component="footer">Copyright © </Stack> */}
|
||||
</TopBarAndSideBarLayout>
|
||||
children
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import { SnackbarProvider, MaterialDesignContent } from 'notistack';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { ToastBridge } from './ToastBridge';
|
||||
|
||||
export function NotistackProvider({ children }: { children: ReactNode }) {
|
||||
/*
|
||||
* Toast colors follow the theme rules: every value is a `--bal-*` token from
|
||||
* src/theme/tokens.css (scheme-aware via [data-mui-color-scheme]). Although
|
||||
* notistack renders in a Portal outside the MUI theme tree, the tokens are
|
||||
* defined on <html>, so they cascade into the portal and switch with dark mode
|
||||
* for free — no hard-coded colors here.
|
||||
*
|
||||
* Direction is inherited the same way: <html dir> cascades to the portal, so
|
||||
* toasts are RTL on `fa` without any explicit prop.
|
||||
*/
|
||||
const StyledSnackbarContent = styled(MaterialDesignContent)({
|
||||
'&.notistack-MuiContent-success': {
|
||||
backgroundColor: 'var(--bal-success)',
|
||||
color: 'var(--bal-success-contrast)',
|
||||
},
|
||||
'&.notistack-MuiContent-error': {
|
||||
backgroundColor: 'var(--bal-error)',
|
||||
color: 'var(--bal-error-contrast)',
|
||||
},
|
||||
'&.notistack-MuiContent-warning': {
|
||||
backgroundColor: 'var(--bal-warning)',
|
||||
color: 'var(--bal-warning-contrast)',
|
||||
},
|
||||
'&.notistack-MuiContent-info': {
|
||||
backgroundColor: 'var(--bal-info)',
|
||||
color: 'var(--bal-info-contrast)',
|
||||
},
|
||||
});
|
||||
|
||||
interface NotistackProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function NotistackProvider({ children }: NotistackProviderProps) {
|
||||
return (
|
||||
<SnackbarProvider maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}>
|
||||
<SnackbarProvider
|
||||
maxSnack={3}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
Components={{
|
||||
success: StyledSnackbarContent,
|
||||
error: StyledSnackbarContent,
|
||||
warning: StyledSnackbarContent,
|
||||
info: StyledSnackbarContent,
|
||||
}}
|
||||
>
|
||||
<ToastBridge />
|
||||
{children}
|
||||
</SnackbarProvider>
|
||||
|
||||
@@ -19,31 +19,8 @@ const AppStoreProvider: FunctionComponent<PropsWithChildren> = ({ children }) =>
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to use the AppStore in functional components
|
||||
* @hook useAppStore
|
||||
* import {useAppStore} from './store'
|
||||
* ...
|
||||
* const [state, dispatch] = useAppStore();
|
||||
* OR
|
||||
* const [state] = useAppStore();
|
||||
*/
|
||||
const useAppStore = (): AppContextReturningType => useContext(AppContext);
|
||||
|
||||
/**
|
||||
* HOC to inject the ApStore to class component, also works for functional components
|
||||
* @hok withAppStore
|
||||
* import {withAppStore} from './store'
|
||||
* ...
|
||||
* class MyComponent
|
||||
*
|
||||
* render () {
|
||||
* const [state, dispatch] = this.props.appStore;
|
||||
* ...
|
||||
* }
|
||||
* ...
|
||||
* export default withAppStore(MyComponent)
|
||||
*/
|
||||
interface WithAppStoreProps {
|
||||
appStore: AppContextReturningType;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,16 @@
|
||||
|
||||
/* Divider */
|
||||
--bal-divider: rgba(29, 74, 64, 0.14);
|
||||
|
||||
/* Feedback — toast / alert backgrounds (brand-harmonized; cream text) */
|
||||
--bal-success: #1f6b50;
|
||||
--bal-success-contrast: #f3efe9;
|
||||
--bal-error: #a8392a;
|
||||
--bal-error-contrast: #f3efe9;
|
||||
--bal-warning: #8a6418;
|
||||
--bal-warning-contrast: #f3efe9;
|
||||
--bal-info: #1d4a40;
|
||||
--bal-info-contrast: #f3efe9;
|
||||
}
|
||||
|
||||
/* ── Dark scheme ────────────────────────────────────────────────────────── */
|
||||
@@ -68,4 +78,14 @@
|
||||
|
||||
/* Divider */
|
||||
--bal-divider: rgba(243, 239, 233, 0.14);
|
||||
|
||||
/* Feedback — toast / alert backgrounds (lifted for dark surfaces; cream text) */
|
||||
--bal-success: #257659;
|
||||
--bal-success-contrast: #f3efe9;
|
||||
--bal-error: #b5402f;
|
||||
--bal-error-contrast: #f3efe9;
|
||||
--bal-warning: #97701f;
|
||||
--bal-warning-contrast: #f3efe9;
|
||||
--bal-info: #2f6b5e;
|
||||
--bal-info-contrast: #f3efe9;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { TypographyVariantsOptions } from '@mui/material';
|
||||
|
||||
/** CSS variable injected by next/font/google (Space Grotesk) in src/app/layout.tsx */
|
||||
/** Space Grotesk CSS variable. Not currently wired to a font loader; the LTR
|
||||
* stack falls back to the system fonts until it is added to the locale layout. */
|
||||
export const BRAND_FONT_VARIABLE_EN = '--font-space-grotesk';
|
||||
|
||||
/** CSS variable injected by next/font/local (Mikhak) in src/app/layout.tsx */
|
||||
/** CSS variable injected by next/font/local (Mikhak), attached to <html> only
|
||||
* on `fa` routes in src/app/[locale]/layout.tsx. */
|
||||
export const BRAND_FONT_VARIABLE_FA = '--font-mikhak';
|
||||
|
||||
const SYSTEM_FONT_STACK = [
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
export const IS_SERVER = typeof window === 'undefined';
|
||||
export const IS_BROWSER = typeof window !== 'undefined' && typeof window?.document !== 'undefined';
|
||||
/* eslint-disable no-restricted-globals */
|
||||
export const IS_WEBWORKER =
|
||||
typeof self === 'object' && self.constructor && self.constructor.name === 'DedicatedWorkerGlobalScope';
|
||||
/* eslint-enable no-restricted-globals */
|
||||
|
||||
/**
|
||||
* Returns the value of the environment variable with the given name, raises an error if it is required and not set.
|
||||
|
||||
Reference in New Issue
Block a user