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,125 @@
import { ReactElement, forwardRef } from 'react';
import { NavLink as RouterLink } from 'react-router-dom';
// @mui
import { Box, Link } from '@mui/material';
// config
import { ICON } from '../../../config';
// type
import { NavItemProps } from '../type';
//
import Iconify from '../../Iconify';
import { ListItemStyle } from './style';
import { isExternalLink } from '..';
// ----------------------------------------------------------------------
export const NavItemRoot = forwardRef<HTMLButtonElement & HTMLAnchorElement, NavItemProps>(
({ item, active, open, onMouseEnter, onMouseLeave }, ref) => {
const { title, path, icon, children } = item;
if (children) {
return (
<ListItemStyle
ref={ref}
open={open}
activeRoot={active}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<NavItemContent icon={icon} title={title} children={children} />
</ListItemStyle>
);
}
return isExternalLink(path) ? (
<ListItemStyle component={Link} href={path} target="_blank" rel="noopener">
<NavItemContent icon={icon} title={title} children={children} />
</ListItemStyle>
) : (
<ListItemStyle component={RouterLink} to={path} activeRoot={active}>
<NavItemContent icon={icon} title={title} children={children} />
</ListItemStyle>
);
}
);
// ----------------------------------------------------------------------
export const NavItemSub = forwardRef<HTMLButtonElement & HTMLAnchorElement, NavItemProps>(
({ item, active, open, onMouseEnter, onMouseLeave }, ref) => {
const { title, path, icon, children } = item;
if (children) {
return (
<ListItemStyle
ref={ref}
subItem
disableRipple
open={open}
activeSub={active}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<NavItemContent icon={icon} title={title} children={children} subItem />
</ListItemStyle>
);
}
return isExternalLink(path) ? (
<ListItemStyle
subItem
href={path}
disableRipple
rel="noopener"
target="_blank"
component={Link}
>
<NavItemContent icon={icon} title={title} children={children} subItem />
</ListItemStyle>
) : (
<ListItemStyle disableRipple component={RouterLink} to={path} activeSub={active} subItem>
<NavItemContent icon={icon} title={title} children={children} subItem />
</ListItemStyle>
);
}
);
// ----------------------------------------------------------------------
type NavItemContentProps = {
title: string;
icon?: ReactElement;
children?: { title: string; path: string }[];
subItem?: boolean;
};
function NavItemContent({ icon, title, children, subItem }: NavItemContentProps) {
return (
<>
{icon && (
<Box
component="span"
sx={{
mr: 1,
width: ICON.NAVBAR_ITEM_HORIZONTAL,
height: ICON.NAVBAR_ITEM_HORIZONTAL,
'& svg': { width: '100%', height: '100%' },
}}
>
{icon}
</Box>
)}
{title}
{children && (
<Iconify
icon={subItem ? 'eva:chevron-right-fill' : 'eva:chevron-down-fill'}
sx={{
ml: 0.5,
width: ICON.NAVBAR_ITEM_HORIZONTAL,
height: ICON.NAVBAR_ITEM_HORIZONTAL,
}}
/>
)}
</>
);
}

View File

