First Commit

This commit is contained in:
cesnimda
2026-03-21 11:55:27 +01:00
commit 2e8a29b4d0
1757 changed files with 166084 additions and 0 deletions
@@ -0,0 +1,67 @@
import PropTypes from 'prop-types';
// @mui
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
// @project
import LogoSection from '@/components/logo';
import MainCard from '@/components/MainCard';
import RouterLink from '@/components/Link';
import { AvatarSize } from '@/enum';
// @assets
import { IconBolt } from '@tabler/icons-react';
/*************************** NAVIGATION CARD - DATA ***************************/
const data = {
title: 'Upgrade Your Experience',
description: 'Take your experience to the next level with our premium offering. Buy now and enjoy more!',
icon: <IconBolt size={16} />
};
/*************************** NAVIGATION CARD - CONTENT ***************************/
function CardContent({ title, description, icon }) {
return (
<Stack sx={{ gap: 3 }}>
<Stack direction="row" sx={{ gap: 0.25, alignItems: 'center' }}>
<Avatar variant="rounded" size={AvatarSize.XS} sx={{ bgcolor: 'transparent' }}>
<LogoSection isIcon sx={{ '& .MuiBox-root': { width: 'auto', height: 'auto' } }} />
</Avatar>
<Typography variant="body2">{import.meta.env.VITE_APP_VERSION}</Typography>
</Stack>
<Stack sx={{ gap: 1, alignItems: 'flex-start', textWrap: 'wrap' }}>
<Typography variant="subtitle1">{title}</Typography>
<Typography variant="caption" color="text.secondary">
{description}
</Typography>
<Button
startIcon={icon}
variant="contained"
component={RouterLink}
to={import.meta.env.VITE_APP_BUY_URL}
target="_blank"
sx={{ mt: 0.5 }}
>
Buy Now
</Button>
</Stack>
</Stack>
);
}
/*************************** DRAWER CONTENT - NAVIGATION CARD ***************************/
export default function NavCard() {
return (
<MainCard sx={{ p: 1.5, bgcolor: 'grey.50', boxShadow: 'none', mb: 3 }}>
<CardContent title={data.title} description={data.description} icon={data.icon} />
</MainCard>
);
}
CardContent.propTypes = { title: PropTypes.string, description: PropTypes.string, icon: PropTypes.any };
@@ -0,0 +1,104 @@
import PropTypes from 'prop-types';
import { Activity, useState } from 'react';
// @mui
import { useTheme } from '@mui/material/styles';
import Collapse from '@mui/material/Collapse';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Typography from '@mui/material/Typography';
// @project
import NavItem from './NavItem';
import DynamicIcon from '@/components/DynamicIcon';
import useMenuCollapse from '@/hooks/useMenuCollapse';
import { usePathname } from '@/utils/navigation';
// @assets
import { IconChevronDown, IconChevronUp } from '@tabler/icons-react';
// @style
const verticalDivider = {
'&:after': {
content: "''",
position: 'absolute',
left: 16,
top: -2,
height: `calc(100% + 2px)`,
width: '1px',
opacity: 1,
bgcolor: 'divider'
}
};
/*************************** COLLAPSE - LOOP ***************************/
function NavCollapseLoop({ item }) {
return item.children?.map((item) => {
switch (item.type) {
case 'collapse':
return <NavCollapse key={item.id} item={item} level={1} />;
case 'item':
return <NavItem key={item.id} item={item} level={1} />;
default:
return (
<Typography key={item.id} variant="h6" color="error" align="center">
Fix - Collapse or Item
</Typography>
);
}
});
}
/*************************** RESPONSIVE DRAWER - COLLAPSE ***************************/
export default function NavCollapse({ item, level = 0 }) {
const theme = useTheme();
const [open, setOpen] = useState(false);
const [selected, setSelected] = useState(null);
// Active item collapse on page load with sub-levels
const pathname = usePathname();
useMenuCollapse(item, pathname, false, setSelected, setOpen);
const handleClick = () => {
setOpen(!open);
};
const iconcolor = theme.vars.palette.text.primary;
return (
<>
<ListItemButton
id={`${item.id}-btn`}
selected={open || selected === item.id}
sx={{
my: 0.25,
color: 'text.primary',
'&.Mui-selected': { color: 'text.primary', '&.Mui-focusVisible': { bgcolor: 'primary.light' } }
}}
onClick={handleClick}
>
<Activity mode={level === 0 ? 'visible' : 'hidden'}>
<ListItemIcon>
<DynamicIcon name={item.icon} color={iconcolor} size={18} stroke={1.5} />
</ListItemIcon>
</Activity>
<ListItemText primary={item.title} sx={{ mb: '-1px' }} />
{open ? <IconChevronUp size={18} stroke={1.5} /> : <IconChevronDown size={18} stroke={1.5} />}
</ListItemButton>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" sx={{ p: 0, pl: 3, position: 'relative', ...verticalDivider }}>
<NavCollapseLoop item={item} />
</List>
</Collapse>
</>
);
}
NavCollapseLoop.propTypes = { item: PropTypes.any };
NavCollapse.propTypes = { item: PropTypes.any, level: PropTypes.number };
@@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
// @mui
import List from '@mui/material/List';
import Typography from '@mui/material/Typography';
// @project
import NavCollapse from './NavCollapse';
import NavItem from './NavItem';
/*************************** RESPONSIVE DRAWER - GROUP ***************************/
export default function NavGroup({ item }) {
const renderNavItem = (menuItem) => {
// Render items based on the type
switch (menuItem.type) {
case 'collapse':
return <NavCollapse key={menuItem.id} item={menuItem} />;
case 'item':
return <NavItem key={menuItem.id} item={menuItem} />;
default:
return (
<Typography key={menuItem.id} variant="h6" color="error" align="center">
Fix - Group Collapse or Items
</Typography>
);
}
};
return (
<List
component="div"
subheader={
<Typography component="div" variant="caption" sx={{ mb: 0.75, color: 'grey.700' }}>
{item.title}
</Typography>
}
sx={{ '&:not(:first-of-type)': { pt: 1, borderTop: '1px solid', borderColor: 'divider' } }}
>
{item.children?.map((menuItem) => renderNavItem(menuItem))}
</List>
);
}
NavGroup.propTypes = { item: PropTypes.any };
@@ -0,0 +1,73 @@
import PropTypes from 'prop-types';
import { Activity, useEffect } from 'react';
// @mui
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
// @project
import { handlerActiveItem, handlerDrawerOpen, useGetMenuMaster } from '@/states/menu';
import DynamicIcon from '@/components/DynamicIcon';
import RouterLink from '@/components/Link';
import { usePathname } from '@/utils/navigation';
/*************************** RESPONSIVE DRAWER - ITEM ***************************/
export default function NavItem({ item, level = 0 }) {
const theme = useTheme();
const { menuMaster } = useGetMenuMaster();
const openItem = menuMaster.openedItem;
const downMD = useMediaQuery(theme.breakpoints.down('md'));
// Active menu item on page load
const pathname = usePathname();
useEffect(() => {
if (pathname === item.url) handlerActiveItem(item.id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);
const iconcolor = theme.palette.text.primary;
const itemHandler = () => {
if (downMD) handlerDrawerOpen(false);
};
return (
<ListItemButton
id={`${item.id}-btn`}
component={RouterLink}
to={item.url}
{...(item?.target && { target: '_blank' })}
selected={openItem === item.id}
disabled={item.disabled}
onClick={itemHandler}
sx={{
color: 'text.primary',
...(level === 0 && { my: 0.25, '&.Mui-selected.Mui-focusVisible': { bgcolor: 'primary.light' } }),
...(level > 0 && {
'&.Mui-selected': {
color: 'primary.main',
bgcolor: 'transparent',
'&:hover': { bgcolor: 'action.hover' },
'&.Mui-focusVisible': { bgcolor: 'action.focus' },
'& .MuiTypography-root': { fontWeight: 600 }
}
})
}}
>
<Activity mode={level === 0 ? 'visible' : 'hidden'}>
<ListItemIcon>
<DynamicIcon name={item.icon} color={iconcolor} size={18} stroke={1.5} />
</ListItemIcon>
</Activity>
<ListItemText primary={item.title} sx={{ mb: '-1px' }} />
</ListItemButton>
);
}
NavItem.propTypes = { item: PropTypes.any, level: PropTypes.number };
@@ -0,0 +1,26 @@
// @mui
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
// @project
import menuItems from '@/menu';
import NavGroup from './NavGroup';
/*************************** DRAWER CONTENT - RESPONSIVE DRAWER ***************************/
export default function ResponsiveDrawer() {
const navGroups = menuItems.items.map((item, index) => {
switch (item.type) {
case 'group':
return <NavGroup key={index} item={item} />;
default:
return (
<Typography key={index} variant="h6" color="error" align="center">
Fix - Navigation Group
</Typography>
);
}
});
return <Box sx={{ py: 1, transition: 'all 0.3s ease-in-out' }}>{navGroups}</Box>;
}
@@ -0,0 +1,30 @@
import useMediaQuery from '@mui/material/useMediaQuery';
import Stack from '@mui/material/Stack';
// @project
import NavCard from './NavCard';
import ResponsiveDrawer from './ResponsiveDrawer';
import { useGetMenuMaster } from '@/states/menu';
import { MINI_DRAWER_WIDTH } from '@/config';
import SimpleBar from '@/components/third-party/SimpleBar';
/*************************** DRAWER - CONTENT ***************************/
export default function DrawerContent() {
const upLG = useMediaQuery((theme) => theme.breakpoints.up('lg'));
const { menuMaster } = useGetMenuMaster();
const drawerOpen = menuMaster.isDashboardDrawerOpened;
const contentHeight = `calc(100vh - ${MINI_DRAWER_WIDTH}px)`;
return (
<SimpleBar sx={{ height: contentHeight }}>
<Stack sx={{ minHeight: contentHeight, px: !drawerOpen && upLG ? 0 : 2, justifyContent: 'space-between' }}>
<ResponsiveDrawer />
<NavCard />
</Stack>
</SimpleBar>
);
}
@@ -0,0 +1,42 @@
import PropTypes from 'prop-types';
import { Activity } from 'react';
// @mui
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
// @project
import { handlerDrawerOpen, useGetMenuMaster } from '@/states/menu';
import Logo from '@/components/logo';
// @assets
import { IconLayoutSidebarLeftCollapse, IconLayoutSidebarRightCollapse } from '@tabler/icons-react';
/*************************** DRAWER HEADER ***************************/
export default function DrawerHeader({ open }) {
const { menuMaster } = useGetMenuMaster();
const drawerOpen = menuMaster.isDashboardDrawerOpened;
return (
<Box sx={{ width: 1, px: 2, py: { xs: 2, md: 2.5 } }}>
<Stack direction="row" sx={{ alignItems: 'center', justifyContent: open ? 'space-between' : 'center', height: 36 }}>
<Activity mode={open ? 'visible' : 'hidden'}>
<Logo />
</Activity>
<IconButton
aria-label="open drawer"
onClick={() => handlerDrawerOpen(!drawerOpen)}
size="small"
color="secondary"
variant="outlined"
>
{!drawerOpen ? <IconLayoutSidebarRightCollapse size={20} /> : <IconLayoutSidebarLeftCollapse size={20} />}
</IconButton>
</Stack>
</Box>
);
}
DrawerHeader.propTypes = { open: PropTypes.bool };
@@ -0,0 +1,53 @@
// @mui
import { styled } from '@mui/material/styles';
import Drawer from '@mui/material/Drawer';
// @project
import { DRAWER_WIDTH } from '@/config';
// Mixin for common ) (open/closed) drawer state0....
const commonDrawerStyles = (theme) => ({
borderRight: `1px solid ${theme.vars.palette.grey[300]}`,
overflowX: 'hidden'
});
// Mixin for opened drawer state
const openedMixin = (theme) => ({
...commonDrawerStyles(theme),
width: DRAWER_WIDTH,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen
})
});
// Mixin for closed drawer state
const closedMixin = (theme) => ({
...commonDrawerStyles(theme),
width: 0,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
})
});
/*************************** DRAWER - MINI STYLED ***************************/
const MiniDrawerStyled = styled(Drawer, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
width: DRAWER_WIDTH,
flexShrink: 0,
whiteSpace: 'nowrap',
boxSizing: 'border-box',
...(open && {
...openedMixin(theme),
'& .MuiDrawer-paper': openedMixin(theme)
}),
...(!open && {
...closedMixin(theme),
'& .MuiDrawer-paper': closedMixin(theme)
})
}));
export default MiniDrawerStyled;
@@ -0,0 +1,69 @@
import PropTypes from 'prop-types';
import { Activity, useMemo } from 'react';
import useMediaQuery from '@mui/material/useMediaQuery';
import Divider from '@mui/material/Divider';
import Drawer from '@mui/material/Drawer';
import Box from '@mui/material/Box';
// @project
import DrawerHeader from './DrawerHeader';
import DrawerContent from './DrawerContent';
import MiniDrawerStyled from './MiniDrawerStyled';
import { handlerDrawerOpen, useGetMenuMaster } from '@/states/menu';
import { DRAWER_WIDTH } from '@/config';
/*************************** ADMIN LAYOUT - DRAWER ***************************/
export default function MainDrawer({ window }) {
const { menuMaster } = useGetMenuMaster();
const drawerOpen = menuMaster.isDashboardDrawerOpened;
const downLG = useMediaQuery((theme) => theme.breakpoints.down('lg'));
// Define container for drawer when window is specified
const container = window !== undefined ? () => window().document.body : undefined;
// Memoize drawer content and header to prevent unnecessary re-renders
const drawerContent = useMemo(() => <DrawerContent />, []);
const drawerHeader = useMemo(() => <DrawerHeader open={drawerOpen} />, [drawerOpen]);
return (
<Box component="nav" sx={{ flexShrink: { md: 0 }, zIndex: 1200 }} aria-label="mailbox folders">
{/* Temporary drawer for small media */}
<Drawer
container={container}
variant="temporary"
open={drawerOpen && downLG}
onClose={() => handlerDrawerOpen(!drawerOpen)}
slotProps={{
paper: {
sx: {
boxSizing: 'border-box',
width: DRAWER_WIDTH,
borderRight: '1px solid',
borderRightColor: 'divider',
backgroundImage: 'none',
boxShadow: 'inherit'
}
}
}}
>
{drawerHeader}
<Divider sx={{ mx: 2 }} />
{drawerContent}
</Drawer>
{/* Permanent mini-drawer for large media */}
<Activity mode={!downLG ? 'visible' : 'hidden'}>
<MiniDrawerStyled variant="permanent" open={drawerOpen}>
{drawerHeader}
<Divider sx={{ mx: 2 }} />
{drawerContent}
</MiniDrawerStyled>
</Activity>
</Box>
);
}
MainDrawer.propTypes = { window: PropTypes.func };
@@ -0,0 +1,26 @@
// @mui
import { styled } from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
// @project
import { DRAWER_WIDTH } from '@/config';
/*************************** HEADER - APP BAR STYLED ***************************/
const AppBarStyled = styled(AppBar, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
zIndex: theme.zIndex.drawer + 1, // Ensure AppBar appears above the Drawer
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
...(open && {
marginLeft: DRAWER_WIDTH, // Shift AppBar to the right by the drawer width
width: `calc(100% - ${DRAWER_WIDTH}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen
})
})
}));
export default AppBarStyled;
@@ -0,0 +1,315 @@
import { Fragment, useState } from 'react';
// @mui
import { keyframes, useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import Badge from '@mui/material/Badge';
import Button from '@mui/material/Button';
import CardHeader from '@mui/material/CardHeader';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Fade from '@mui/material/Fade';
import IconButton from '@mui/material/IconButton';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import Popper from '@mui/material/Popper';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
// @project
import EmptyNotification from '@/components/header/empty-state/EmptyNotification';
import MainCard from '@/components/MainCard';
import NotificationItem from '@/components/NotificationItem';
import SimpleBar from '@/components/third-party/SimpleBar';
// @assets
import { IconBell, IconCode, IconChevronDown, IconGitBranch, IconNote, IconGps } from '@tabler/icons-react';
import avatar1 from '@/assets/images/users/avatar-1.png';
import avatar4 from '@/assets/images/users/avatar-4.png';
import avatar5 from '@/assets/images/users/avatar-5.png';
const swing = keyframes`
20% {
transform: rotate(15deg) scale(1);
}
40% {
transform: rotate(-10deg) scale(1.05);
}
60% {
transform: rotate(5deg) scale(1.1);
}
80% {
transform: rotate(-5deg) scale(1.05);
}
100% {
transform: rotate(0deg) scale(1);
}
`;
/*************************** HEADER - NOTIFICATION ***************************/
export default function Notification() {
const theme = useTheme();
const downSM = useMediaQuery(theme.breakpoints.down('sm'));
const [anchorEl, setAnchorEl] = useState(null);
const [innerAnchorEl, setInnerAnchorEl] = useState(null);
const [allRead, setAllRead] = useState(false);
const [showEmpty, setShowEmpty] = useState(false);
const open = Boolean(anchorEl);
const innerOpen = Boolean(innerAnchorEl);
const id = open ? 'notification-action-popper' : undefined;
const innerId = innerOpen ? 'notification-inner-popper' : undefined;
const buttonStyle = { borderRadius: 2, p: 1 };
const listcontent = ['All notification', 'Users', 'Account', 'Language', 'Role & Permission', 'Setting'];
const [notifications, setNotifications] = useState([
{
avatar: { alt: 'Travis Howard', src: avatar1 },
badge: <IconCode size={14} />,
title: 'New Feature Deployed · Code Review Needed',
subTitle: 'Brenda Skiles',
dateTime: 'Jul 9'
},
{
avatar: <IconGitBranch />,
title: 'New Branch Created - "feature-user-auth"',
subTitle: 'Michael Carter',
dateTime: 'Jul 10',
isSeen: true
},
{
avatar: <IconGitBranch />,
title: 'Pull Request Opened "fix-dashboard-bug"',
subTitle: 'Sophia Green',
dateTime: 'Jul 11'
},
{
avatar: { alt: 'Travis Howard', src: avatar4 },
badge: <IconNote size={14} />,
title: 'Admin Approval · Document Submission Accepted',
subTitle: 'Salvatore Bogan',
dateTime: 'Jul 15',
isSeen: true
},
{
avatar: <IconGps />,
title: 'Location Access Request, Pending Your Approval',
subTitle: 'System Notification',
dateTime: 'Jul 24',
isSeen: true
}
]);
const [notifications2, setNotifications2] = useState([
{
avatar: { alt: 'Travis Howard', src: avatar1 },
badge: <IconCode size={14} />,
title: 'Code Review Requested · Feature Deployment',
subTitle: 'Brenda Skiles',
dateTime: 'Jul 9'
},
{
avatar: <IconGps />,
title: 'Location Access Granted [Security Update]',
subTitle: 'System Notification',
dateTime: 'Jul 24',
isSeen: true
},
{
avatar: { alt: 'Alice Smith', src: avatar5 },
badge: <IconNote size={14} />,
title: 'Document Submission Approval Received',
subTitle: 'Salvatore Bogan',
dateTime: 'Aug 12',
isSeen: true
},
{
avatar: { alt: 'Travis Howard', src: avatar1 },
badge: <IconCode size={14} />,
title: 'New Commit Pushed · Review Changes',
subTitle: 'Brenda Skiles',
dateTime: 'Jul 9'
},
{
avatar: <IconGps />,
title: 'Unusual Login Attempt [Verify Activity]',
subTitle: 'Security Alert',
dateTime: 'Jul 24'
}
]);
const handleActionClick = (event) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
};
const handleInnerActionClick = (event) => {
setInnerAnchorEl(innerAnchorEl ? null : event.currentTarget);
};
// Function to mark all notifications as read
const handleMarkAllAsRead = () => {
setNotifications((prevNotifications) => prevNotifications.map((notification) => ({ ...notification, isSeen: true })));
setNotifications2((prevNotifications2) => prevNotifications2.map((notification) => ({ ...notification, isSeen: true })));
setAllRead(true);
};
const handleClearAll = () => {
setNotifications([]);
setNotifications2([]);
setShowEmpty(true); // Set empty state to true when cleared
};
return (
<>
<IconButton
variant="outlined"
color="secondary"
size="small"
onClick={handleActionClick}
aria-label="show notifications"
{...(notifications.length !== 0 && !allRead && { sx: { '& svg': { animation: `${swing} 1s ease infinite` } } })}
>
<Badge
color="error"
variant="dot"
invisible={allRead || notifications.length === 0}
slotProps={{
badge: { sx: { height: 6, minWidth: 6, top: 4, right: 4, border: `1px solid ${theme.vars.palette.background.default}` } }
}}
>
<IconBell size={16} />
</Badge>
</IconButton>
<Popper
placement="bottom-end"
id={id}
open={open}
anchorEl={anchorEl}
popperOptions={{
modifiers: [{ name: 'offset', options: { offset: [downSM ? 45 : 0, 8] } }]
}}
transition
>
{({ TransitionProps }) => (
<Fade in={open} {...TransitionProps}>
<MainCard
sx={{
borderRadius: 2,
boxShadow: theme.vars.customShadows.tooltip,
width: 1,
minWidth: { xs: 352, sm: 240 },
maxWidth: { xs: 352, md: 420 },
p: 0
}}
>
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
<Box>
<CardHeader
sx={{ p: 1 }}
title={
<Stack direction="row" sx={{ gap: 1, justifyContent: 'space-between' }}>
<Button
color="secondary"
size="small"
sx={{ typography: 'h6' }}
endIcon={<IconChevronDown size={16} />}
onClick={handleInnerActionClick}
>
All Notification
</Button>
<Popper
placement="bottom-start"
id={innerId}
open={innerOpen}
anchorEl={innerAnchorEl}
transition
popperOptions={{ modifiers: [{ name: 'preventOverflow', options: { boundary: 'clippingParents' } }] }}
>
{({ TransitionProps }) => (
<Fade in={innerOpen} {...TransitionProps}>
<MainCard sx={{ borderRadius: 2, boxShadow: theme.vars.customShadows.tooltip, minWidth: 156, p: 0.5 }}>
<ClickAwayListener onClickAway={() => setInnerAnchorEl(null)}>
<List disablePadding>
{listcontent.map((item, index) => (
<ListItemButton key={index} sx={buttonStyle} onClick={handleInnerActionClick}>
<ListItemText>{item}</ListItemText>
</ListItemButton>
))}
</List>
</ClickAwayListener>
</MainCard>
</Fade>
)}
</Popper>
<Button color="primary" size="small" onClick={handleMarkAllAsRead} disabled={allRead}>
Mark All as Read
</Button>
</Stack>
}
/>
{showEmpty ? (
<EmptyNotification />
) : (
<Fragment>
<CardContent sx={{ px: 0.5, py: 2, '&:last-child': { pb: 2 } }}>
<SimpleBar sx={{ maxHeight: 405, height: 1 }}>
<List disablePadding>
<ListSubheader disableSticky sx={{ color: 'text.disabled', typography: 'caption', py: 0.5, px: 1, mb: 0.5 }}>
Last 7 Days
</ListSubheader>
{notifications.map((notification, index) => (
<ListItemButton key={index} sx={buttonStyle}>
<NotificationItem
avatar={notification.avatar}
{...(notification.badge && { badgeAvatar: { children: notification.badge } })}
title={notification.title}
subTitle={notification.subTitle}
dateTime={notification.dateTime}
isSeen={notification.isSeen}
/>
</ListItemButton>
))}
<ListSubheader
disableSticky
sx={{ color: 'text.disabled', typography: 'caption', py: 0.5, px: 1, mb: 0.5, mt: 1.5 }}
>
Older
</ListSubheader>
{notifications2.map((notification, index) => (
<ListItemButton key={index} sx={buttonStyle}>
<NotificationItem
avatar={notification.avatar}
{...(notification.badge && { badgeAvatar: { children: notification.badge } })}
title={notification.title}
subTitle={notification.subTitle}
dateTime={notification.dateTime}
isSeen={notification.isSeen}
/>
</ListItemButton>
))}
</List>
</SimpleBar>
</CardContent>
<CardActions sx={{ p: 1 }}>
<Button fullWidth color="error" onClick={handleClearAll}>
Clear all
</Button>
</CardActions>
</Fragment>
)}
</Box>
</ClickAwayListener>
</MainCard>
</Fade>
)}
</Popper>
</>
);
}
@@ -0,0 +1,210 @@
import { useState } from 'react';
// @mui
import { useTheme } from '@mui/material/styles';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Divider from '@mui/material/Divider';
import Fade from '@mui/material/Fade';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Popper from '@mui/material/Popper';
import Stack from '@mui/material/Stack';
import Switch from '@mui/material/Switch';
import Box from '@mui/material/Box';
// @third-party
import { enqueueSnackbar } from 'notistack';
// @project
import { ThemeI18n } from '@/config';
import MainCard from '@/components/MainCard';
import Profile from '@/components/Profile';
import { AvatarSize, ChipIconPosition } from '@/enum';
import useConfig from '@/hooks/useConfig';
// @assets
import { IconChevronRight, IconLanguage, IconLogout, IconSettings, IconTextDirectionLtr } from '@tabler/icons-react';
import profile from '@/assets/images/users/avatar-1.png';
/*************************** HEADER - PROFILE DATA ***************************/
const profileData = {
avatar: { src: profile, size: AvatarSize.XS },
title: 'Erika Collins',
caption: 'Super Admin'
};
const languageList = [
{ key: ThemeI18n.EN, value: 'English' },
{ key: ThemeI18n.FR, value: 'French' },
{ key: ThemeI18n.RO, value: 'Romanian' },
{ key: ThemeI18n.ZH, value: 'Chinese' }
];
/*************************** HEADER - PROFILE ***************************/
export default function ProfileSection() {
const theme = useTheme();
const {
state: { i18n }
} = useConfig();
const [anchorEl, setAnchorEl] = useState(null);
const [innerAnchorEl, setInnerAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const innerOpen = Boolean(innerAnchorEl);
const id = open ? 'profile-action-popper' : undefined;
const innerId = innerOpen ? 'profile-inner-popper' : undefined;
const buttonStyle = { borderRadius: 2, p: 1 };
const handleActionClick = (event) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
};
const handleInnerActionClick = (event) => {
setInnerAnchorEl(innerAnchorEl ? null : event.currentTarget);
};
const logoutAccount = () => {
setAnchorEl(null);
};
const i18nHandler = (event, key) => {
handleInnerActionClick(event);
if (key != i18n) enqueueSnackbar('Upgrade to pro for language change');
};
return (
<>
<Box onClick={handleActionClick} sx={{ cursor: 'pointer' }}>
<Box sx={{ display: { xs: 'none', sm: 'flex' } }}>
<Profile {...profileData} />
</Box>
<Box sx={{ display: { xs: 'block', sm: 'none' } }}>
<Avatar {...profileData.avatar} alt={profileData.title} />
</Box>
</Box>
<Popper
placement="bottom-end"
id={id}
open={open}
anchorEl={anchorEl}
transition
popperOptions={{ modifiers: [{ name: 'offset', options: { offset: [8, 8] } }] }}
>
{({ TransitionProps }) => (
<Fade in={open} {...TransitionProps}>
<MainCard sx={{ borderRadius: 2, boxShadow: theme.vars.customShadows.tooltip, minWidth: 220, p: 0.5 }}>
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
<Stack sx={{ px: 0.5, py: 0.75 }}>
<Profile
{...profileData}
sx={{
flexDirection: 'column',
justifyContent: 'center',
textAlign: 'center',
width: 1,
'& .MuiAvatar-root': { width: 48, height: 48 }
}}
/>
<Divider sx={{ my: 1 }} />
<List disablePadding>
<ListItem
secondaryAction={<Switch size="small" checked={false} onChange={() => enqueueSnackbar('Upgrade to pro for RTL')} />}
sx={{ py: 1, pl: 1, '& .MuiListItemSecondaryAction-root': { right: 8 } }}
>
<ListItemIcon>
<IconTextDirectionLtr size={16} />
</ListItemIcon>
<ListItemText primary="RTL" />
</ListItem>
<ListItemButton sx={buttonStyle} onClick={handleInnerActionClick}>
<ListItemIcon>
<IconLanguage size={16} />
</ListItemIcon>
<ListItemText primary="Language" />
<Chip
label={languageList.filter((item) => item.key === i18n)[0]?.value.slice(0, 3)}
variant="text"
size="small"
color="secondary"
icon={<IconChevronRight size={16} />}
position={ChipIconPosition.RIGHT}
sx={{ textTransform: 'capitalize' }}
/>
<Popper
placement="left-start"
id={innerId}
open={innerOpen}
anchorEl={innerAnchorEl}
transition
popperOptions={{
modifiers: [
{
name: 'preventOverflow',
options: {
boundary: 'clippingParents'
}
},
{ name: 'offset', options: { offset: [0, 8] } }
]
}}
>
{({ TransitionProps }) => (
<Fade in={innerOpen} {...TransitionProps}>
<MainCard sx={{ borderRadius: 2, boxShadow: theme.vars.customShadows.tooltip, minWidth: 150, p: 0.5 }}>
<ClickAwayListener onClickAway={() => setInnerAnchorEl(null)}>
<List disablePadding>
{languageList.map((item, index) => (
<ListItemButton
selected={item.key === i18n}
key={index}
sx={buttonStyle}
onClick={(event) => i18nHandler(event, item.key)}
>
<ListItemText>{item.value}</ListItemText>
</ListItemButton>
))}
</List>
</ClickAwayListener>
</MainCard>
</Fade>
)}
</Popper>
</ListItemButton>
<ListItemButton href="#" sx={{ ...buttonStyle, my: 0.5 }}>
<ListItemIcon>
<IconSettings size={16} />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItemButton>
<ListItem disablePadding>
<Button
fullWidth
variant="outlined"
color="secondary"
size="small"
endIcon={<IconLogout size={16} />}
onClick={logoutAccount}
>
Logout
</Button>
</ListItem>
</List>
</Stack>
</ClickAwayListener>
</MainCard>
</Fade>
)}
</Popper>
</>
);
}
@@ -0,0 +1,205 @@
import { Fragment, useEffect, useRef, useState } from 'react';
// @mui
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Fade from '@mui/material/Fade';
import InputAdornment from '@mui/material/InputAdornment';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import OutlinedInput from '@mui/material/OutlinedInput';
import Popper from '@mui/material/Popper';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
// @project
import EmptySearch from '@/components/header/empty-state/EmptySearch';
import MainCard from '@/components/MainCard';
import NotificationItem from '@/components/NotificationItem';
import { AvatarSize } from '@/enum';
// @assets
import { IconCommand, IconSearch } from '@tabler/icons-react';
import avatar1 from '@/assets/images/users/avatar-1.png';
import avatar2 from '@/assets/images/users/avatar-2.png';
/*************************** HEADER - SEARCH DATA ***************************/
const profileData = [
{ alt: 'Aplican Warner', src: avatar1, title: 'Aplican Warner', subTitle: 'Admin' },
{ alt: 'Apliaye Aweoa', src: avatar2, title: 'Apliaye Aweoa', subTitle: 'Admin' }
];
const listCotent = [
{ title: 'Role', items: ['Applican', 'App User'] },
{ title: 'Files', items: ['Applican', 'Applican'] }
];
/*************************** HEADER - SEARCH BAR ***************************/
export default function SearchBar() {
const theme = useTheme();
const downSM = useMediaQuery(theme.breakpoints.down('sm'));
const buttonStyle = { borderRadius: 2, p: 1 };
const [anchorEl, setAnchorEl] = useState(null);
const [isEmptySearch, setIsEmptySearch] = useState(true);
const [isPopperOpen, setIsPopperOpen] = useState(false);
const inputRef = useRef(null);
// Function to open the popper
const openPopper = (event) => {
setAnchorEl(inputRef.current);
setIsPopperOpen(true);
};
const handleActionClick = (event) => {
if (isPopperOpen) {
// If popper is open, close it
setIsPopperOpen(false);
setAnchorEl(null);
} else {
openPopper(event);
}
};
const handleInputChange = (event) => {
const isEmpty = event.target.value.trim() === '';
setIsEmptySearch(isEmpty);
if (!isPopperOpen && !isEmpty) {
openPopper(event);
}
};
const handleKeyDown = (event) => {
if (event.key === 'Enter' && !isPopperOpen) {
openPopper(event);
} else if (event.key === 'Escape' && isPopperOpen) {
setIsPopperOpen(false);
setAnchorEl(null);
} else if (event.ctrlKey && event.key === 'k') {
event.preventDefault();
if (!isPopperOpen) {
openPopper(event);
}
}
};
const renderSubheader = (title, withMarginTop = false) => (
<ListSubheader sx={{ color: 'text.disabled', typography: 'caption', py: 0.5, px: 1, mb: 0.5, ...(withMarginTop && { mt: 1.5 }) }}>
{title}
</ListSubheader>
);
const renderListItem = (item, index) => (
<ListItemButton key={index} sx={buttonStyle} onClick={handleActionClick}>
<ListItemText primary={item} />
</ListItemButton>
);
useEffect(() => {
const handleGlobalKeyDown = (event) => {
if (event.ctrlKey && event.key === 'k') {
event.preventDefault();
// Check if the search input is not focused before opening the popper
if (document.activeElement !== inputRef.current) {
openPopper(event);
inputRef.current?.focus();
}
}
};
window.addEventListener('keydown', handleGlobalKeyDown);
return () => {
window.removeEventListener('keydown', handleGlobalKeyDown);
};
}, [isPopperOpen]);
return (
<>
<OutlinedInput
inputRef={inputRef}
placeholder="Search here"
startAdornment={
<InputAdornment position="start">
<IconSearch />
</InputAdornment>
}
endAdornment={
<InputAdornment position="end">
<Stack direction="row" sx={{ gap: 0.25, opacity: 0.8, alignItems: 'center', color: 'grey.600', '& svg': { color: 'inherit' } }}>
<IconCommand />
<Typography variant="caption">+ K</Typography>
</Stack>
</InputAdornment>
}
aria-describedby="Search"
slotProps={{ input: { 'aria-label': 'search' } }}
onClick={handleActionClick}
onKeyDown={handleKeyDown}
onChange={handleInputChange}
sx={{ minWidth: { xs: 170, sm: 240 } }}
/>
<Popper
placement="bottom"
id={isPopperOpen ? 'search-action-popper' : undefined}
open={isPopperOpen}
anchorEl={anchorEl}
transition
popperOptions={{
modifiers: [{ name: 'offset', options: { offset: [downSM ? 20 : 0, 8] } }]
}}
>
{({ TransitionProps }) => (
<Fade in={isPopperOpen} {...TransitionProps}>
<MainCard
sx={{
borderRadius: 2,
boxShadow: theme.vars.customShadows.tooltip,
width: 1,
minWidth: { xs: 352, sm: 240 },
maxWidth: { xs: 352, md: 420 },
p: 0.5
}}
>
<ClickAwayListener
onClickAway={() => {
setIsPopperOpen(false);
setAnchorEl(null);
}}
>
{isEmptySearch ? (
<EmptySearch />
) : (
<List disablePadding>
{renderSubheader('Users')}
{profileData.map((user, index) => (
<ListItemButton sx={buttonStyle} key={index} onClick={handleActionClick}>
<NotificationItem
avatar={{ alt: user.alt, src: user.src, size: AvatarSize.XS }}
title={user.title}
subTitle={user.subTitle}
/>
</ListItemButton>
))}
{listCotent.map((list, item) => (
<Fragment key={item}>
{renderSubheader(list.title, true)}
{list.items.map((item, index) => renderListItem(item, index))}
</Fragment>
))}
</List>
)}
</ClickAwayListener>
</MainCard>
</Fade>
)}
</Popper>
</>
);
}
@@ -0,0 +1,24 @@
// @mui
import IconButton from '@mui/material/IconButton';
// @assets
import { IconSun } from '@tabler/icons-react';
// @third-party
import { useSnackbar } from 'notistack';
/*************************** HEADER - THEME MODE SWITCHER ***************************/
export default function ThemeModeSwitcher() {
const { enqueueSnackbar } = useSnackbar();
const handleClick = () => {
enqueueSnackbar('Upgrade to pro for theme mode');
};
return (
<IconButton variant="outlined" color="secondary" size="small" onClick={handleClick} aria-label="show theme mode">
<IconSun size={16} />
</IconButton>
);
}
@@ -0,0 +1,30 @@
// @mui
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
// @project
import Notification from './Notification';
import Profile from './Profile';
import SearchBar from './SearchBar';
import ThemeModeSwitcher from './ThemeModeSwitcher';
import Breadcrumbs from '@/components/Breadcrumbs';
/*************************** HEADER CONTENT ***************************/
export default function HeaderContent() {
return (
<>
<Stack direction="row" sx={{ alignItems: 'center', justifyContent: { xs: 'flex-end', md: 'space-between' }, gap: 2, width: 1 }}>
<Box sx={{ display: { xs: 'none', md: 'block' } }}>
<Breadcrumbs />
</Box>
<Stack direction="row" sx={{ alignItems: 'center', gap: { xs: 1, sm: 1.5 } }}>
<SearchBar />
<ThemeModeSwitcher />
<Notification />
<Profile />
</Stack>
</Stack>
</>
);
}
@@ -0,0 +1,74 @@
import { useMemo } from 'react';
// @mui
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import AppBar from '@mui/material/AppBar';
import IconButton from '@mui/material/IconButton';
import Toolbar from '@mui/material/Toolbar';
// @project
import AppBarStyled from './AppBarStyled';
import HeaderContent from './HeaderContent';
import { handlerDrawerOpen, useGetMenuMaster } from '@/states/menu';
import { DRAWER_WIDTH } from '@/config';
// @assets
import { IconLayoutSidebarRightCollapse, IconMenu2 } from '@tabler/icons-react';
/*************************** ADMIN LAYOUT - HEADER ***************************/
export default function Header() {
const theme = useTheme();
const downLG = useMediaQuery(theme.breakpoints.down('lg'));
const { menuMaster } = useGetMenuMaster();
const drawerOpen = menuMaster.isDashboardDrawerOpened;
// Memoized header content to avoid unnecessary re-renders
const headerContent = useMemo(() => <HeaderContent />, []);
// Common header content
const mainHeader = (
<Toolbar sx={{ minHeight: { xs: 68, md: 76 } }}>
<IconButton
aria-label="open drawer"
onClick={() => handlerDrawerOpen(!drawerOpen)}
size="small"
color="secondary"
variant="outlined"
sx={{ display: { xs: 'inline-flex', lg: !drawerOpen ? 'inline-flex' : 'none' }, mr: 1 }}
>
<>
{!drawerOpen && !downLG && <IconLayoutSidebarRightCollapse size={20} />}
{downLG && <IconMenu2 size={20} />}
</>
</IconButton>
{headerContent}
</Toolbar>
);
// AppBar props, including styles that vary based on drawer state and screen size
const appBar = {
color: 'inherit',
position: 'fixed',
elevation: 0,
sx: {
borderBottom: `1px solid ${theme.vars.palette.grey[300]}`,
zIndex: 1200,
width: { xs: '100%', lg: drawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : 1 }
}
};
return (
<>
{!downLG ? (
<AppBarStyled open={drawerOpen} {...appBar}>
{mainHeader}
</AppBarStyled>
) : (
<AppBar {...appBar}>{mainHeader}</AppBar>
)}
</>
);
}
@@ -0,0 +1,57 @@
import { useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import useMediaQuery from '@mui/material/useMediaQuery';
import Container from '@mui/material/Container';
import Toolbar from '@mui/material/Toolbar';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
// @project
import Drawer from './Drawer';
import Header from './Header';
import { handlerDrawerOpen, useGetMenuMaster } from '@/states/menu';
import Breadcrumbs from '@/components/Breadcrumbs';
import Loader from '@/components/Loader';
import { DRAWER_WIDTH } from '@/config';
/*************************** ADMIN LAYOUT ***************************/
export default function DashboardLayout() {
const { menuMasterLoading } = useGetMenuMaster();
const downXL = useMediaQuery((theme) => theme.breakpoints.down('xl'));
useEffect(() => {
handlerDrawerOpen(!downXL);
}, [downXL]);
if (menuMasterLoading) return <Loader />;
return (
<Stack direction="row" sx={{ width: 1 }}>
<Header />
<Drawer />
<Box component="main" sx={{ width: `calc(100% - ${DRAWER_WIDTH}px)`, flexGrow: 1, p: { xs: 2, sm: 3 } }}>
<Toolbar sx={{ minHeight: { xs: 54, sm: 46, md: 76 } }} />
<Box
sx={{
py: 0.4,
px: 1.5,
mx: { xs: -2, sm: -3 },
display: { xs: 'block', md: 'none' },
borderBottom: 1,
borderColor: 'divider',
mb: 2
}}
>
<Breadcrumbs />
</Box>
<Container maxWidth="lg" sx={{ px: { xs: 0, sm: 2 } }}>
<Outlet />
</Container>
</Box>
</Stack>
);
}
@@ -0,0 +1,55 @@
import { Outlet } from 'react-router-dom';
// @mui
import CardMedia from '@mui/material/CardMedia';
import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
// @project
import LogoMain from '@/components/logo/LogoMain';
import GetImagePath from '@/utils/GetImagePath';
// @assets
import dashboardLightIcon from '@/assets/images/graphics/hosting/dashboard-light.svg';
import dashboardDarkIcon from '@/assets/images/graphics/hosting/dashboard-dark.svg';
const dashBoardImage = { light: dashboardLightIcon, dark: dashboardDarkIcon };
/*************************** AUTH LAYOUT ***************************/
export default function AuthLayout() {
return (
<Grid container sx={{ height: '100vh' }}>
<Grid size={{ xs: 12, md: 6, lg: 7 }} sx={{ p: { xs: 3, sm: 7 } }}>
<Outlet />
</Grid>
<Grid size={{ xs: 12, md: 6, lg: 5 }} sx={{ bgcolor: 'grey.100', pt: 7, display: { xs: 'none', md: 'block' } }}>
<Stack sx={{ height: 1, justifyContent: 'space-between' }}>
<Stack sx={{ alignItems: 'center', gap: 2 }}>
<LogoMain />
<Typography variant="body2" color="grey.700" align="center" sx={{ maxWidth: 400 }}>
SaaS platform for seamless data management and user insights. Unlock growth with real-time analytics and flexible features.
</Typography>
</Stack>
<Box sx={{ pt: 6, pl: 6, height: 'calc(100% - 114px)' }}>
<CardMedia
image={GetImagePath(dashBoardImage)}
sx={{
height: 1,
border: '4px solid',
borderColor: 'grey.300',
borderBottom: 'none',
borderRight: 'none',
backgroundPositionX: 'left',
backgroundPositionY: 'top',
borderTopLeftRadius: 24
}}
/>
</Box>
</Stack>
</Grid>
</Grid>
);
}