another step for constructing base project

This commit is contained in:
hamid
2026-06-17 22:53:49 +03:30
parent 5b4c0d183f
commit 5388bea320
76 changed files with 3836 additions and 1961 deletions
-15
View File
@@ -1,15 +0,0 @@
'use client';
import React, { FunctionComponent, PropsWithChildren } from 'react';
import { useIsAuthenticated } from '@/hooks';
import PrivateLayout from './PrivateLayout';
import PublicLayout from './PublicLayout';
/**
* Returns the current Layout component depending on different circumstances.
* @layout CurrentLayout
*/
const CurrentLayout: FunctionComponent<PropsWithChildren> = (props) => {
return useIsAuthenticated() ? <PrivateLayout {...props} /> : <PublicLayout {...props} />;
};
export default CurrentLayout;
+9 -31
View File
@@ -1,45 +1,23 @@
'use client';
import { FunctionComponent, PropsWithChildren } from 'react';
import { useTranslations } from 'next-intl';
import { LinkToPage } from '@/utils';
import TopBarAndSideBarLayout from './TopBarAndSideBarLayout';
const TITLE_PRIVATE = 'Balinyaar'; // Title for pages after authentication
/**
* SideBar navigation items with links for Private Layout
*/
const SIDE_BAR_ITEMS: Array<LinkToPage> = [
{
title: 'Home',
path: '/',
icon: 'home',
},
{
title: 'My Profile',
path: '/me',
icon: 'account',
},
{
title: '404',
path: '/wrong-url',
icon: 'error',
},
{
title: 'About',
path: '/about',
icon: 'info',
},
];
/**
* Renders "Private Layout" composition
* @layout PrivateLayout
*/
const PrivateLayout: FunctionComponent<PropsWithChildren> = ({ children }) => {
const title = TITLE_PRIVATE;
document.title = title; // Also Update Tab Title // TODO: Do we need this? Move it to useEffect()?
const t = useTranslations('nav');
const sidebarItems: Array<LinkToPage> = [
{ title: t('home'), path: '/', icon: 'home' },
{ title: '404', path: '/wrong-url', icon: 'error' },
];
return (
<TopBarAndSideBarLayout sidebarItems={SIDE_BAR_ITEMS} title={title} variant="sidebarPersistentOnDesktop">
<TopBarAndSideBarLayout sidebarItems={sidebarItems} title="Balinyaar" variant="sidebarPersistentOnDesktop">
{children}
{/* <Stack component="footer">Copyright &copy; </Stack> */}
</TopBarAndSideBarLayout>
+2 -35
View File
@@ -11,44 +11,12 @@ const TITLE_PUBLIC = 'Unauthorized - Balinyaar'; // Title for pages without/befo
/**
* SideBar navigation items with links for Public Layout
*/
const SIDE_BAR_ITEMS: Array<LinkToPage> = [
{
title: 'Log In',
path: '/auth/login',
icon: 'login',
},
{
title: 'Sign Up',
path: '/auth/signup',
icon: 'signup',
},
{
title: 'About',
path: '/about',
icon: 'info',
},
];
const SIDE_BAR_ITEMS: Array<LinkToPage> = [];
/**
* BottomBar navigation items with links for Public Layout
*/
const BOTTOM_BAR_ITEMS: Array<LinkToPage> = [
{
title: 'Log In',
path: '/auth/login',
icon: 'login',
},
{
title: 'Sign Up',
path: '/auth/signup',
icon: 'signup',
},
{
title: 'About',
path: '/about',
icon: 'info',
},
];
const BOTTOM_BAR_ITEMS: Array<LinkToPage> = [];
/**
* Renders "Public Layout" composition
@@ -59,7 +27,6 @@ const PublicLayout: FunctionComponent<PropsWithChildren> = ({ children }) => {
const bottomBarVisible = onMobile || BOTTOM_BAR_DESKTOP_VISIBLE;
const title = TITLE_PUBLIC;
document.title = title; // Also Update Tab Title // TODO: Do we need this? Move it to useEffect()?
return (
<TopBarAndSideBarLayout sidebarItems={SIDE_BAR_ITEMS} title={title} variant="sidebarAlwaysTemporary">
+19 -39
View File
@@ -3,11 +3,11 @@ import { FunctionComponent, useMemo, useState } from 'react';
import { Stack, StackProps } from '@mui/material';
import { IS_DEBUG } from '@/config';
import { AppIconButton, ErrorBoundary } from '@/components';
import { useAppStore } from '@/store';
import { LinkToPage } from '@/utils';
import { useEventSwitchDarkMode, useIsMobile } from '@/hooks';
import { useIsMobile } from '@/hooks';
import { TopBar } from './components';
import SideBar, { SideBarProps } from './components/SideBar';
import { DarkModeToggleButton } from './components/DarkModeButton';
import {
SIDE_BAR_DESKTOP_ANCHOR,
SIDE_BAR_MOBILE_ANCHOR,
@@ -23,14 +23,13 @@ interface Props extends StackProps {
}
/**
* Renders "TopBar and SideBar" composition
* Renders "TopBar and SideBar" composition.
* Does NOT subscribe to the color scheme — only <DarkModeToggleButton> does.
* @layout TopBarAndSideBarLayout
*/
const TopBarAndSideBarLayout: FunctionComponent<Props> = ({ children, sidebarItems, title, variant }) => {
const [state] = useAppStore();
const [sidebarVisible, setSidebarVisible] = useState(false); // TODO: Verify is default value is correct
const [sidebarVisible, setSidebarVisible] = useState(false);
const onMobile = useIsMobile();
const onSwitchDarkMode = useEventSwitchDarkMode();
const sidebarProps = useMemo((): Partial<SideBarProps> => {
const anchor = onMobile ? SIDE_BAR_MOBILE_ANCHOR : SIDE_BAR_DESKTOP_ANCHOR;
@@ -53,7 +52,7 @@ const TopBarAndSideBarLayout: FunctionComponent<Props> = ({ children, sidebarIte
const stackStyles = useMemo(
() => ({
minHeight: '100vh', // Full screen height
minHeight: '100vh',
paddingTop: onMobile ? TOP_BAR_MOBILE_HEIGHT : TOP_BAR_DESKTOP_HEIGHT,
paddingLeft:
sidebarProps.variant === 'persistent' && sidebarProps.open && sidebarProps?.anchor?.includes('left')
@@ -67,43 +66,28 @@ const TopBarAndSideBarLayout: FunctionComponent<Props> = ({ children, sidebarIte
[onMobile, sidebarProps]
);
const onSideBarOpen = () => {
if (!sidebarVisible) setSidebarVisible(true); // Don't re-render Layout when SideBar is already open
};
const onSideBarClose = () => {
if (sidebarVisible) setSidebarVisible(false); // Don't re-render Layout when SideBar is already closed
};
const onSideBarOpen = () => { if (!sidebarVisible) setSidebarVisible(true); };
const onSideBarClose = () => { if (sidebarVisible) setSidebarVisible(false); };
const LogoButton = (
<AppIconButton
icon="logo"
title={sidebarProps.open ? undefined : 'Open Sidebar'}
to={sidebarProps.open ? '/' : undefined} // Navigate to Home only when SideBar is closed
onClick={sidebarProps.open ? undefined : onSideBarOpen} // Open SideBar only when it's closed
to={sidebarProps.open ? '/' : undefined}
onClick={sidebarProps.open ? undefined : onSideBarOpen}
/>
);
const DarkModeButton = (
<AppIconButton
icon={state.darkMode ? 'day' : 'night'} // Variant 1
// icon="daynight" // Variant 2
title={state.darkMode ? 'Switch to Light mode' : 'Switch to Dark mode'}
onClick={onSwitchDarkMode}
/>
);
// Note: useMemo() is not needed for startNode, endNode. We need respect store.darkMode and so on.
/*
* DarkModeToggleButton is a self-contained component that subscribes to
* useColorScheme() on its own. This layout component never reads the color
* scheme and therefore never re-renders when the theme switches.
*/
const { startNode, endNode } = sidebarProps?.anchor?.includes('left')
? { startNode: LogoButton, endNode: DarkModeButton }
: { startNode: DarkModeButton, endNode: LogoButton };
? { startNode: LogoButton, endNode: <DarkModeToggleButton /> }
: { startNode: <DarkModeToggleButton />, endNode: LogoButton };
IS_DEBUG &&
console.log('Render <TopbarAndSidebarLayout/>', {
onMobile,
darkMode: state.darkMode,
sidebarProps,
});
IS_DEBUG && console.log('Render <TopbarAndSidebarLayout/>', { onMobile, sidebarProps });
return (
<Stack sx={stackStyles}>
@@ -114,11 +98,7 @@ const TopBarAndSideBarLayout: FunctionComponent<Props> = ({ children, sidebarIte
<Stack
component="main"
flexGrow={1} // Takes all possible space
justifyContent="space-between" // Push children content (Footer, StatusBar, etc.) to the bottom
paddingLeft={1}
paddingRight={1}
paddingTop={1}
sx={{ flexGrow: 1, justifyContent: 'space-between', paddingLeft: 1, paddingRight: 1, paddingTop: 1 }}
>
<ErrorBoundary name="Content">{children}</ErrorBoundary>
</Stack>
@@ -0,0 +1,42 @@
'use client';
/*
* Tiny components that are the ONLY React nodes subscribed to useColorScheme().
* When the user flips the theme:
* 1. useColorScheme().setMode() sets data-mui-color-scheme on <html>
* 2. CSS custom properties resolve to new values → browser repaints
* 3. React re-renders ONLY these two components (icon label / switch state)
* Nothing above them in the tree is touched.
*/
import { FormControlLabel, Switch, Tooltip } from '@mui/material';
import { useColorScheme } from '@mui/material/styles';
import { useTranslations } from 'next-intl';
import { AppIconButton } from '@/components';
/** Icon button for the TopBar dark-mode toggle. */
export function DarkModeToggleButton() {
const { colorScheme, setMode } = useColorScheme();
const t = useTranslations('common');
const isDark = colorScheme === 'dark';
return (
<AppIconButton
icon={isDark ? 'day' : 'night'}
title={isDark ? t('light_mode') : t('dark_mode')}
onClick={() => setMode(isDark ? 'light' : 'dark')}
/>
);
}
/** Labeled switch for the SideBar dark-mode toggle. */
export function DarkModeFormSwitch() {
const { colorScheme, setMode } = useColorScheme();
const t = useTranslations('common');
const isDark = colorScheme === 'dark';
return (
<Tooltip title={isDark ? t('light_mode') : t('dark_mode')}>
<FormControlLabel
label={isDark ? t('dark_mode') : t('light_mode')}
control={<Switch checked={isDark} onChange={() => setMode(isDark ? 'light' : 'dark')} />}
/>
</Tooltip>
);
}
+14 -29
View File
@@ -1,11 +1,11 @@
import { FunctionComponent, useCallback, MouseEvent } from 'react';
import { Stack, Divider, Drawer, DrawerProps, FormControlLabel, Switch, Tooltip } from '@mui/material';
import { useAppStore } from '@/store';
import { Stack, Divider, Drawer, DrawerProps } from '@mui/material';
import { LinkToPage } from '@/utils';
import { useEventLogout, useEventSwitchDarkMode, useIsAuthenticated, useIsMobile } from '@/hooks';
import { useEventLogout, useIsAuthenticated, useIsMobile } from '@/hooks';
import { AppIconButton, UserInfo } from '@/components';
import { SIDE_BAR_WIDTH, TOP_BAR_DESKTOP_HEIGHT } from '../config';
import SideBarNavList from './SideBarNavList';
import { DarkModeFormSwitch } from './DarkModeButton';
export interface SideBarProps extends Pick<DrawerProps, 'anchor' | 'className' | 'open' | 'variant' | 'onClose'> {
items: Array<LinkToPage>;
@@ -13,20 +13,11 @@ export interface SideBarProps extends Pick<DrawerProps, 'anchor' | 'className' |
/**
* Renders SideBar with Menu and User details
* Actually for Authenticated users only, rendered in "Private Layout"
* @component SideBar
* @param {string} anchor - 'left' or 'right'
* @param {boolean} open - the Drawer is visible when true
* @param {string} variant - variant of the Drawer, one of 'permanent', 'persistent', 'temporary'
* @param {function} onClose - called when the Drawer is closing
*/
const SideBar: FunctionComponent<SideBarProps> = ({ anchor, open, variant, items, onClose, ...restOfProps }) => {
const [state] = useAppStore();
// const isAuthenticated = state.isAuthenticated; // Variant 1
const isAuthenticated = useIsAuthenticated(); // Variant 2
const isAuthenticated = useIsAuthenticated();
const onMobile = useIsMobile();
const onSwitchDarkMode = useEventSwitchDarkMode();
const onLogout = useEventLogout();
const handleAfterLinkClick = useCallback(
@@ -43,20 +34,19 @@ const SideBar: FunctionComponent<SideBarProps> = ({ anchor, open, variant, items
anchor={anchor}
open={open}
variant={variant}
PaperProps={{
sx: {
width: SIDE_BAR_WIDTH,
marginTop: onMobile ? 0 : variant === 'temporary' ? 0 : TOP_BAR_DESKTOP_HEIGHT,
height: onMobile ? '100%' : variant === 'temporary' ? '100%' : `calc(100% - ${TOP_BAR_DESKTOP_HEIGHT})`,
slotProps={{
paper: {
sx: {
width: SIDE_BAR_WIDTH,
marginTop: onMobile ? 0 : variant === 'temporary' ? 0 : TOP_BAR_DESKTOP_HEIGHT,
height: onMobile ? '100%' : variant === 'temporary' ? '100%' : `calc(100% - ${TOP_BAR_DESKTOP_HEIGHT})`,
},
},
}}
onClose={onClose}
>
<Stack
sx={{
height: '100%',
padding: 2,
}}
sx={{ height: '100%', padding: 2 }}
{...restOfProps}
onClick={handleAfterLinkClick}
>
@@ -73,19 +63,14 @@ const SideBar: FunctionComponent<SideBarProps> = ({ anchor, open, variant, items
<Stack
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-evenly',
alignItems: 'center',
marginTop: 2,
}}
>
<Tooltip title={state.darkMode ? 'Switch to Light mode' : 'Switch to Dark mode'}>
<FormControlLabel
label={!state.darkMode ? 'Light mode' : 'Dark mode'}
control={<Switch checked={state.darkMode} onChange={onSwitchDarkMode} />}
/>
</Tooltip>
{/* Only DarkModeFormSwitch subscribes to useColorScheme — it's the sole re-render target */}
<DarkModeFormSwitch />
{isAuthenticated && <AppIconButton icon="logout" title="Logout Current User" onClick={onLogout} />}
</Stack>
-2
View File
@@ -1,6 +1,4 @@
import CurrentLayout from './CurrentLayout';
import PrivateLayout from './PrivateLayout';
import PublicLayout from './PublicLayout';
export { PublicLayout, PrivateLayout };
export default CurrentLayout;