@@ -0,0 +1,131 @@
import { useState, useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
// type
import { NavListProps } from '../type';
//
import { NavItemRoot, NavItemSub } from './NavItem';
import { PaperStyle } from './style';
import { getActive } from '..';
// ----------------------------------------------------------------------
type NavListRootProps = {
list: NavListProps;
};
export function NavListRoot({ list }: NavListRootProps) {
const menuRef = useRef(null);
const { pathname } = useLocation();
const active = getActive(list.path, pathname);
const [open, setOpen] = useState(false);
const hasChildren = list.children;
useEffect(() => {
if (open) {
handleClose();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
if (hasChildren) {
return (
<>
<NavItemRoot
open={open}
item={list}
active={active}
ref={menuRef}
onMouseEnter={handleOpen}
onMouseLeave={handleClose}
/>
<PaperStyle
open={open}
anchorEl={menuRef.current}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
PaperProps={{
onMouseEnter: handleOpen,
onMouseLeave: handleClose,
}}
>
{(list.children || []).map((item) => (
<NavListSub key={item.title} list={item} />
))}
</PaperStyle>
</>
);
}
return <NavItemRoot item={list} active={active} />;
}
// ----------------------------------------------------------------------
type NavListSubProps = {
list: NavListProps;
};
function NavListSub({ list }: NavListSubProps) {
const menuRef = useRef(null);
const { pathname } = useLocation();
const active = getActive(list.path, pathname);
const [open, setOpen] = useState(false);
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const hasChildren = list.children;
if (hasChildren) {
return (
<>
<NavItemSub
ref={menuRef}
open={open}
item={list}
active={active}
onMouseEnter={handleOpen}
onMouseLeave={handleClose}
/>
<PaperStyle
open={open}
anchorEl={menuRef.current}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
PaperProps={{
onMouseEnter: handleOpen,
onMouseLeave: handleClose,
}}
>
{(list.children || []).map((item) => (
<NavListSub key={item.title} list={item} />
))}
</PaperStyle>
</>
);
}
return <NavItemSub item={list} active={active} />;
}

View File

@@ -0,0 +1,40 @@
import { memo } from 'react';
// @mui
import { Stack } from '@mui/material';
// type
import { NavSectionProps } from '../type';
//
import { NavListRoot } from './NavList';
// ----------------------------------------------------------------------
const hideScrollbar = {
msOverflowStyle: 'none',
scrollbarWidth: 'none',
overflowY: 'scroll',
'&::-webkit-scrollbar': {
display: 'none',
},
} as const;
function NavSectionHorizontal({ navConfig }: NavSectionProps) {
return (
<Stack
direction="row"
justifyContent="center"
sx={{ bgcolor: 'background.neutral', borderRadius: 1, px: 0.5 }}
>
<Stack direction="row" sx={{ ...hideScrollbar, py: 1 }}>
{navConfig.map((group) => (
<Stack key={group.subheader} direction="row" flexShrink={0}>
{group.items.map((list) => (
<NavListRoot key={list.title} list={list} />
))}
</Stack>
))}
</Stack>
</Stack>
);
}
export default memo(NavSectionHorizontal);

View File

@@ -0,0 +1,85 @@
import { ReactNode } from 'react';
// @mui
import { alpha, styled } from '@mui/material/styles';
import { Button, Popover, ButtonProps, LinkProps } from '@mui/material';
// config
import { NAVBAR } from '../../../config';
// ----------------------------------------------------------------------
type IProps = LinkProps & ButtonProps;
interface ListItemStyleProps extends IProps {
component?: ReactNode;
to?: string;
activeRoot?: boolean;
activeSub?: boolean;
subItem?: boolean;
open?: boolean;
}
export const ListItemStyle = styled(Button, {
shouldForwardProp: (prop) =>
prop !== 'activeRoot' && prop !== 'activeSub' && prop !== 'subItem' && prop !== 'open',
})<ListItemStyleProps>(({ activeRoot, activeSub, subItem, open, theme }) => {
const isLight = theme.palette.mode === 'light';
const activeRootStyle = {
color: theme.palette.grey[800],
backgroundColor: theme.palette.common.white,
boxShadow: `-2px 4px 6px 0 ${alpha(
isLight ? theme.palette.grey[500] : theme.palette.common.black,
0.16
)}`,
};
return {
...theme.typography.body2,
margin: theme.spacing(0, 0.5),
padding: theme.spacing(0, 1),
color: theme.palette.text.secondary,
height: NAVBAR.DASHBOARD_ITEM_HORIZONTAL_HEIGHT,
'&:hover': {
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.paper,
},
// activeRoot
...(activeRoot && {
...theme.typography.subtitle2,
...activeRootStyle,
'&:hover': { ...activeRootStyle },
}),
// activeSub
...(activeSub && {
...theme.typography.subtitle2,
color: theme.palette.text.primary,
}),
// subItem
...(subItem && {
width: '100%',
margin: 0,
paddingRight: 0,
paddingLeft: theme.spacing(1),
justifyContent: 'space-between',
}),
// open
...(open &&
!activeRoot && {
color: theme.palette.text.primary,
backgroundColor: theme.palette.action.hover,
}),
};
});
// ----------------------------------------------------------------------
export const PaperStyle = styled(Popover)(({ theme }) => ({
pointerEvents: 'none',
'& .MuiPopover-paper': {
width: 160,
pointerEvents: 'auto',
padding: theme.spacing(1),
borderRadius: Number(theme.shape.borderRadius) * 1.5,
boxShadow: theme.customShadows.dropdown,
},
}));

View File

@@ -0,0 +1,14 @@
import { matchPath } from 'react-router-dom';
// ----------------------------------------------------------------------
export { default as NavSectionVertical } from './vertical';
export { default as NavSectionHorizontal } from './horizontal';
export function isExternalLink(path: string) {
return path.includes('http');
}
export function getActive(path: string, pathname: string) {
return path ? !!matchPath({ path: path, end: false }, pathname) : false;
}

View File

@@ -0,0 +1,34 @@
import { ReactElement } from 'react';
import { BoxProps } from '@mui/material';
// ----------------------------------------------------------------------
export type NavListProps = {
title: string;
path: string;
icon?: ReactElement;
info?: ReactElement;
children?: {
title: string;
path: string;
children?: { title: string; path: string }[];
}[];
};
export type NavItemProps = {
item: NavListProps;
isCollapse?: boolean;
active?: boolean | undefined;
open?: boolean;
onOpen?: VoidFunction;
onMouseEnter?: VoidFunction;
onMouseLeave?: VoidFunction;
};
export interface NavSectionProps extends BoxProps {
isCollapse?: boolean;
navConfig: {
subheader: string;
items: NavListProps[];
}[];
}

View File

@@ -0,0 +1,126 @@
import { NavLink as RouterLink } from 'react-router-dom';
// @mui
import { Box, Link, ListItemText } from '@mui/material';
// type
import { NavItemProps } from '../type';
//
import Iconify from '../../Iconify';
import { ListItemStyle, ListItemTextStyle, ListItemIconStyle } from './style';
import { isExternalLink } from '..';
// ----------------------------------------------------------------------
export function NavItemRoot({ item, isCollapse, open = false, active, onOpen }: NavItemProps) {
const { title, path, icon, info, children } = item;
const renderContent = (
<>
{icon && <ListItemIconStyle>{icon}</ListItemIconStyle>}
<ListItemTextStyle disableTypography primary={title} isCollapse={isCollapse} />
{!isCollapse && (
<>
{info && info}
{children && <ArrowIcon open={open} />}
</>
)}
</>
);
if (children) {
return (
<ListItemStyle onClick={onOpen} activeRoot={active}>
{renderContent}
</ListItemStyle>
);
}
return isExternalLink(path) ? (
<ListItemStyle component={Link} href={path} target="_blank" rel="noopener">
{renderContent}
</ListItemStyle>
) : (
<ListItemStyle component={RouterLink} to={path} activeRoot={active}>
{renderContent}
</ListItemStyle>
);
}
// ----------------------------------------------------------------------
type NavItemSubProps = Omit<NavItemProps, 'isCollapse'>;
export function NavItemSub({ item, open = false, active = false, onOpen }: NavItemSubProps) {
const { title, path, info, children } = item;
const renderContent = (
<>
<DotIcon active={active} />
<ListItemText disableTypography primary={title} />
{info && info}
{children && <ArrowIcon open={open} />}
</>
);
if (children) {
return (
<ListItemStyle onClick={onOpen} activeSub={active} subItem>
{renderContent}
</ListItemStyle>
);
}
return isExternalLink(path) ? (
<ListItemStyle component={Link} href={path} target="_blank" rel="noopener" subItem>
{renderContent}
</ListItemStyle>
) : (
<ListItemStyle component={RouterLink} to={path} activeSub={active} subItem>
{renderContent}
</ListItemStyle>
);
}
// ----------------------------------------------------------------------
type DotIconProps = {
active: boolean;
};
export function DotIcon({ active }: DotIconProps) {
return (
<ListItemIconStyle>
<Box
component="span"
sx={{
width: 4,
height: 4,
borderRadius: '50%',
bgcolor: 'text.disabled',
transition: (theme) =>
theme.transitions.create('transform', {
duration: theme.transitions.duration.shorter,
}),
...(active && {
transform: 'scale(2)',
bgcolor: 'primary.main',
}),
}}
/>
</ListItemIconStyle>
);
}
// ----------------------------------------------------------------------
type ArrowIconProps = {
open: boolean;
};
export function ArrowIcon({ open }: ArrowIconProps) {
return (
<Iconify
icon={open ? 'eva:arrow-ios-downward-fill' : 'eva:arrow-ios-forward-fill'}
sx={{ width: 16, height: 16, ml: 1 }}
/>
);
}

View File

@@ -0,0 +1,86 @@
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
// @mui
import { List, Collapse } from '@mui/material';
// type
import { NavListProps } from '../type';
//
import { NavItemRoot, NavItemSub } from './NavItem';
import { getActive } from '..';
// ----------------------------------------------------------------------
type NavListRootProps = {
list: NavListProps;
isCollapse: boolean;
};
export function NavListRoot({ list, isCollapse }: NavListRootProps) {
const { pathname } = useLocation();
const active = getActive(list.path, pathname);
const [open, setOpen] = useState(active);
const hasChildren = list.children;
if (hasChildren) {
return (
<>
<NavItemRoot
item={list}
isCollapse={isCollapse}
active={active}
open={open}
onOpen={() => setOpen(!open)}
/>
{!isCollapse && (
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{(list.children || []).map((item) => (
<NavListSub key={item.title} list={item} />
))}
</List>
</Collapse>
)}
</>
);
}
return <NavItemRoot item={list} active={active} isCollapse={isCollapse} />;
}
// ----------------------------------------------------------------------
type NavListSubProps = {
list: NavListProps;
};
function NavListSub({ list }: NavListSubProps) {
const { pathname } = useLocation();
const active = getActive(list.path, pathname);
const [open, setOpen] = useState(active);
const hasChildren = list.children;
if (hasChildren) {
return (
<>
<NavItemSub item={list} onOpen={() => setOpen(!open)} open={open} active={active} />
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding sx={{ pl: 3 }}>
{(list.children || []).map((item) => (
<NavItemSub key={item.title} item={item} active={getActive(item.path, pathname)} />
))}
</List>
</Collapse>
</>
);
}
return <NavItemSub item={list} active={active} />;
}

View File

@@ -0,0 +1,52 @@
// @mui
import { styled } from '@mui/material/styles';
import { List, Box, ListSubheader } from '@mui/material';
// type
import { NavSectionProps } from '../type';
//
import { NavListRoot } from './NavList';
// ----------------------------------------------------------------------
export const ListSubheaderStyle = styled((props) => (
<ListSubheader disableSticky disableGutters {...props} />
))(({ theme }) => ({
...theme.typography.overline,
paddingTop: theme.spacing(3),
paddingLeft: theme.spacing(2),
paddingBottom: theme.spacing(1),
color: theme.palette.text.primary,
transition: theme.transitions.create('opacity', {
duration: theme.transitions.duration.shorter,
}),
}));
// ----------------------------------------------------------------------
export default function NavSectionVertical({
navConfig,
isCollapse = false,
...other
}: NavSectionProps) {
return (
<Box {...other}>
{navConfig.map((group) => (
<List key={group.subheader} disablePadding sx={{ px: 2 }}>
<ListSubheaderStyle
sx={{
...(isCollapse && {
opacity: 0,
}),
}}
>
{group.subheader}
</ListSubheaderStyle>
{group.items.map((list) => (
<NavListRoot key={list.title} list={list} isCollapse={isCollapse} />
))}
</List>
))}
</Box>
);
}

View File

@@ -0,0 +1,79 @@
import { ReactNode } from 'react';
// @mui
import { alpha, styled } from '@mui/material/styles';
import {
LinkProps,
ListItemText,
ListItemButton,
ListItemIcon,
ListItemButtonProps,
} from '@mui/material';
// config
import { ICON, NAVBAR } from '../../../config';
// ----------------------------------------------------------------------
type IProps = LinkProps & ListItemButtonProps;
interface ListItemStyleProps extends IProps {
component?: ReactNode;
to?: string;
activeRoot?: boolean;
activeSub?: boolean;
subItem?: boolean;
}
export const ListItemStyle = styled(ListItemButton, {
shouldForwardProp: (prop) => prop !== 'activeRoot' && prop !== 'activeSub' && prop !== 'subItem',
})<ListItemStyleProps>(({ activeRoot, activeSub, subItem, theme }) => ({
...theme.typography.body2,
position: 'relative',
height: NAVBAR.DASHBOARD_ITEM_ROOT_HEIGHT,
textTransform: 'capitalize',
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1.5),
marginBottom: theme.spacing(0.5),
color: theme.palette.text.secondary,
borderRadius: theme.shape.borderRadius,
// activeRoot
...(activeRoot && {
...theme.typography.subtitle2,
color: theme.palette.primary.main,
backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity),
}),
// activeSub
...(activeSub && {
...theme.typography.subtitle2,
color: theme.palette.text.primary,
}),
// subItem
...(subItem && {
height: NAVBAR.DASHBOARD_ITEM_SUB_HEIGHT,
}),
}));
interface ListItemTextStyleProps extends ListItemButtonProps {
isCollapse?: boolean;
}
export const ListItemTextStyle = styled(ListItemText, {
shouldForwardProp: (prop) => prop !== 'isCollapse',
})<ListItemTextStyleProps>(({ isCollapse, theme }) => ({
whiteSpace: 'nowrap',
transition: theme.transitions.create(['width', 'opacity'], {
duration: theme.transitions.duration.shorter,
}),
...(isCollapse && {
width: 0,
opacity: 0,
}),
}));
export const ListItemIconStyle = styled(ListItemIcon)({
width: ICON.NAVBAR_ITEM,
height: ICON.NAVBAR_ITEM,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'& svg': { width: '100%', height: '100%' },
});