This commit is contained in:
hamid
2026-06-16 01:32:43 +03:30
commit 69bbd28bb0
298 changed files with 24728 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
'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;
+49
View File
@@ -0,0 +1,49 @@
import { FunctionComponent, PropsWithChildren } from 'react';
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()?
return (
<TopBarAndSideBarLayout sidebarItems={SIDE_BAR_ITEMS} title={title} variant="sidebarPersistentOnDesktop">
{children}
{/* <Stack component="footer">Copyright &copy; </Stack> */}
</TopBarAndSideBarLayout>
);
};
export default PrivateLayout;
+72
View File
@@ -0,0 +1,72 @@
import { FunctionComponent, PropsWithChildren } from 'react';
import { Stack } from '@mui/material';
import { LinkToPage } from '@/utils';
import { useIsMobile } from '@/hooks';
import { BottomBar } from './components';
import TopBarAndSideBarLayout from './TopBarAndSideBarLayout';
import { BOTTOM_BAR_DESKTOP_VISIBLE } from './config';
const TITLE_PUBLIC = 'Unauthorized - Balinyaar'; // Title for pages without/before authentication
/**
* 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',
},
];
/**
* 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',
},
];
/**
* Renders "Public Layout" composition
* @layout PublicLayout
*/
const PublicLayout: FunctionComponent<PropsWithChildren> = ({ children }) => {
const onMobile = useIsMobile();
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">
{children}
<Stack component="footer">{bottomBarVisible && <BottomBar items={BOTTOM_BAR_ITEMS} />}</Stack>
</TopBarAndSideBarLayout>
);
};
export default PublicLayout;
@@ -0,0 +1,129 @@
'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 { useAppStore } from '@/store';
import { LinkToPage } from '@/utils';
import { useEventSwitchDarkMode, useIsMobile } from '@/hooks';
import { TopBar } from './components';
import SideBar, { SideBarProps } from './components/SideBar';
import {
SIDE_BAR_DESKTOP_ANCHOR,
SIDE_BAR_MOBILE_ANCHOR,
SIDE_BAR_WIDTH,
TOP_BAR_DESKTOP_HEIGHT,
TOP_BAR_MOBILE_HEIGHT,
} from './config';
interface Props extends StackProps {
sidebarItems: Array<LinkToPage>;
title: string;
variant: 'sidebarAlwaysTemporary' | 'sidebarPersistentOnDesktop' | 'sidebarAlwaysPersistent';
}
/**
* Renders "TopBar and SideBar" composition
* @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 onMobile = useIsMobile();
const onSwitchDarkMode = useEventSwitchDarkMode();
const sidebarProps = useMemo((): Partial<SideBarProps> => {
const anchor = onMobile ? SIDE_BAR_MOBILE_ANCHOR : SIDE_BAR_DESKTOP_ANCHOR;
let open = sidebarVisible;
let sidebarVariant: SideBarProps['variant'] = 'temporary';
switch (variant) {
case 'sidebarAlwaysTemporary':
break;
case 'sidebarPersistentOnDesktop':
open = onMobile ? sidebarVisible : true;
sidebarVariant = onMobile ? 'temporary' : 'persistent';
break;
case 'sidebarAlwaysPersistent':
open = true;
sidebarVariant = 'persistent';
break;
}
return { anchor, open, variant: sidebarVariant };
}, [onMobile, sidebarVisible, variant]);
const stackStyles = useMemo(
() => ({
minHeight: '100vh', // Full screen height
paddingTop: onMobile ? TOP_BAR_MOBILE_HEIGHT : TOP_BAR_DESKTOP_HEIGHT,
paddingLeft:
sidebarProps.variant === 'persistent' && sidebarProps.open && sidebarProps?.anchor?.includes('left')
? SIDE_BAR_WIDTH
: undefined,
paddingRight:
sidebarProps.variant === 'persistent' && sidebarProps.open && sidebarProps?.anchor?.includes('right')
? SIDE_BAR_WIDTH
: undefined,
}),
[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 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
/>
);
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.
const { startNode, endNode } = sidebarProps?.anchor?.includes('left')
? { startNode: LogoButton, endNode: DarkModeButton }
: { startNode: DarkModeButton, endNode: LogoButton };
IS_DEBUG &&
console.log('Render <TopbarAndSidebarLayout/>', {
onMobile,
darkMode: state.darkMode,
sidebarProps,
});
return (
<Stack sx={stackStyles}>
<Stack component="header">
<TopBar startNode={startNode} title={title} endNode={endNode} />
<SideBar items={sidebarItems} onClose={onSideBarClose} {...sidebarProps} />
</Stack>
<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}
>
<ErrorBoundary name="Content">{children}</ErrorBoundary>
</Stack>
</Stack>
);
};
export default TopBarAndSideBarLayout;
@@ -0,0 +1,39 @@
'use client';
import { FunctionComponent, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { BottomNavigation, BottomNavigationAction } from '@mui/material';
import { LinkToPage } from '@/utils';
import { AppIcon } from '@/components';
interface Props {
items: Array<LinkToPage>;
}
/**
* Renders horizontal Navigation Bar using MUI BottomNavigation component
* @component BottomBar
*/
const BottomBar: FunctionComponent<Props> = ({ items }) => {
const router = useRouter();
const onNavigationChange = useCallback(
(_event: unknown, newValue: string) => {
router.push(newValue);
},
[router]
);
return (
<BottomNavigation
value={location.pathname} // Automatically highlights bottom navigation for current page
showLabels // Always show labels on bottom navigation, otherwise label visible only for active page
onChange={onNavigationChange}
>
{items.map(({ title, path, icon }) => (
<BottomNavigationAction key={`${title}-${path}`} label={title} value={path} icon={<AppIcon icon={icon} />} />
))}
</BottomNavigation>
);
};
export default BottomBar;
+97
View File
@@ -0,0 +1,97 @@
import { FunctionComponent, useCallback, MouseEvent } from 'react';
import { Stack, Divider, Drawer, DrawerProps, FormControlLabel, Switch, Tooltip } from '@mui/material';
import { useAppStore } from '@/store';
import { LinkToPage } from '@/utils';
import { useEventLogout, useEventSwitchDarkMode, useIsAuthenticated, useIsMobile } from '@/hooks';
import { AppIconButton, UserInfo } from '@/components';
import { SIDE_BAR_WIDTH, TOP_BAR_DESKTOP_HEIGHT } from '../config';
import SideBarNavList from './SideBarNavList';
export interface SideBarProps extends Pick<DrawerProps, 'anchor' | 'className' | 'open' | 'variant' | 'onClose'> {
items: Array<LinkToPage>;
}
/**
* 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 onMobile = useIsMobile();
const onSwitchDarkMode = useEventSwitchDarkMode();
const onLogout = useEventLogout();
const handleAfterLinkClick = useCallback(
(event: MouseEvent) => {
if (variant === 'temporary' && typeof onClose === 'function') {
onClose(event, 'backdropClick');
}
},
[variant, onClose]
);
return (
<Drawer
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})`,
},
}}
onClose={onClose}
>
<Stack
sx={{
height: '100%',
padding: 2,
}}
{...restOfProps}
onClick={handleAfterLinkClick}
>
{isAuthenticated && (
<>
<UserInfo showAvatar />
<Divider />
</>
)}
<SideBarNavList items={items} showIcons />
<Divider />
<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>
{isAuthenticated && <AppIconButton icon="logout" title="Logout Current User" onClick={onLogout} />}
</Stack>
</Stack>
</Drawer>
);
};
export default SideBar;
@@ -0,0 +1,45 @@
'use client';
import { FunctionComponent, MouseEventHandler } from 'react';
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { AppIcon, AppLink } from '@/components';
import { LinkToPage } from '@/utils';
import { usePathname } from 'next/navigation';
interface Props extends LinkToPage {
openInNewTab?: boolean;
selected?: boolean;
onClick?: MouseEventHandler;
}
/**
* Renders Navigation Item for SideBar, detects current url and sets selected state if needed
* @component SideBarNavItem
*/
const SideBarNavItem: FunctionComponent<Props> = ({
openInNewTab,
icon,
path,
selected: propSelected = false,
subtitle,
title,
onClick,
}) => {
const pathname = usePathname();
const selected = propSelected || (path && path.length > 1 && pathname.startsWith(path)) || false;
return (
<ListItemButton
component={AppLink}
selected={selected}
to={path}
href="" // Hard reset for .href property, otherwise links are always opened in new tab :(
openInNewTab={openInNewTab}
onClick={onClick}
>
<ListItemIcon>{icon && <AppIcon icon={icon} />}</ListItemIcon>
<ListItemText primary={title} secondary={subtitle} />
</ListItemButton>
);
};
export default SideBarNavItem;
@@ -0,0 +1,35 @@
import { FunctionComponent, MouseEventHandler } from 'react';
import List from '@mui/material/List';
import { LinkToPage } from '@/utils';
import SideBarNavItem from './SideBarNavItem';
interface Props {
items: Array<LinkToPage>;
showIcons?: boolean;
onClick?: MouseEventHandler;
}
/**
* Renders list of Navigation Items inside SideBar
* @component SideBarNavList
* @param {array} items - list of objects to render as navigation items
* @param {boolean} [showIcons] - icons in navigation items are visible when true
* @param {function} [onAfterLinkClick] - optional callback called when some navigation item was clicked
*/
const SideBarNavList: FunctionComponent<Props> = ({ items, showIcons, onClick, ...restOfProps }) => {
return (
<List component="nav" {...restOfProps}>
{items.map(({ icon, path, title }) => (
<SideBarNavItem
key={`${title}-${path}`}
icon={showIcons ? icon : undefined}
path={path}
title={title}
onClick={onClick}
/>
))}
</List>
);
};
export default SideBarNavList;
+46
View File
@@ -0,0 +1,46 @@
import { FunctionComponent, ReactNode } from 'react';
import { AppBar, Toolbar, Typography } from '@mui/material';
interface Props {
endNode?: ReactNode;
startNode?: ReactNode;
title?: string;
}
/**
* Renders TopBar composition
* @component TopBar
*/
const TopBar: FunctionComponent<Props> = ({ endNode, startNode, title = '', ...restOfProps }) => {
return (
<AppBar
component="div"
sx={
{
// boxShadow: 'none', // Uncomment to hide shadow
}
}
{...restOfProps}
>
<Toolbar disableGutters sx={{ paddingX: 1 }}>
{startNode}
<Typography
variant="h6"
sx={{
marginX: 1,
flexGrow: 1,
textAlign: 'center',
whiteSpace: 'nowrap',
}}
>
{title}
</Typography>
{endNode}
</Toolbar>
</AppBar>
);
};
export default TopBar;
+5
View File
@@ -0,0 +1,5 @@
import BottomBar from './BottomBar';
import SideBar from './SideBar';
import TopBar from './TopBar';
export { BottomBar, SideBar, TopBar };
+21
View File
@@ -0,0 +1,21 @@
/**
* Layout configuration
*/
/**
* SideBar configuration
*/
export const SIDE_BAR_MOBILE_ANCHOR = 'right'; // 'right';
export const SIDE_BAR_DESKTOP_ANCHOR = 'left'; // 'right';
export const SIDE_BAR_WIDTH = '240px';
/**
* TopBar configuration
*/
export const TOP_BAR_MOBILE_HEIGHT = '56px';
export const TOP_BAR_DESKTOP_HEIGHT = '64px';
/**
* BottomBar configuration
*/
export const BOTTOM_BAR_DESKTOP_VISIBLE = false; // true;
+6
View File
@@ -0,0 +1,6 @@
import CurrentLayout from './CurrentLayout';
import PrivateLayout from './PrivateLayout';
import PublicLayout from './PublicLayout';
export { PublicLayout, PrivateLayout };
export default CurrentLayout;