mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-10 18:43:41 +08:00
feat: add new theme berry (#860)
* feat: add theme berry * docs: add development notes * fix: fix blank page * chore: update implementation * fix: fix package.json * chore: update ui copy --------- Co-authored-by: JustSong <songquanpeng@foxmail.com>
This commit is contained in:
173
web/berry/src/layout/MainLayout/Header/ProfileSection/index.js
Normal file
173
web/berry/src/layout/MainLayout/Header/ProfileSection/index.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import {
|
||||
Avatar,
|
||||
Chip,
|
||||
ClickAwayListener,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Popper,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard';
|
||||
import Transitions from 'ui-component/extended/Transitions';
|
||||
import User1 from 'assets/images/users/user-round.svg';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
|
||||
// assets
|
||||
import { IconLogout, IconSettings, IconUserScan } from '@tabler/icons-react';
|
||||
|
||||
// ==============================|| PROFILE MENU ||============================== //
|
||||
|
||||
const ProfileSection = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const customization = useSelector((state) => state.customization);
|
||||
const { logout } = useLogin();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
/**
|
||||
* anchorRef is used on different componets and specifying one type leads to other components throwing an error
|
||||
* */
|
||||
const anchorRef = useRef(null);
|
||||
const handleLogout = async () => {
|
||||
logout();
|
||||
};
|
||||
|
||||
const handleClose = (event) => {
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
|
||||
const prevOpen = useRef(open);
|
||||
useEffect(() => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
anchorRef.current.focus();
|
||||
}
|
||||
|
||||
prevOpen.current = open;
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Chip
|
||||
sx={{
|
||||
height: '48px',
|
||||
alignItems: 'center',
|
||||
borderRadius: '27px',
|
||||
transition: 'all .2s ease-in-out',
|
||||
borderColor: theme.palette.primary.light,
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
'&[aria-controls="menu-list-grow"], &:hover': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
background: `${theme.palette.primary.main}!important`,
|
||||
color: theme.palette.primary.light,
|
||||
'& svg': {
|
||||
stroke: theme.palette.primary.light
|
||||
}
|
||||
},
|
||||
'& .MuiChip-label': {
|
||||
lineHeight: 0
|
||||
}
|
||||
}}
|
||||
icon={
|
||||
<Avatar
|
||||
src={User1}
|
||||
sx={{
|
||||
...theme.typography.mediumAvatar,
|
||||
margin: '8px 0 8px 8px !important',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
ref={anchorRef}
|
||||
aria-controls={open ? 'menu-list-grow' : undefined}
|
||||
aria-haspopup="true"
|
||||
color="inherit"
|
||||
/>
|
||||
}
|
||||
label={<IconSettings stroke={1.5} size="1.5rem" color={theme.palette.primary.main} />}
|
||||
variant="outlined"
|
||||
ref={anchorRef}
|
||||
aria-controls={open ? 'menu-list-grow' : undefined}
|
||||
aria-haspopup="true"
|
||||
onClick={handleToggle}
|
||||
color="primary"
|
||||
/>
|
||||
<Popper
|
||||
placement="bottom-end"
|
||||
open={open}
|
||||
anchorEl={anchorRef.current}
|
||||
role={undefined}
|
||||
transition
|
||||
disablePortal
|
||||
popperOptions={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 14]
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Transitions in={open} {...TransitionProps}>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
|
||||
<List
|
||||
component="nav"
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: 350,
|
||||
minWidth: 150,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: '10px',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
minWidth: '100%'
|
||||
},
|
||||
'& .MuiListItemButton-root': {
|
||||
mt: 0.5
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemButton sx={{ borderRadius: `${customization.borderRadius}px` }} onClick={() => navigate('/panel/profile')}>
|
||||
<ListItemIcon>
|
||||
<IconUserScan stroke={1.5} size="1.3rem" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant="body2">设置</Typography>} />
|
||||
</ListItemButton>
|
||||
|
||||
<ListItemButton sx={{ borderRadius: `${customization.borderRadius}px` }} onClick={handleLogout}>
|
||||
<ListItemIcon>
|
||||
<IconLogout stroke={1.5} size="1.3rem" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant="body2">登出</Typography>} />
|
||||
</ListItemButton>
|
||||
</List>
|
||||
</MainCard>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Transitions>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileSection;
|
||||
68
web/berry/src/layout/MainLayout/Header/index.js
Normal file
68
web/berry/src/layout/MainLayout/Header/index.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Avatar, Box, ButtonBase } from '@mui/material';
|
||||
|
||||
// project imports
|
||||
import LogoSection from '../LogoSection';
|
||||
import ProfileSection from './ProfileSection';
|
||||
|
||||
// assets
|
||||
import { IconMenu2 } from '@tabler/icons-react';
|
||||
|
||||
// ==============================|| MAIN NAVBAR / HEADER ||============================== //
|
||||
|
||||
const Header = ({ handleLeftDrawerToggle }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* logo & toggler button */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 228,
|
||||
display: 'flex',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
width: 'auto'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box component="span" sx={{ display: { xs: 'none', md: 'block' }, flexGrow: 1 }}>
|
||||
<LogoSection />
|
||||
</Box>
|
||||
<ButtonBase sx={{ borderRadius: '12px', overflow: 'hidden' }}>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.secondary.light,
|
||||
color: theme.palette.secondary.dark,
|
||||
'&:hover': {
|
||||
background: theme.palette.secondary.dark,
|
||||
color: theme.palette.secondary.light
|
||||
}
|
||||
}}
|
||||
onClick={handleLeftDrawerToggle}
|
||||
color="inherit"
|
||||
>
|
||||
<IconMenu2 stroke={1.5} size="1.3rem" />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
<ProfileSection />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
handleLeftDrawerToggle: PropTypes.func
|
||||
};
|
||||
|
||||
export default Header;
|
||||
23
web/berry/src/layout/MainLayout/LogoSection/index.js
Normal file
23
web/berry/src/layout/MainLayout/LogoSection/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
// material-ui
|
||||
import { ButtonBase } from '@mui/material';
|
||||
|
||||
// project imports
|
||||
import Logo from 'ui-component/Logo';
|
||||
import { MENU_OPEN } from 'store/actions';
|
||||
|
||||
// ==============================|| MAIN LOGO ||============================== //
|
||||
|
||||
const LogoSection = () => {
|
||||
const defaultId = useSelector((state) => state.customization.defaultId);
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<ButtonBase disableRipple onClick={() => dispatch({ type: MENU_OPEN, id: defaultId })} component={Link} to="/">
|
||||
<Logo />
|
||||
</ButtonBase>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogoSection;
|
||||
130
web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js
Normal file
130
web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js
Normal file
@@ -0,0 +1,130 @@
|
||||
// import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
// material-ui
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
CardContent,
|
||||
// Grid,
|
||||
// LinearProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Typography
|
||||
// linearProgressClasses
|
||||
} from '@mui/material';
|
||||
import User1 from 'assets/images/users/user-round.svg';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
// assets
|
||||
// import TableChartOutlinedIcon from '@mui/icons-material/TableChartOutlined';
|
||||
|
||||
// styles
|
||||
// const BorderLinearProgress = styled(LinearProgress)(({ theme }) => ({
|
||||
// height: 10,
|
||||
// borderRadius: 30,
|
||||
// [`&.${linearProgressClasses.colorPrimary}`]: {
|
||||
// backgroundColor: '#fff'
|
||||
// },
|
||||
// [`& .${linearProgressClasses.bar}`]: {
|
||||
// borderRadius: 5,
|
||||
// backgroundColor: theme.palette.primary.main
|
||||
// }
|
||||
// }));
|
||||
|
||||
const CardStyle = styled(Card)(({ theme }) => ({
|
||||
background: theme.palette.primary.light,
|
||||
marginBottom: '22px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
'&:after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
width: '157px',
|
||||
height: '157px',
|
||||
background: theme.palette.primary[200],
|
||||
borderRadius: '50%',
|
||||
top: '-105px',
|
||||
right: '-96px'
|
||||
}
|
||||
}));
|
||||
|
||||
// ==============================|| PROGRESS BAR WITH LABEL ||============================== //
|
||||
|
||||
// function LinearProgressWithLabel({ value, ...others }) {
|
||||
// const theme = useTheme();
|
||||
|
||||
// return (
|
||||
// <Grid container direction="column" spacing={1} sx={{ mt: 1.5 }}>
|
||||
// <Grid item>
|
||||
// <Grid container justifyContent="space-between">
|
||||
// <Grid item>
|
||||
// <Typography variant="h6" sx={{ color: theme.palette.primary[800] }}>
|
||||
// Progress
|
||||
// </Typography>
|
||||
// </Grid>
|
||||
// <Grid item>
|
||||
// <Typography variant="h6" color="inherit">{`${Math.round(value)}%`}</Typography>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// <Grid item>
|
||||
// <BorderLinearProgress variant="determinate" value={value} {...others} />
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// );
|
||||
// }
|
||||
|
||||
// LinearProgressWithLabel.propTypes = {
|
||||
// value: PropTypes.number
|
||||
// };
|
||||
|
||||
// ==============================|| SIDEBAR MENU Card ||============================== //
|
||||
|
||||
const MenuCard = () => {
|
||||
const theme = useTheme();
|
||||
const account = useSelector((state) => state.account);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<CardStyle>
|
||||
<CardContent sx={{ p: 2 }}>
|
||||
<List sx={{ p: 0, m: 0 }}>
|
||||
<ListItem alignItems="flex-start" disableGutters sx={{ p: 0 }}>
|
||||
<ListItemAvatar sx={{ mt: 0 }}>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
src={User1}
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.largeAvatar,
|
||||
color: theme.palette.primary.main,
|
||||
border: 'none',
|
||||
borderColor: theme.palette.primary.main,
|
||||
background: '#fff',
|
||||
marginRight: '12px'
|
||||
}}
|
||||
onClick={() => navigate('/panel/profile')}
|
||||
></Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
sx={{ mt: 0 }}
|
||||
primary={
|
||||
<Typography variant="subtitle1" sx={{ color: theme.palette.primary[800] }}>
|
||||
{account.user?.username}
|
||||
</Typography>
|
||||
}
|
||||
secondary={<Typography variant="caption"> 欢迎回来 </Typography>}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
{/* <LinearProgressWithLabel value={80} /> */}
|
||||
</CardContent>
|
||||
</CardStyle>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuCard;
|
||||
@@ -0,0 +1,158 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation, useNavigate } from 'react-router';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Collapse, List, ListItemButton, ListItemIcon, ListItemText, Typography } from '@mui/material';
|
||||
|
||||
// project imports
|
||||
import NavItem from '../NavItem';
|
||||
|
||||
// assets
|
||||
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
|
||||
import { IconChevronDown, IconChevronUp } from '@tabler/icons-react';
|
||||
|
||||
// ==============================|| SIDEBAR MENU LIST COLLAPSE ITEMS ||============================== //
|
||||
|
||||
const NavCollapse = ({ menu, level }) => {
|
||||
const theme = useTheme();
|
||||
const customization = useSelector((state) => state.customization);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open);
|
||||
setSelected(!selected ? menu.id : null);
|
||||
if (menu?.id !== 'authentication') {
|
||||
navigate(menu.children[0]?.url);
|
||||
}
|
||||
};
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const checkOpenForParent = (child, id) => {
|
||||
child.forEach((item) => {
|
||||
if (item.url === pathname) {
|
||||
setOpen(true);
|
||||
setSelected(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// menu collapse for sub-levels
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
setSelected(null);
|
||||
if (menu.children) {
|
||||
menu.children.forEach((item) => {
|
||||
if (item.children?.length) {
|
||||
checkOpenForParent(item.children, menu.id);
|
||||
}
|
||||
if (item.url === pathname) {
|
||||
setSelected(menu.id);
|
||||
setOpen(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname, menu.children]);
|
||||
|
||||
// menu collapse & item
|
||||
const menus = menu.children?.map((item) => {
|
||||
switch (item.type) {
|
||||
case 'collapse':
|
||||
return <NavCollapse key={item.id} menu={item} level={level + 1} />;
|
||||
case 'item':
|
||||
return <NavItem key={item.id} item={item} level={level + 1} />;
|
||||
default:
|
||||
return (
|
||||
<Typography key={item.id} variant="h6" color="error" align="center">
|
||||
Menu Items Error
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const Icon = menu.icon;
|
||||
const menuIcon = menu.icon ? (
|
||||
<Icon strokeWidth={1.5} size="1.3rem" style={{ marginTop: 'auto', marginBottom: 'auto' }} />
|
||||
) : (
|
||||
<FiberManualRecordIcon
|
||||
sx={{
|
||||
width: selected === menu.id ? 8 : 6,
|
||||
height: selected === menu.id ? 8 : 6
|
||||
}}
|
||||
fontSize={level > 0 ? 'inherit' : 'medium'}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
mb: 0.5,
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
|
||||
py: level > 1 ? 1 : 1.25,
|
||||
pl: `${level * 24}px`
|
||||
}}
|
||||
selected={selected === menu.id}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<ListItemIcon sx={{ my: 'auto', minWidth: !menu.icon ? 18 : 36 }}>{menuIcon}</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant={selected === menu.id ? 'h5' : 'body1'} color="inherit" sx={{ my: 'auto' }}>
|
||||
{menu.title}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
menu.caption && (
|
||||
<Typography variant="caption" sx={{ ...theme.typography.subMenuCaption }} display="block" gutterBottom>
|
||||
{menu.caption}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{open ? (
|
||||
<IconChevronUp stroke={1.5} size="1rem" style={{ marginTop: 'auto', marginBottom: 'auto' }} />
|
||||
) : (
|
||||
<IconChevronDown stroke={1.5} size="1rem" style={{ marginTop: 'auto', marginBottom: 'auto' }} />
|
||||
)}
|
||||
</ListItemButton>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<List
|
||||
component="div"
|
||||
disablePadding
|
||||
sx={{
|
||||
position: 'relative',
|
||||
'&:after': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
left: '32px',
|
||||
top: 0,
|
||||
height: '100%',
|
||||
width: '1px',
|
||||
opacity: 1,
|
||||
background: theme.palette.primary.light
|
||||
}
|
||||
}}
|
||||
>
|
||||
{menus}
|
||||
</List>
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
NavCollapse.propTypes = {
|
||||
menu: PropTypes.object,
|
||||
level: PropTypes.number
|
||||
};
|
||||
|
||||
export default NavCollapse;
|
||||
@@ -0,0 +1,61 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Divider, List, Typography } from '@mui/material';
|
||||
|
||||
// project imports
|
||||
import NavItem from '../NavItem';
|
||||
import NavCollapse from '../NavCollapse';
|
||||
|
||||
// ==============================|| SIDEBAR MENU LIST GROUP ||============================== //
|
||||
|
||||
const NavGroup = ({ item }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// menu list collapse & items
|
||||
const items = item.children?.map((menu) => {
|
||||
switch (menu.type) {
|
||||
case 'collapse':
|
||||
return <NavCollapse key={menu.id} menu={menu} level={1} />;
|
||||
case 'item':
|
||||
return <NavItem key={menu.id} item={menu} level={1} />;
|
||||
default:
|
||||
return (
|
||||
<Typography key={menu.id} variant="h6" color="error" align="center">
|
||||
Menu Items Error
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<List
|
||||
subheader={
|
||||
item.title && (
|
||||
<Typography variant="caption" sx={{ ...theme.typography.menuCaption }} display="block" gutterBottom>
|
||||
{item.title}
|
||||
{item.caption && (
|
||||
<Typography variant="caption" sx={{ ...theme.typography.subMenuCaption }} display="block" gutterBottom>
|
||||
{item.caption}
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
>
|
||||
{items}
|
||||
</List>
|
||||
|
||||
{/* group divider */}
|
||||
<Divider sx={{ mt: 0.25, mb: 1.25 }} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
NavGroup.propTypes = {
|
||||
item: PropTypes.object
|
||||
};
|
||||
|
||||
export default NavGroup;
|
||||
@@ -0,0 +1,115 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { forwardRef, useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography, useMediaQuery } from '@mui/material';
|
||||
|
||||
// project imports
|
||||
import { MENU_OPEN, SET_MENU } from 'store/actions';
|
||||
|
||||
// assets
|
||||
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
|
||||
|
||||
// ==============================|| SIDEBAR MENU LIST ITEMS ||============================== //
|
||||
|
||||
const NavItem = ({ item, level }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { pathname } = useLocation();
|
||||
const customization = useSelector((state) => state.customization);
|
||||
const matchesSM = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
|
||||
const Icon = item.icon;
|
||||
const itemIcon = item?.icon ? (
|
||||
<Icon stroke={1.5} size="1.3rem" />
|
||||
) : (
|
||||
<FiberManualRecordIcon
|
||||
sx={{
|
||||
width: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6,
|
||||
height: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6
|
||||
}}
|
||||
fontSize={level > 0 ? 'inherit' : 'medium'}
|
||||
/>
|
||||
);
|
||||
|
||||
let itemTarget = '_self';
|
||||
if (item.target) {
|
||||
itemTarget = '_blank';
|
||||
}
|
||||
|
||||
let listItemProps = {
|
||||
component: forwardRef((props, ref) => <Link ref={ref} {...props} to={item.url} target={itemTarget} />)
|
||||
};
|
||||
if (item?.external) {
|
||||
listItemProps = { component: 'a', href: item.url, target: itemTarget };
|
||||
}
|
||||
|
||||
const itemHandler = (id) => {
|
||||
dispatch({ type: MENU_OPEN, id });
|
||||
if (matchesSM) dispatch({ type: SET_MENU, opened: false });
|
||||
};
|
||||
|
||||
// active menu item on page load
|
||||
useEffect(() => {
|
||||
const currentIndex = document.location.pathname
|
||||
.toString()
|
||||
.split('/')
|
||||
.findIndex((id) => id === item.id);
|
||||
if (currentIndex > -1) {
|
||||
dispatch({ type: MENU_OPEN, id: item.id });
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
{...listItemProps}
|
||||
disabled={item.disabled}
|
||||
sx={{
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
mb: 0.5,
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
|
||||
py: level > 1 ? 1 : 1.25,
|
||||
pl: `${level * 24}px`
|
||||
}}
|
||||
selected={customization.isOpen.findIndex((id) => id === item.id) > -1}
|
||||
onClick={() => itemHandler(item.id)}
|
||||
>
|
||||
<ListItemIcon sx={{ my: 'auto', minWidth: !item?.icon ? 18 : 36 }}>{itemIcon}</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant={customization.isOpen.findIndex((id) => id === item.id) > -1 ? 'h5' : 'body1'} color="inherit">
|
||||
{item.title}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
item.caption && (
|
||||
<Typography variant="caption" sx={{ ...theme.typography.subMenuCaption }} display="block" gutterBottom>
|
||||
{item.caption}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{item.chip && (
|
||||
<Chip
|
||||
color={item.chip.color}
|
||||
variant={item.chip.variant}
|
||||
size={item.chip.size}
|
||||
label={item.chip.label}
|
||||
avatar={item.chip.avatar && <Avatar>{item.chip.avatar}</Avatar>}
|
||||
/>
|
||||
)}
|
||||
</ListItemButton>
|
||||
);
|
||||
};
|
||||
|
||||
NavItem.propTypes = {
|
||||
item: PropTypes.object,
|
||||
level: PropTypes.number
|
||||
};
|
||||
|
||||
export default NavItem;
|
||||
36
web/berry/src/layout/MainLayout/Sidebar/MenuList/index.js
Normal file
36
web/berry/src/layout/MainLayout/Sidebar/MenuList/index.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// material-ui
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
// project imports
|
||||
import NavGroup from './NavGroup';
|
||||
import menuItem from 'menu-items';
|
||||
import { isAdmin } from 'utils/common';
|
||||
|
||||
// ==============================|| SIDEBAR MENU LIST ||============================== //
|
||||
const MenuList = () => {
|
||||
const userIsAdmin = isAdmin();
|
||||
|
||||
return (
|
||||
<>
|
||||
{menuItem.items.map((item) => {
|
||||
if (item.type !== 'group') {
|
||||
return (
|
||||
<Typography key={item.id} variant="h6" color="error" align="center">
|
||||
Menu Items Error
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const filteredChildren = item.children.filter((child) => !child.isAdmin || userIsAdmin);
|
||||
|
||||
if (filteredChildren.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <NavGroup key={item.id} item={{ ...item, children: filteredChildren }} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuList;
|
||||
94
web/berry/src/layout/MainLayout/Sidebar/index.js
Normal file
94
web/berry/src/layout/MainLayout/Sidebar/index.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Box, Chip, Drawer, Stack, useMediaQuery } from '@mui/material';
|
||||
|
||||
// third-party
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import { BrowserView, MobileView } from 'react-device-detect';
|
||||
|
||||
// project imports
|
||||
import MenuList from './MenuList';
|
||||
import LogoSection from '../LogoSection';
|
||||
import MenuCard from './MenuCard';
|
||||
import { drawerWidth } from 'store/constant';
|
||||
|
||||
// ==============================|| SIDEBAR DRAWER ||============================== //
|
||||
|
||||
const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
|
||||
const theme = useTheme();
|
||||
const matchUpMd = useMediaQuery(theme.breakpoints.up('md'));
|
||||
|
||||
const drawer = (
|
||||
<>
|
||||
<Box sx={{ display: { xs: 'block', md: 'none' } }}>
|
||||
<Box sx={{ display: 'flex', p: 2, mx: 'auto' }}>
|
||||
<LogoSection />
|
||||
</Box>
|
||||
</Box>
|
||||
<BrowserView>
|
||||
<PerfectScrollbar
|
||||
component="div"
|
||||
style={{
|
||||
height: !matchUpMd ? 'calc(100vh - 56px)' : 'calc(100vh - 88px)',
|
||||
paddingLeft: '16px',
|
||||
paddingRight: '16px'
|
||||
}}
|
||||
>
|
||||
<MenuList />
|
||||
<MenuCard />
|
||||
<Stack direction="row" justifyContent="center" sx={{ mb: 2 }}>
|
||||
<Chip label={process.env.REACT_APP_VERSION} disabled chipcolor="secondary" size="small" sx={{ cursor: 'pointer' }} />
|
||||
</Stack>
|
||||
</PerfectScrollbar>
|
||||
</BrowserView>
|
||||
<MobileView>
|
||||
<Box sx={{ px: 2 }}>
|
||||
<MenuList />
|
||||
<MenuCard />
|
||||
<Stack direction="row" justifyContent="center" sx={{ mb: 2 }}>
|
||||
<Chip label={process.env.REACT_APP_VERSION} disabled chipcolor="secondary" size="small" sx={{ cursor: 'pointer' }} />
|
||||
</Stack>
|
||||
</Box>
|
||||
</MobileView>
|
||||
</>
|
||||
);
|
||||
|
||||
const container = window !== undefined ? () => window.document.body : undefined;
|
||||
|
||||
return (
|
||||
<Box component="nav" sx={{ flexShrink: { md: 0 }, width: matchUpMd ? drawerWidth : 'auto' }} aria-label="mailbox folders">
|
||||
<Drawer
|
||||
container={container}
|
||||
variant={matchUpMd ? 'persistent' : 'temporary'}
|
||||
anchor="left"
|
||||
open={drawerOpen}
|
||||
onClose={drawerToggle}
|
||||
sx={{
|
||||
'& .MuiDrawer-paper': {
|
||||
width: drawerWidth,
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
borderRight: 'none',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
top: '88px'
|
||||
}
|
||||
}
|
||||
}}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
color="inherit"
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Sidebar.propTypes = {
|
||||
drawerOpen: PropTypes.bool,
|
||||
drawerToggle: PropTypes.func,
|
||||
window: PropTypes.object
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
103
web/berry/src/layout/MainLayout/index.js
Normal file
103
web/berry/src/layout/MainLayout/index.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import AuthGuard from 'utils/route-guard/AuthGuard';
|
||||
|
||||
// material-ui
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import { AppBar, Box, CssBaseline, Toolbar, useMediaQuery } from '@mui/material';
|
||||
import AdminContainer from 'ui-component/AdminContainer';
|
||||
|
||||
// project imports
|
||||
import Breadcrumbs from 'ui-component/extended/Breadcrumbs';
|
||||
import Header from './Header';
|
||||
import Sidebar from './Sidebar';
|
||||
import navigation from 'menu-items';
|
||||
import { drawerWidth } from 'store/constant';
|
||||
import { SET_MENU } from 'store/actions';
|
||||
|
||||
// assets
|
||||
import { IconChevronRight } from '@tabler/icons-react';
|
||||
|
||||
// styles
|
||||
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
|
||||
...theme.typography.mainContent,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
transition: theme.transitions.create(
|
||||
'margin',
|
||||
open
|
||||
? {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen
|
||||
}
|
||||
: {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
}
|
||||
),
|
||||
[theme.breakpoints.up('md')]: {
|
||||
marginLeft: open ? 0 : -(drawerWidth - 20),
|
||||
width: `calc(100% - ${drawerWidth}px)`
|
||||
},
|
||||
[theme.breakpoints.down('md')]: {
|
||||
marginLeft: '20px',
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
padding: '16px'
|
||||
},
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
marginLeft: '10px',
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
padding: '16px',
|
||||
marginRight: '10px'
|
||||
}
|
||||
}));
|
||||
|
||||
// ==============================|| MAIN LAYOUT ||============================== //
|
||||
|
||||
const MainLayout = () => {
|
||||
const theme = useTheme();
|
||||
const matchDownMd = useMediaQuery(theme.breakpoints.down('md'));
|
||||
// Handle left drawer
|
||||
const leftDrawerOpened = useSelector((state) => state.customization.opened);
|
||||
const dispatch = useDispatch();
|
||||
const handleLeftDrawerToggle = () => {
|
||||
dispatch({ type: SET_MENU, opened: !leftDrawerOpened });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<CssBaseline />
|
||||
{/* header */}
|
||||
<AppBar
|
||||
enableColorOnDark
|
||||
position="fixed"
|
||||
color="inherit"
|
||||
elevation={0}
|
||||
sx={{
|
||||
bgcolor: theme.palette.background.default,
|
||||
transition: leftDrawerOpened ? theme.transitions.create('width') : 'none'
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<Header handleLeftDrawerToggle={handleLeftDrawerToggle} />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{/* drawer */}
|
||||
<Sidebar drawerOpen={!matchDownMd ? leftDrawerOpened : !leftDrawerOpened} drawerToggle={handleLeftDrawerToggle} />
|
||||
|
||||
{/* main content */}
|
||||
<Main theme={theme} open={leftDrawerOpened}>
|
||||
{/* breadcrumb */}
|
||||
<Breadcrumbs separator={IconChevronRight} navigation={navigation} icon title rightAlign />
|
||||
<AuthGuard>
|
||||
<AdminContainer>
|
||||
<Outlet />
|
||||
</AdminContainer>
|
||||
</AuthGuard>
|
||||
</Main>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
||||
Reference in New Issue
Block a user