another step just a little remaining
This commit is contained in:
@@ -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