diff --git a/client/.env.development b/client/.env.development
new file mode 100644
index 0000000..0db0a4c
--- /dev/null
+++ b/client/.env.development
@@ -0,0 +1,22 @@
+# Environment similar to NODE_ENV.
+# Analytics and public resources are enabled only in "production"
+# How to use: set value to "production" to get fully functional application.
+NEXT_PUBLIC_ENV = development
+# NEXT_PUBLIC_ENV = preview
+# NEXT_PUBLIC_ENV = production
+
+# Enables additional debug features, no additional debug information if the variable is not set
+# How to use: set value to "true" to get more debugging information, but don't do it on production.
+NEXT_PUBLIC_DEBUG = true
+
+# Public URL of the application/website.
+# How to use: Do not set any value until you need custom domain for your application.
+# NEXT_PUBLIC_PUBLIC_URL = https://xxx.com
+# NEXT_PUBLIC_PUBLIC_URL = https://xxx.web.app
+NEXT_PUBLIC_PUBLIC_URL = http://localhost:3000
+
+
+# API/Backend basic URL (the Baya server)
+NEXT_PUBLIC_API_URL = https://localhost:5002
+# NEXT_PUBLIC_API_URL = https://dev-api.domain.com
+# NEXT_PUBLIC_API_URL = https://api.domain.com
\ No newline at end of file
diff --git a/client/CLAUDE.md b/client/CLAUDE.md
index e55bdde..170264e 100644
--- a/client/CLAUDE.md
+++ b/client/CLAUDE.md
@@ -81,7 +81,7 @@ client/
## Server / Client Component Boundaries
**Root layout** (`src/app/layout.tsx`) is a **lean Server Component** (RSC). It:
-- Reads locale from the **`x-next-intl-locale` request header** set by the middleware (with fallback to `defaultLocale` for build-time pre-rendering where no request context exists).
+- 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 `` 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.
diff --git a/client/middleware.ts b/client/middleware.ts
index 1c7aa14..d1f67a7 100644
--- a/client/middleware.ts
+++ b/client/middleware.ts
@@ -2,7 +2,7 @@ import createMiddleware from 'next-intl/middleware';
import { type NextRequest, NextResponse } from 'next/server';
import { routing } from './src/i18n/routing';
import { COOKIE_NAMES } from './src/lib/cookies';
-import { PUBLIC_PATHS, ROUTES } from './src/constants';
+import { HEADER_NAMES, PUBLIC_PATHS, ROUTES } from './src/constants';
const intlMiddleware = createMiddleware(routing);
@@ -39,7 +39,22 @@ export default function middleware(request: NextRequest) {
}
}
- return i18nResponse;
+ // Detect locale from the normalized URL (next-intl always puts it at position 1)
+ const locale = routing.locales.find(
+ (l) => pathname === `/${l}` || pathname.startsWith(`/${l}/`),
+ ) ?? routing.defaultLocale;
+
+ const requestHeaders = new Headers(request.headers);
+ requestHeaders.set(HEADER_NAMES.LOCALE, locale);
+
+ const response = NextResponse.next({ request: { headers: requestHeaders } });
+
+ // Preserve response headers set by next-intl (e.g. Link: alternate hreflang)
+ i18nResponse.headers.forEach((value, key) => {
+ response.headers.set(key, value);
+ });
+
+ return response;
}
export const config = {
diff --git a/client/src/app/[locale]/layout.tsx b/client/src/app/[locale]/layout.tsx
index 0aed5f1..d15b049 100644
--- a/client/src/app/[locale]/layout.tsx
+++ b/client/src/app/[locale]/layout.tsx
@@ -1,11 +1,10 @@
import type { ReactNode } from 'react';
import { setRequestLocale, getMessages } from 'next-intl/server';
import { NextIntlClientProvider } from 'next-intl';
-import { SnackbarProvider } from 'notistack';
import { getThemeMode } from '@/lib/cookies/server';
import { AppStoreProvider } from '@/store';
import { ThemeProvider, getDirection } from '@/theme';
-import { ToastBridge } from '@/lib/toast';
+import { NotistackProvider } from '@/lib/toast';
import { QueryProvider } from '@/lib/query/QueryProvider';
import { routing } from '@/i18n/routing';
@@ -46,10 +45,9 @@ export default async function LocaleLayout({
-
-
+
{children}
-
+
diff --git a/client/src/app/globals.css b/client/src/app/globals.css
index 55ce595..30b6834 100644
--- a/client/src/app/globals.css
+++ b/client/src/app/globals.css
@@ -8,7 +8,6 @@ html,
body {
max-width: 100vw;
overflow-x: hidden;
- /* required for sticky elements: HeaderMobile, and so on */
max-height: 100vh;
}
diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx
index f5808bf..06c42e6 100644
--- a/client/src/app/layout.tsx
+++ b/client/src/app/layout.tsx
@@ -1,23 +1,15 @@
import type { ReactNode } from 'react';
import { Metadata, Viewport } from 'next';
-import { Space_Grotesk } from 'next/font/google';
import localFont from 'next/font/local';
import { headers } from 'next/headers';
import { getThemeMode } from '@/lib/cookies/server';
-import { ColorSchemeScript, getDirection } from '@/theme';
+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';
-// EN brand font — loaded for all locales so the CSS variable is always defined
-const spaceGrotesk = Space_Grotesk({
- subsets: ['latin'],
- weight: ['400', '500', '600', '700'],
- display: 'swap',
- variable: '--font-space-grotesk',
-});
-
// FA brand font — Mikhak, a free Persian typeface
const mikhak = localFont({
src: [
@@ -40,23 +32,11 @@ export const metadata: Metadata = {
};
export default async function RootLayout({ children }: { children: ReactNode }) {
- /*
- * The root layout is intentionally kept as a lean HTML shell.
- * NextIntlClientProvider, ThemeProvider, and AppStoreProvider live in
- * [locale]/layout.tsx so they always receive the correct locale from URL
- * params — independent of whether this root layout is rendered statically
- * or dynamically.
- *
- * We still read x-next-intl-locale here for lang/dir on so that
- * screen readers and CSS direction are correct on real requests.
- * During build-time pre-rendering (no request context) we fall back to
- * defaultLocale — this only affects the static HTML shell; the live
- * request always re-renders with the correct locale header.
- */
+
let locale: string = routing.defaultLocale;
try {
const hdrs = await headers();
- const headerLocale = hdrs.get('x-next-intl-locale');
+ const headerLocale = hdrs.get(HEADER_NAMES.LOCALE);
if (headerLocale && routing.locales.includes(headerLocale as (typeof routing.locales)[number])) {
locale = headerLocale;
}
@@ -71,13 +51,9 @@ export default async function RootLayout({ children }: { children: ReactNode })
-
-
-
{children}
);
diff --git a/client/src/constants/headers.ts b/client/src/constants/headers.ts
new file mode 100644
index 0000000..eedb91d
--- /dev/null
+++ b/client/src/constants/headers.ts
@@ -0,0 +1,3 @@
+export const HEADER_NAMES = {
+ LOCALE: 'x-app-locale',
+} as const;
diff --git a/client/src/constants/index.ts b/client/src/constants/index.ts
index a382098..5e201ac 100644
--- a/client/src/constants/index.ts
+++ b/client/src/constants/index.ts
@@ -1 +1,2 @@
+export * from './headers';
export * from './routes';
diff --git a/client/src/layout/TopBarAndSideBarLayout.tsx b/client/src/layout/TopBarAndSideBarLayout.tsx
index 148ecea..9b5a212 100644
--- a/client/src/layout/TopBarAndSideBarLayout.tsx
+++ b/client/src/layout/TopBarAndSideBarLayout.tsx
@@ -1,7 +1,6 @@
'use client';
import { FunctionComponent, useMemo, useState } from 'react';
import { Stack, StackProps } from '@mui/material';
-import { IS_DEBUG } from '@/config';
import { AppIconButton, ErrorBoundary } from '@/components';
import { LinkToPage } from '@/utils';
import { useIsMobile } from '@/hooks';
@@ -22,11 +21,6 @@ interface Props extends StackProps {
variant: 'sidebarAlwaysTemporary' | 'sidebarPersistentOnDesktop' | 'sidebarAlwaysPersistent';
}
-/**
- * Renders "TopBar and SideBar" composition.
- * Does NOT subscribe to the color scheme — only does.
- * @layout TopBarAndSideBarLayout
- */
const TopBarAndSideBarLayout: FunctionComponent = ({ children, sidebarItems, title, variant }) => {
const [sidebarVisible, setSidebarVisible] = useState(false);
const onMobile = useIsMobile();
@@ -87,8 +81,6 @@ const TopBarAndSideBarLayout: FunctionComponent = ({ children, sidebarIte
? { startNode: LogoButton, endNode: }
: { startNode: , endNode: LogoButton };
- IS_DEBUG && console.log('Render ', { onMobile, sidebarProps });
-
return (
diff --git a/client/src/lib/api/server.ts b/client/src/lib/api/server.ts
index a72b23e..35f6592 100644
--- a/client/src/lib/api/server.ts
+++ b/client/src/lib/api/server.ts
@@ -9,6 +9,7 @@ import { headers } from 'next/headers';
import { API_URL } from '@/config';
import { COOKIE_NAMES } from '@/lib/cookies';
+import { HEADER_NAMES } from '@/constants';
import { getServerCookie } from '@/lib/cookies/server';
import { ApiError } from './errors';
@@ -26,7 +27,7 @@ export async function serverFetch(path: string, options?: RequestInit): Promi
let locale = 'fa';
try {
const reqHeaders = await headers();
- locale = reqHeaders.get('x-next-intl-locale') ?? 'fa';
+ locale = reqHeaders.get(HEADER_NAMES.LOCALE) ?? 'fa';
} catch {
// Outside request context (build-time prerendering) — use default locale
}
diff --git a/client/src/lib/toast/NotistackProvider.tsx b/client/src/lib/toast/NotistackProvider.tsx
new file mode 100644
index 0000000..227219f
--- /dev/null
+++ b/client/src/lib/toast/NotistackProvider.tsx
@@ -0,0 +1,14 @@
+'use client';
+
+import type { ReactNode } from 'react';
+import { SnackbarProvider } from 'notistack';
+import { ToastBridge } from './ToastBridge';
+
+export function NotistackProvider({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/client/src/lib/toast/index.ts b/client/src/lib/toast/index.ts
index 4a48064..cd930ac 100644
--- a/client/src/lib/toast/index.ts
+++ b/client/src/lib/toast/index.ts
@@ -1,2 +1,3 @@
export { ToastBridge } from './ToastBridge';
+export { NotistackProvider } from './NotistackProvider';
export { dispatchToast, type ToastSeverity } from './dispatchToast';
diff --git a/client/src/theme/ColorSchemeScript.tsx b/client/src/theme/ColorSchemeScript.tsx
deleted file mode 100644
index 370da64..0000000
--- a/client/src/theme/ColorSchemeScript.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { COOKIE_NAMES } from '@/lib/cookies';
-
-/**
- * Inline synchronous script placed in — runs before any paint.
- *
- * Two jobs:
- * 1. Set data-mui-color-scheme on from our cookie, so the server-set
- * attribute and the first-paint attribute always agree (no flash).
- * 2. Patch Storage.prototype so MUI v9's internal localStorage reads for key
- * 'mode' return null (forcing MUI to use the defaultMode prop we derive
- * from the cookie on the server) and writes to 'mode' are routed to our
- * cookie instead of localStorage.
- *
- * Why patch Storage.prototype instead of storageWindow={null}?
- * In MUI v9 localStorageManager: `if (!storageWindow && typeof window !== 'undefined') {
- * storageWindow = window; }` — null is falsy, so storageWindow={null} is silently
- * overridden to window in the browser. The prototype patch is the only reliable way.
- */
-export function ColorSchemeScript() {
- const cookieName = COOKIE_NAMES.COLOR_SCHEME; // 'color-scheme'
- const maxAge = 60 * 60 * 24 * 365;
- // MUI v9 default modeStorageKey (InitColorSchemeScript.mjs: DEFAULT_MODE_STORAGE_KEY = 'mode')
- const muiModeKey = 'mode';
-
- const script =
- `(function(){` +
- // ── 1. Set attribute from cookie before first paint ─────────────────────
- `var m=document.cookie.match(/(^|;)\\s*${cookieName}=([^;]*)/);` +
- `var s=m?decodeURIComponent(m[2]):(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light');` +
- `document.documentElement.setAttribute('data-mui-color-scheme',s==='dark'?'dark':'light');` +
- // ── 2. Intercept Storage so MUI never touches localStorage ───────────────
- `try{` +
- `var _s=Storage.prototype.setItem,_g=Storage.prototype.getItem,_r=Storage.prototype.removeItem;` +
- `Storage.prototype.setItem=function(k,v){` +
- `if(this===window.localStorage&&k==='${muiModeKey}'){` +
- `if(v==='dark'||v==='light')document.cookie='${cookieName}='+v+';path=/;max-age=${maxAge};samesite=lax';` +
- `else document.cookie='${cookieName}=;path=/;max-age=0';` +
- `return;}` +
- `_s.call(this,k,v);};` +
- `Storage.prototype.getItem=function(k){` +
- `if(this===window.localStorage&&k==='${muiModeKey}')return null;` +
- `return _g.call(this,k);};` +
- `Storage.prototype.removeItem=function(k){` +
- `if(this===window.localStorage&&k==='${muiModeKey}'){` +
- `document.cookie='${cookieName}=;path=/;max-age=0';return;}` +
- `_r.call(this,k);};` +
- `}catch(e){}` +
- `})();`;
-
- // eslint-disable-next-line react/no-danger
- return ;
-}
diff --git a/client/src/theme/index.ts b/client/src/theme/index.ts
index 2a5514d..ef58670 100644
--- a/client/src/theme/index.ts
+++ b/client/src/theme/index.ts
@@ -3,7 +3,7 @@ import { LIGHT_THEME } from './light';
import { DARK_THEME } from './dark';
import APP_THEME, { APP_THEME_LTR, APP_THEME_RTL } from './theme';
import { getDirection } from './direction';
-import { ColorSchemeScript } from './ColorSchemeScript';
+
export {
APP_THEME,
@@ -14,5 +14,4 @@ export {
LIGHT_THEME as default,
AppThemeProvider as ThemeProvider,
getDirection,
- ColorSchemeScript,
};