Separate Client Portal & Dashboard

This commit is contained in:
2022-05-23 10:38:16 +07:00
parent f2e84e6244
commit 89bb57f357
569 changed files with 60252 additions and 280 deletions

View 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 />
</>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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' }}>
&nbsp; {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,
};
}

View File

@@ -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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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>
);
}