Separate Client Portal & Dashboard
This commit is contained in:
32
frontend/dashboard/src/layouts/LogoOnlyLayout.tsx
Normal file
32
frontend/dashboard/src/layouts/LogoOnlyLayout.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
// @mui
|
||||
import { styled } from '@mui/material/styles';
|
||||
// components
|
||||
import Logo from '../components/Logo';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const HeaderStyle = styled('header')(({ theme }) => ({
|
||||
top: 0,
|
||||
left: 0,
|
||||
lineHeight: 0,
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
padding: theme.spacing(3, 3, 0),
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
padding: theme.spacing(5, 5, 0)
|
||||
}
|
||||
}));
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export default function LogoOnlyLayout() {
|
||||
return (
|
||||
<>
|
||||
<HeaderStyle>
|
||||
<Logo />
|
||||
</HeaderStyle>
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useState } from 'react';
|
||||
// @mui
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import { Box, Divider, Typography, Stack, MenuItem, Avatar } from '@mui/material';
|
||||
// components
|
||||
import MenuPopover from '../../../components/MenuPopover';
|
||||
import { IconButtonAnimate } from '../../../components/animate';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const MENU_OPTIONS = [
|
||||
{
|
||||
label: 'Home',
|
||||
linkTo: '/',
|
||||
},
|
||||
{
|
||||
label: 'Profile',
|
||||
linkTo: '/',
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
linkTo: '/',
|
||||
},
|
||||
];
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export default function AccountPopover() {
|
||||
const [open, setOpen] = useState<HTMLElement | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setOpen(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(null);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
navigate('/auth/login');
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButtonAnimate
|
||||
onClick={handleOpen}
|
||||
sx={{
|
||||
p: 0,
|
||||
...(open && {
|
||||
'&:before': {
|
||||
zIndex: 1,
|
||||
content: "''",
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
position: 'absolute',
|
||||
bgcolor: (theme) => alpha(theme.palette.grey[900], 0.8),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src="https://minimal-assets-api.vercel.app/assets/images/avatars/avatar_5.jpg"
|
||||
alt="Rayan Moran"
|
||||
/>
|
||||
</IconButtonAnimate>
|
||||
|
||||
<MenuPopover
|
||||
open={Boolean(open)}
|
||||
anchorEl={open}
|
||||
onClose={handleClose}
|
||||
sx={{
|
||||
p: 0,
|
||||
mt: 1.5,
|
||||
ml: 0.75,
|
||||
'& .MuiMenuItem-root': {
|
||||
typography: 'body2',
|
||||
borderRadius: 0.75,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ my: 1.5, px: 2.5 }}>
|
||||
<Typography variant="subtitle2" noWrap>
|
||||
Rayan Moran
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }} noWrap>
|
||||
rayan.moran@gmail.com
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
|
||||
<Stack sx={{ p: 1 }}>
|
||||
{MENU_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.label} onClick={handleClose}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
|
||||
<MenuItem sx={{ m: 1 }} onClick={handleLogout}>Logout</MenuItem>
|
||||
</MenuPopover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useState } from 'react';
|
||||
// @mui
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import { Avatar, Typography, ListItemText, ListItemAvatar, MenuItem } from '@mui/material';
|
||||
// utils
|
||||
import { fToNow } from '../../../utils/formatTime';
|
||||
// _mock_
|
||||
import { _contacts } from '../../../_mock';
|
||||
// components
|
||||
import Iconify from '../../../components/Iconify';
|
||||
import Scrollbar from '../../../components/Scrollbar';
|
||||
import MenuPopover from '../../../components/MenuPopover';
|
||||
import BadgeStatus from '../../../components/BadgeStatus';
|
||||
import { IconButtonAnimate } from '../../../components/animate';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const ITEM_HEIGHT = 64;
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export default function ContactsPopover() {
|
||||
const [open, setOpen] = useState<HTMLElement | null>(null);
|
||||
|
||||
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setOpen(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButtonAnimate
|
||||
color={open ? 'primary' : 'default'}
|
||||
onClick={handleOpen}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
...(open && {
|
||||
bgcolor: (theme) =>
|
||||
alpha(theme.palette.primary.main, theme.palette.action.focusOpacity),
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Iconify icon={'eva:people-fill'} width={20} height={20} />
|
||||
</IconButtonAnimate>
|
||||
|
||||
<MenuPopover
|
||||
open={Boolean(open)}
|
||||
anchorEl={open}
|
||||
onClose={handleClose}
|
||||
sx={{
|
||||
mt: 1.5,
|
||||
ml: 0.75,
|
||||
width: 320,
|
||||
'& .MuiMenuItem-root': {
|
||||
px: 1.5,
|
||||
height: ITEM_HEIGHT,
|
||||
borderRadius: 0.75,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ p: 1.5 }}>
|
||||
Contacts <Typography component="span">({_contacts.length})</Typography>
|
||||
</Typography>
|
||||
|
||||
<Scrollbar sx={{ height: ITEM_HEIGHT * 6 }}>
|
||||
{_contacts.map((contact) => (
|
||||
<MenuItem key={contact.id}>
|
||||
<ListItemAvatar sx={{ position: 'relative' }}>
|
||||
<Avatar src={contact.avatar} />
|
||||
<BadgeStatus
|
||||
status={contact.status}
|
||||
sx={{ position: 'absolute', right: 1, bottom: 1 }}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ typography: 'subtitle2', mb: 0.25 }}
|
||||
secondaryTypographyProps={{ typography: 'caption' }}
|
||||
primary={contact.name}
|
||||
secondary={contact.status === 'offline' && fToNow(contact.lastActivity)}
|
||||
/>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Scrollbar>
|
||||
</MenuPopover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useState } from 'react';
|
||||
// @mui
|
||||
import { MenuItem, Stack } from '@mui/material';
|
||||
// components
|
||||
import Image from '../../../components/Image';
|
||||
import MenuPopover from '../../../components/MenuPopover';
|
||||
import { IconButtonAnimate } from '../../../components/animate';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const LANGS = [
|
||||
{
|
||||
label: 'English',
|
||||
value: 'en',
|
||||
icon: 'https://minimal-assets-api.vercel.app/assets/icons/ic_flag_en.svg',
|
||||
},
|
||||
{
|
||||
label: 'German',
|
||||
value: 'de',
|
||||
icon: 'https://minimal-assets-api.vercel.app/assets/icons/ic_flag_de.svg',
|
||||
},
|
||||
{
|
||||
label: 'French',
|
||||
value: 'fr',
|
||||
icon: 'https://minimal-assets-api.vercel.app/assets/icons/ic_flag_fr.svg',
|
||||
},
|
||||
];
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export default function LanguagePopover() {
|
||||
const [open, setOpen] = useState<HTMLElement | null>(null);
|
||||
|
||||
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setOpen(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButtonAnimate
|
||||
onClick={handleOpen}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
...(open && { bgcolor: 'action.selected' }),
|
||||
}}
|
||||
>
|
||||
<Image disabledEffect src={LANGS[0].icon} alt={LANGS[0].label} />
|
||||
</IconButtonAnimate>
|
||||
|
||||
<MenuPopover
|
||||
open={Boolean(open)}
|
||||
anchorEl={open}
|
||||
onClose={handleClose}
|
||||
sx={{
|
||||
mt: 1.5,
|
||||
ml: 0.75,
|
||||
width: 180,
|
||||
'& .MuiMenuItem-root': { px: 1, typography: 'body2', borderRadius: 0.75 },
|
||||
}}
|
||||
>
|
||||
<Stack spacing={0.75}>
|
||||
{LANGS.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
selected={option.value === LANGS[0].value}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<Image
|
||||
disabledEffect
|
||||
alt={option.label}
|
||||
src={option.icon}
|
||||
sx={{ width: 28, mr: 2 }}
|
||||
/>
|
||||
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Stack>
|
||||
</MenuPopover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { noCase } from 'change-case';
|
||||
import { useState } from 'react';
|
||||
// @mui
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
Badge,
|
||||
Button,
|
||||
Avatar,
|
||||
Tooltip,
|
||||
Divider,
|
||||
Typography,
|
||||
ListItemText,
|
||||
ListSubheader,
|
||||
ListItemAvatar,
|
||||
ListItemButton,
|
||||
} from '@mui/material';
|
||||
// utils
|
||||
import { fToNow } from '../../../utils/formatTime';
|
||||
// _mock_
|
||||
import { _notifications } from '../../../_mock';
|
||||
// components
|
||||
import Iconify from '../../../components/Iconify';
|
||||
import Scrollbar from '../../../components/Scrollbar';
|
||||
import MenuPopover from '../../../components/MenuPopover';
|
||||
import { IconButtonAnimate } from '../../../components/animate';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export default function NotificationsPopover() {
|
||||
const [notifications, setNotifications] = useState(_notifications);
|
||||
|
||||
const totalUnRead = notifications.filter((item) => item.isUnRead === true).length;
|
||||
|
||||
const [open, setOpen] = useState<HTMLElement | null>(null);
|
||||
|
||||
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setOpen(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(null);
|
||||
};
|
||||
|
||||
const handleMarkAllAsRead = () => {
|
||||
setNotifications(
|
||||
notifications.map((notification) => ({
|
||||
...notification,
|
||||
isUnRead: false,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButtonAnimate
|
||||
color={open ? 'primary' : 'default'}
|
||||
onClick={handleOpen}
|
||||
sx={{ width: 40, height: 40 }}
|
||||
>
|
||||
<Badge badgeContent={totalUnRead} color="error">
|
||||
<Iconify icon="eva:bell-fill" width={20} height={20} />
|
||||
</Badge>
|
||||
</IconButtonAnimate>
|
||||
|
||||
<MenuPopover
|
||||
open={Boolean(open)}
|
||||
anchorEl={open}
|
||||
onClose={handleClose}
|
||||
sx={{ width: 360, p: 0, mt: 1.5, ml: 0.75 }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', py: 2, px: 2.5 }}>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant="subtitle1">Notifications</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
You have {totalUnRead} unread messages
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{totalUnRead > 0 && (
|
||||
<Tooltip title=" Mark all as read">
|
||||
<IconButtonAnimate color="primary" onClick={handleMarkAllAsRead}>
|
||||
<Iconify icon="eva:done-all-fill" width={20} height={20} />
|
||||
</IconButtonAnimate>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
|
||||
<Scrollbar sx={{ height: { xs: 340, sm: 'auto' } }}>
|
||||
<List
|
||||
disablePadding
|
||||
subheader={
|
||||
<ListSubheader disableSticky sx={{ py: 1, px: 2.5, typography: 'overline' }}>
|
||||
New
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
{notifications.slice(0, 2).map((notification) => (
|
||||
<NotificationItem key={notification.id} notification={notification} />
|
||||
))}
|
||||
</List>
|
||||
|
||||
<List
|
||||
disablePadding
|
||||
subheader={
|
||||
<ListSubheader disableSticky sx={{ py: 1, px: 2.5, typography: 'overline' }}>
|
||||
Before that
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
{notifications.slice(2, 5).map((notification) => (
|
||||
<NotificationItem key={notification.id} notification={notification} />
|
||||
))}
|
||||
</List>
|
||||
</Scrollbar>
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Button fullWidth disableRipple>
|
||||
View All
|
||||
</Button>
|
||||
</Box>
|
||||
</MenuPopover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type NotificationItemProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
avatar: string | null;
|
||||
type: string;
|
||||
createdAt: Date;
|
||||
isUnRead: boolean;
|
||||
};
|
||||
|
||||
function NotificationItem({ notification }: { notification: NotificationItemProps }) {
|
||||
const { avatar, title } = renderContent(notification);
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
sx={{
|
||||
py: 1.5,
|
||||
px: 2.5,
|
||||
mt: '1px',
|
||||
...(notification.isUnRead && {
|
||||
bgcolor: 'action.selected',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: 'background.neutral' }}>{avatar}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={title}
|
||||
secondary={
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
mt: 0.5,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'text.disabled',
|
||||
}}
|
||||
>
|
||||
<Iconify icon="eva:clock-outline" sx={{ mr: 0.5, width: 16, height: 16 }} />
|
||||
{fToNow(notification.createdAt)}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
function renderContent(notification: NotificationItemProps) {
|
||||
const title = (
|
||||
<Typography variant="subtitle2">
|
||||
{notification.title}
|
||||
<Typography component="span" variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{noCase(notification.description)}
|
||||
</Typography>
|
||||
</Typography>
|
||||
);
|
||||
|
||||
if (notification.type === 'order_placed') {
|
||||
return {
|
||||
avatar: (
|
||||
<img
|
||||
alt={notification.title}
|
||||
src="https://minimal-assets-api.vercel.app/assets/icons/ic_notification_package.svg"
|
||||
/>
|
||||
),
|
||||
title,
|
||||
};
|
||||
}
|
||||
if (notification.type === 'order_shipped') {
|
||||
return {
|
||||
avatar: (
|
||||
<img
|
||||
alt={notification.title}
|
||||
src="https://minimal-assets-api.vercel.app/assets/icons/ic_notification_shipping.svg"
|
||||
/>
|
||||
),
|
||||
title,
|
||||
};
|
||||
}
|
||||
if (notification.type === 'mail') {
|
||||
return {
|
||||
avatar: (
|
||||
<img
|
||||
alt={notification.title}
|
||||
src="https://minimal-assets-api.vercel.app/assets/icons/ic_notification_mail.svg"
|
||||
/>
|
||||
),
|
||||
title,
|
||||
};
|
||||
}
|
||||
if (notification.type === 'chat_message') {
|
||||
return {
|
||||
avatar: (
|
||||
<img
|
||||
alt={notification.title}
|
||||
src="https://minimal-assets-api.vercel.app/assets/icons/ic_notification_chat.svg"
|
||||
/>
|
||||
),
|
||||
title,
|
||||
};
|
||||
}
|
||||
return {
|
||||
avatar: notification.avatar ? <img alt={notification.title} src={notification.avatar} /> : null,
|
||||
title,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useState } from 'react';
|
||||
// @mui
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Input, Slide, Button, InputAdornment, ClickAwayListener } from '@mui/material';
|
||||
// utils
|
||||
import cssStyles from '../../../utils/cssStyles';
|
||||
// components
|
||||
import Iconify from '../../../components/Iconify';
|
||||
import { IconButtonAnimate } from '../../../components/animate';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const APPBAR_MOBILE = 64;
|
||||
const APPBAR_DESKTOP = 92;
|
||||
|
||||
const SearchbarStyle = styled('div')(({ theme }) => ({
|
||||
...cssStyles(theme).bgBlur(),
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 99,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
height: APPBAR_MOBILE,
|
||||
padding: theme.spacing(0, 3),
|
||||
boxShadow: theme.customShadows.z8,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
height: APPBAR_DESKTOP,
|
||||
padding: theme.spacing(0, 5),
|
||||
},
|
||||
}));
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export default function Searchbar() {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<div>
|
||||
{!isOpen && (
|
||||
<IconButtonAnimate onClick={handleOpen}>
|
||||
<Iconify icon={'eva:search-fill'} width={20} height={20} />
|
||||
</IconButtonAnimate>
|
||||
)}
|
||||
|
||||
<Slide direction="down" in={isOpen} mountOnEnter unmountOnExit>
|
||||
<SearchbarStyle>
|
||||
<Input
|
||||
autoFocus
|
||||
fullWidth
|
||||
disableUnderline
|
||||
placeholder="Search…"
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<Iconify
|
||||
icon={'eva:search-fill'}
|
||||
sx={{ color: 'text.disabled', width: 20, height: 20 }}
|
||||
/>
|
||||
</InputAdornment>
|
||||
}
|
||||
sx={{ mr: 1, fontWeight: 'fontWeightBold' }}
|
||||
/>
|
||||
<Button variant="contained" onClick={handleClose}>
|
||||
Search
|
||||
</Button>
|
||||
</SearchbarStyle>
|
||||
</Slide>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
}
|
||||
103
frontend/dashboard/src/layouts/dashboard/header/index.tsx
Normal file
103
frontend/dashboard/src/layouts/dashboard/header/index.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
// @mui
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Box, Stack, AppBar, Toolbar } from '@mui/material';
|
||||
// hooks
|
||||
import useOffSetTop from '../../../hooks/useOffSetTop';
|
||||
import useResponsive from '../../../hooks/useResponsive';
|
||||
// utils
|
||||
import cssStyles from '../../../utils/cssStyles';
|
||||
// config
|
||||
import { HEADER, NAVBAR } from '../../../config';
|
||||
// components
|
||||
import Logo from '../../../components/Logo';
|
||||
import Iconify from '../../../components/Iconify';
|
||||
import { IconButtonAnimate } from '../../../components/animate';
|
||||
//
|
||||
import Searchbar from './Searchbar';
|
||||
import AccountPopover from './AccountPopover';
|
||||
import LanguagePopover from './LanguagePopover';
|
||||
import ContactsPopover from './ContactsPopover';
|
||||
import NotificationsPopover from './NotificationsPopover';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type RootStyleProps = {
|
||||
isCollapse: boolean;
|
||||
isOffset: boolean;
|
||||
verticalLayout: boolean;
|
||||
};
|
||||
|
||||
const RootStyle = styled(AppBar, {
|
||||
shouldForwardProp: (prop) =>
|
||||
prop !== 'isCollapse' && prop !== 'isOffset' && prop !== 'verticalLayout',
|
||||
})<RootStyleProps>(({ isCollapse, isOffset, verticalLayout, theme }) => ({
|
||||
...cssStyles(theme).bgBlur(),
|
||||
boxShadow: 'none',
|
||||
height: HEADER.MOBILE_HEIGHT,
|
||||
zIndex: theme.zIndex.appBar + 1,
|
||||
transition: theme.transitions.create(['width', 'height'], {
|
||||
duration: theme.transitions.duration.shorter,
|
||||
}),
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
height: HEADER.DASHBOARD_DESKTOP_HEIGHT,
|
||||
width: `calc(100% - ${NAVBAR.DASHBOARD_WIDTH + 1}px)`,
|
||||
...(isCollapse && {
|
||||
width: `calc(100% - ${NAVBAR.DASHBOARD_COLLAPSE_WIDTH}px)`,
|
||||
}),
|
||||
...(isOffset && {
|
||||
height: HEADER.DASHBOARD_DESKTOP_OFFSET_HEIGHT,
|
||||
}),
|
||||
...(verticalLayout && {
|
||||
width: '100%',
|
||||
height: HEADER.DASHBOARD_DESKTOP_OFFSET_HEIGHT,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
onOpenSidebar: VoidFunction;
|
||||
isCollapse?: boolean;
|
||||
verticalLayout?: boolean;
|
||||
};
|
||||
|
||||
export default function DashboardHeader({
|
||||
onOpenSidebar,
|
||||
isCollapse = false,
|
||||
verticalLayout = false,
|
||||
}: Props) {
|
||||
const isOffset = useOffSetTop(HEADER.DASHBOARD_DESKTOP_HEIGHT) && !verticalLayout;
|
||||
|
||||
const isDesktop = useResponsive('up', 'lg');
|
||||
|
||||
return (
|
||||
<RootStyle isCollapse={isCollapse} isOffset={isOffset} verticalLayout={verticalLayout}>
|
||||
<Toolbar
|
||||
sx={{
|
||||
minHeight: '100% !important',
|
||||
px: { lg: 5 },
|
||||
}}
|
||||
>
|
||||
{isDesktop && verticalLayout && <Logo sx={{ mr: 2.5 }} />}
|
||||
|
||||
{!isDesktop && (
|
||||
<IconButtonAnimate onClick={onOpenSidebar} sx={{ mr: 1, color: 'text.primary' }}>
|
||||
<Iconify icon="eva:menu-2-fill" />
|
||||
</IconButtonAnimate>
|
||||
)}
|
||||
|
||||
<Searchbar />
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
<Stack direction="row" alignItems="center" spacing={{ xs: 0.5, sm: 1.5 }}>
|
||||
<LanguagePopover />
|
||||
<NotificationsPopover />
|
||||
<ContactsPopover />
|
||||
<AccountPopover />
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
</RootStyle>
|
||||
);
|
||||
}
|
||||
104
frontend/dashboard/src/layouts/dashboard/index.tsx
Normal file
104
frontend/dashboard/src/layouts/dashboard/index.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useState } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
// @mui
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Box } from '@mui/material';
|
||||
// hooks
|
||||
import useSettings from '../../hooks/useSettings';
|
||||
import useResponsive from '../../hooks/useResponsive';
|
||||
import useCollapseDrawer from '../../hooks/useCollapseDrawer';
|
||||
// config
|
||||
import { HEADER, NAVBAR } from '../../config';
|
||||
//
|
||||
import DashboardHeader from './header';
|
||||
import NavbarVertical from './navbar/NavbarVertical';
|
||||
import NavbarHorizontal from './navbar/NavbarHorizontal';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type MainStyleProps = {
|
||||
collapseClick: boolean;
|
||||
};
|
||||
|
||||
const MainStyle = styled('main', {
|
||||
shouldForwardProp: (prop) => prop !== 'collapseClick',
|
||||
})<MainStyleProps>(({ collapseClick, theme }) => ({
|
||||
flexGrow: 1,
|
||||
paddingTop: HEADER.MOBILE_HEIGHT + 24,
|
||||
paddingBottom: HEADER.MOBILE_HEIGHT + 24,
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: HEADER.DASHBOARD_DESKTOP_HEIGHT + 24,
|
||||
paddingBottom: HEADER.DASHBOARD_DESKTOP_HEIGHT + 24,
|
||||
width: `calc(100% - ${NAVBAR.DASHBOARD_WIDTH}px)`,
|
||||
transition: theme.transitions.create('margin-left', {
|
||||
duration: theme.transitions.duration.shorter,
|
||||
}),
|
||||
...(collapseClick && {
|
||||
marginLeft: NAVBAR.DASHBOARD_COLLAPSE_WIDTH,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export default function DashboardLayout() {
|
||||
const { collapseClick, isCollapse } = useCollapseDrawer();
|
||||
|
||||
const { themeLayout } = useSettings();
|
||||
|
||||
const isDesktop = useResponsive('up', 'lg');
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const verticalLayout = themeLayout === 'vertical';
|
||||
|
||||
if (verticalLayout) {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader onOpenSidebar={() => setOpen(true)} verticalLayout={verticalLayout} />
|
||||
|
||||
{isDesktop ? (
|
||||
<NavbarHorizontal />
|
||||
) : (
|
||||
<NavbarVertical isOpenSidebar={open} onCloseSidebar={() => setOpen(false)} />
|
||||
)}
|
||||
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
px: { lg: 2 },
|
||||
pt: {
|
||||
xs: `${HEADER.MOBILE_HEIGHT + 24}px`,
|
||||
lg: `${HEADER.DASHBOARD_DESKTOP_HEIGHT + 80}px`,
|
||||
},
|
||||
pb: {
|
||||
xs: `${HEADER.MOBILE_HEIGHT + 24}px`,
|
||||
lg: `${HEADER.DASHBOARD_DESKTOP_HEIGHT + 24}px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: { lg: 'flex' },
|
||||
minHeight: { lg: 1 },
|
||||
}}
|
||||
>
|
||||
<DashboardHeader isCollapse={isCollapse} onOpenSidebar={() => setOpen(true)} />
|
||||
|
||||
<NavbarVertical isOpenSidebar={open} onCloseSidebar={() => setOpen(false)} />
|
||||
|
||||
<MainStyle collapseClick={collapseClick}>
|
||||
<Outlet />
|
||||
</MainStyle>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// @mui
|
||||
import { Box } from '@mui/material';
|
||||
// components
|
||||
import { IconButtonAnimate } from '../../../components/animate';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
onToggleCollapse: VoidFunction;
|
||||
collapseClick: boolean;
|
||||
};
|
||||
|
||||
export default function CollapseButton({ onToggleCollapse, collapseClick }: Props) {
|
||||
return (
|
||||
<IconButtonAnimate onClick={onToggleCollapse}>
|
||||
<Box
|
||||
sx={{
|
||||
lineHeight: 0,
|
||||
transition: (theme) =>
|
||||
theme.transitions.create('transform', {
|
||||
duration: theme.transitions.duration.shorter,
|
||||
}),
|
||||
...(collapseClick && {
|
||||
transform: 'rotate(180deg)',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
</IconButtonAnimate>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const icon = (
|
||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0h24v24H0z" />
|
||||
<g fill="currentColor" fillRule="nonzero">
|
||||
<path
|
||||
d="M14.3283 11.4343 18.5126 7.25c.4142-.4142.4142-1.0858 0-1.5-.4142-.4142-1.0858-.4142-1.5 0l-5.543 5.5429c-.3904.3905-.3904 1.0237 0 1.4142l5.543 5.5429c.4142.4142 1.0858.4142 1.5 0 .4142-.4142.4142-1.0858 0-1.5l-4.1843-4.1843a.8.8 0 0 1 0-1.1314Z"
|
||||
opacity=".48"
|
||||
/>
|
||||
<path d="M8.3283 11.4343 12.5126 7.25c.4142-.4142.4142-1.0858 0-1.5-.4142-.4142-1.0858-.4142-1.5 0l-5.543 5.5429c-.3904.3905-.3904 1.0237 0 1.4142l5.543 5.5429c.4142.4142 1.0858.4142 1.5 0 .4142-.4142.4142-1.0858 0-1.5l-4.1843-4.1843a.8.8 0 0 1 0-1.1314Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
// components
|
||||
import SvgIconStyle from '../../../components/SvgIconStyle';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const getIcon = (name: string) => (
|
||||
<SvgIconStyle src={`/icons/${name}.svg`} sx={{ width: 1, height: 1 }} />
|
||||
);
|
||||
|
||||
const ICONS = {
|
||||
user: getIcon('ic_user'),
|
||||
ecommerce: getIcon('ic_ecommerce'),
|
||||
analytics: getIcon('ic_analytics'),
|
||||
dashboard: getIcon('ic_dashboard'),
|
||||
};
|
||||
|
||||
const navConfig = [
|
||||
// GENERAL
|
||||
// ----------------------------------------------------------------------
|
||||
{
|
||||
subheader: 'general v3.2.0',
|
||||
items: [
|
||||
{ title: 'One', path: '/dashboard/one', icon: ICONS.dashboard },
|
||||
{ title: 'Two', path: '/dashboard/two', icon: ICONS.ecommerce },
|
||||
{ title: 'Three', path: '/dashboard/three', icon: ICONS.analytics },
|
||||
],
|
||||
},
|
||||
|
||||
// MANAGEMENT
|
||||
// ----------------------------------------------------------------------
|
||||
{
|
||||
subheader: 'Management',
|
||||
items: [
|
||||
{
|
||||
title: 'Master Data',
|
||||
// path: '/',
|
||||
icon: ICONS.user,
|
||||
children: [
|
||||
{ title: 'Obat', path: '/medicines' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default navConfig;
|
||||
@@ -0,0 +1,62 @@
|
||||
// @mui
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Box, Link, Typography, Avatar } from '@mui/material';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const RootStyle = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(2, 2.5),
|
||||
borderRadius: Number(theme.shape.borderRadius) * 1.5,
|
||||
backgroundColor: theme.palette.grey[500_12],
|
||||
transition: theme.transitions.create('opacity', {
|
||||
duration: theme.transitions.duration.shorter,
|
||||
}),
|
||||
}));
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
isCollapse: boolean | undefined;
|
||||
};
|
||||
|
||||
export default function NavbarAccount({ isCollapse }: Props) {
|
||||
return (
|
||||
<Link underline="none" color="inherit">
|
||||
<RootStyle
|
||||
sx={{
|
||||
...(isCollapse && {
|
||||
bgcolor: 'transparent',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src="https://minimal-assets-api.vercel.app/assets/images/avatars/avatar_5.jpg"
|
||||
alt="Rayan Moran"
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
ml: 2,
|
||||
transition: (theme) =>
|
||||
theme.transitions.create('width', {
|
||||
duration: theme.transitions.duration.shorter,
|
||||
}),
|
||||
...(isCollapse && {
|
||||
ml: 0,
|
||||
width: 0,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" noWrap>
|
||||
Rayan Moran
|
||||
</Typography>
|
||||
<Typography variant="body2" noWrap sx={{ color: 'text.secondary' }}>
|
||||
user
|
||||
</Typography>
|
||||
</Box>
|
||||
</RootStyle>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// @mui
|
||||
import { Stack, Button, Typography } from '@mui/material';
|
||||
// assets
|
||||
import { DocIllustration } from '../../../assets';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export default function NavbarDocs() {
|
||||
return (
|
||||
<Stack
|
||||
spacing={3}
|
||||
sx={{ px: 5, pb: 5, mt: 10, width: 1, textAlign: 'center', display: 'block' }}
|
||||
>
|
||||
<DocIllustration sx={{ width: 1 }} />
|
||||
|
||||
<div>
|
||||
<Typography gutterBottom variant="subtitle1">
|
||||
Hi, Rayan Moran
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
Need help?
|
||||
<br /> Please check our docs
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<Button variant="contained">Documentation</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { memo } from 'react';
|
||||
// @mui
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Container, AppBar } from '@mui/material';
|
||||
// config
|
||||
import { HEADER } from '../../../config';
|
||||
// components
|
||||
import { NavSectionHorizontal } from '../../../components/nav-section';
|
||||
//
|
||||
import navConfig from './NavConfig';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const RootStyle = styled(AppBar)(({ theme }) => ({
|
||||
transition: theme.transitions.create('top', {
|
||||
easing: theme.transitions.easing.easeInOut,
|
||||
duration: theme.transitions.duration.shorter,
|
||||
}),
|
||||
width: '100%',
|
||||
position: 'fixed',
|
||||
zIndex: theme.zIndex.appBar,
|
||||
padding: theme.spacing(1, 0),
|
||||
boxShadow: theme.customShadows.z8,
|
||||
top: HEADER.DASHBOARD_DESKTOP_OFFSET_HEIGHT,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
}));
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
function NavbarHorizontal() {
|
||||
return (
|
||||
<RootStyle>
|
||||
<Container maxWidth={false}>
|
||||
<NavSectionHorizontal navConfig={navConfig} />
|
||||
</Container>
|
||||
</RootStyle>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(NavbarHorizontal);
|
||||
@@ -0,0 +1,145 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
// @mui
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import { Box, Stack, Drawer } from '@mui/material';
|
||||
// hooks
|
||||
import useResponsive from '../../../hooks/useResponsive';
|
||||
import useCollapseDrawer from '../../../hooks/useCollapseDrawer';
|
||||
// utils
|
||||
import cssStyles from '../../../utils/cssStyles';
|
||||
// config
|
||||
import { NAVBAR } from '../../../config';
|
||||
// components
|
||||
import Logo from '../../../components/Logo';
|
||||
import Scrollbar from '../../../components/Scrollbar';
|
||||
import { NavSectionVertical } from '../../../components/nav-section';
|
||||
//
|
||||
import navConfig from './NavConfig';
|
||||
import NavbarDocs from './NavbarDocs';
|
||||
import NavbarAccount from './NavbarAccount';
|
||||
import CollapseButton from './CollapseButton';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const RootStyle = styled('div')(({ theme }) => ({
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
flexShrink: 0,
|
||||
transition: theme.transitions.create('width', {
|
||||
duration: theme.transitions.duration.shorter,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
isOpenSidebar: boolean;
|
||||
onCloseSidebar: VoidFunction;
|
||||
};
|
||||
|
||||
export default function NavbarVertical({ isOpenSidebar, onCloseSidebar }: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const isDesktop = useResponsive('up', 'lg');
|
||||
|
||||
const { isCollapse, collapseClick, collapseHover, onToggleCollapse, onHoverEnter, onHoverLeave } =
|
||||
useCollapseDrawer();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpenSidebar) {
|
||||
onCloseSidebar();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname]);
|
||||
|
||||
const renderContent = (
|
||||
<Scrollbar
|
||||
sx={{
|
||||
height: 1,
|
||||
'& .simplebar-content': { height: 1, display: 'flex', flexDirection: 'column' },
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
spacing={3}
|
||||
sx={{
|
||||
pt: 3,
|
||||
pb: 2,
|
||||
px: 2.5,
|
||||
flexShrink: 0,
|
||||
...(isCollapse && { alignItems: 'center' }),
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
||||
<Logo />
|
||||
|
||||
{isDesktop && !isCollapse && (
|
||||
<CollapseButton onToggleCollapse={onToggleCollapse} collapseClick={collapseClick} />
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<NavbarAccount isCollapse={isCollapse} />
|
||||
</Stack>
|
||||
|
||||
<NavSectionVertical navConfig={navConfig} isCollapse={isCollapse} />
|
||||
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
{!isCollapse && <NavbarDocs />}
|
||||
</Scrollbar>
|
||||
);
|
||||
|
||||
return (
|
||||
<RootStyle
|
||||
sx={{
|
||||
width: {
|
||||
lg: isCollapse ? NAVBAR.DASHBOARD_COLLAPSE_WIDTH : NAVBAR.DASHBOARD_WIDTH,
|
||||
},
|
||||
...(collapseClick && {
|
||||
position: 'absolute',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{!isDesktop && (
|
||||
<Drawer
|
||||
open={isOpenSidebar}
|
||||
onClose={onCloseSidebar}
|
||||
PaperProps={{ sx: { width: NAVBAR.DASHBOARD_WIDTH } }}
|
||||
>
|
||||
{renderContent}
|
||||
</Drawer>
|
||||
)}
|
||||
|
||||
{isDesktop && (
|
||||
<Drawer
|
||||
open
|
||||
variant="persistent"
|
||||
onMouseEnter={onHoverEnter}
|
||||
onMouseLeave={onHoverLeave}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: NAVBAR.DASHBOARD_WIDTH,
|
||||
borderRightStyle: 'dashed',
|
||||
bgcolor: 'background.default',
|
||||
transition: (theme) =>
|
||||
theme.transitions.create('width', {
|
||||
duration: theme.transitions.duration.standard,
|
||||
}),
|
||||
...(isCollapse && {
|
||||
width: NAVBAR.DASHBOARD_COLLAPSE_WIDTH,
|
||||
}),
|
||||
...(collapseHover && {
|
||||
...cssStyles(theme).bgBlur(),
|
||||
boxShadow: (theme) => theme.customShadows.z24,
|
||||
}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{renderContent}
|
||||
</Drawer>
|
||||
)}
|
||||
</RootStyle>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user