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