First Commit
This commit is contained in:
Vendored
+67
@@ -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 };
|
||||
+104
@@ -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 };
|
||||
+44
@@ -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 };
|
||||
+73
@@ -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 };
|
||||
+26
@@ -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>;
|
||||
}
|
||||
+30
@@ -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>
|
||||
);
|
||||
}
|
||||
+42
@@ -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 };
|
||||
+53
@@ -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 };
|
||||
+26
@@ -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;
|
||||
Vendored
+315
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Vendored
+210
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Vendored
+205
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+24
@@ -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>
|
||||
);
|
||||
}
|
||||
+30
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user