another step just a little remaining

This commit is contained in:
hamid
2026-06-21 00:05:07 +03:30
parent da42f15a32
commit 3fd147cf80
35 changed files with 4620 additions and 4537 deletions
@@ -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>
)
}
+70 -22
View File
@@ -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>
);
}
-60
View File
@@ -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';
+3
View File
@@ -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)));
}, []);
+4 -1
View File
@@ -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;
+2 -18
View File
@@ -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 &copy; </Stack> */}
</TopBarAndSideBarLayout>
children
);
};
+46 -3
View File
@@ -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>
-23
View File
@@ -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;
}
+20
View File
@@ -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;
}
+4 -2
View File
@@ -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 = [
-2
View File
@@ -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.