First Commit
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { Activity, useEffect, useState } from 'react';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import MuiBreadcrumbs from '@mui/material/Breadcrumbs';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import RouterLink from '@/components/Link';
|
||||
import { APP_DEFAULT_PATH } from '@/config';
|
||||
import menuItems from '@/menu';
|
||||
import { useGetBreadcrumbsMaster } from '@/states/breadcrumbs';
|
||||
import { generateFocusStyle } from '@/utils/generateFocusStyle';
|
||||
import { usePathname } from '@/utils/navigation';
|
||||
|
||||
// @assets
|
||||
import { IconChevronRight } from '@tabler/icons-react';
|
||||
|
||||
// @data
|
||||
const homeBreadcrumb = { title: 'Home', url: APP_DEFAULT_PATH };
|
||||
|
||||
/*************************** BREADCRUMBS ***************************/
|
||||
|
||||
export default function Breadcrumbs() {
|
||||
const theme = useTheme();
|
||||
const location = usePathname();
|
||||
const { breadcrumbsMaster } = useGetBreadcrumbsMaster();
|
||||
|
||||
const [breadcrumbItems, setBreadcrumbItems] = useState([]);
|
||||
const [activeItem, setActiveItem] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
if (breadcrumbsMaster && breadcrumbsMaster.data?.length && breadcrumbsMaster.activePath === location) {
|
||||
dataHandler(breadcrumbsMaster.data);
|
||||
} else {
|
||||
for (const menu of menuItems?.items) {
|
||||
if (menu.type && menu.type === 'group') {
|
||||
const matchedParents = findParentElements(menu.children || [], location);
|
||||
dataHandler(matchedParents || []);
|
||||
if (matchedParents) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [breadcrumbsMaster, location]);
|
||||
|
||||
const dataHandler = (data) => {
|
||||
const active = data.at(-1);
|
||||
const linkItems = data.slice(0, -1);
|
||||
if (active && active.url != homeBreadcrumb.url) {
|
||||
linkItems.unshift(homeBreadcrumb);
|
||||
}
|
||||
setActiveItem(active);
|
||||
setBreadcrumbItems(linkItems);
|
||||
};
|
||||
|
||||
function findParentElements(navItems, targetUrl, parents = []) {
|
||||
for (const item of navItems) {
|
||||
// Add the current item to the parents array
|
||||
const newParents = [...parents, item];
|
||||
|
||||
// Check if the current item matches the target URL
|
||||
if (item.url && targetUrl.includes(item.url)) {
|
||||
return newParents; // Return the array of parent elements
|
||||
}
|
||||
|
||||
// If the item has children, recurse into them
|
||||
if (item.children) {
|
||||
const result = findParentElements(item.children, targetUrl, newParents);
|
||||
if (result) {
|
||||
return result; // Return the result if found in children
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Return null if no match is found
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiBreadcrumbs aria-label="breadcrumb" separator={<IconChevronRight size={16} />}>
|
||||
{breadcrumbItems.length &&
|
||||
breadcrumbItems.map((item, index) => (
|
||||
<Typography
|
||||
{...(item.url && { component: RouterLink, to: item.url })}
|
||||
variant="body2"
|
||||
sx={{
|
||||
p: 0.5,
|
||||
color: 'grey.700',
|
||||
textDecoration: 'none',
|
||||
...(item.url && { cursor: 'pointer', ':hover': { color: 'primary.main' } }),
|
||||
':focus-visible': { outline: 'none', borderRadius: 0.25, ...generateFocusStyle(theme.vars.palette.primary.main) }
|
||||
}}
|
||||
key={index}
|
||||
>
|
||||
{item.title}
|
||||
</Typography>
|
||||
))}
|
||||
<Activity mode={activeItem ? 'visible' : 'hidden'}>
|
||||
<Typography variant="body2" sx={{ p: 0.5 }}>
|
||||
{activeItem?.title}
|
||||
</Typography>
|
||||
</Activity>
|
||||
</MuiBreadcrumbs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
/*************************** COMPONENTS WRAPPER ***************************/
|
||||
|
||||
export default function ComponentsWrapper({ children, title }) {
|
||||
return (
|
||||
<Stack sx={{ gap: { xs: 2, sm: 4 } }}>
|
||||
<Stack sx={{ py: 1.25, justifyContent: 'center' }}>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
</Stack>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
ComponentsWrapper.propTypes = { children: PropTypes.any, title: PropTypes.string };
|
||||
@@ -0,0 +1,186 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardMedia from '@mui/material/CardMedia';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @third-party
|
||||
import { Controller } from 'react-hook-form';
|
||||
|
||||
// @project
|
||||
import { contactSchema } from '@/utils/validation-schema/common';
|
||||
|
||||
// @icons
|
||||
import { IconChevronDown, IconHelp } from '@tabler/icons-react';
|
||||
|
||||
// @types
|
||||
|
||||
// @data
|
||||
import countries from '@/data/countries';
|
||||
|
||||
/*************************** CONTACT ***************************/
|
||||
|
||||
export default function Contact({
|
||||
dialCode,
|
||||
placeholder = 'ex. 9876x xxxxx',
|
||||
helpText,
|
||||
isDisabled = false,
|
||||
isCountryDisabled = false,
|
||||
fullWidth = false,
|
||||
control,
|
||||
isError = false,
|
||||
onCountryChange
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [selectedCountry, setSelectedCountry] = useState(countries[0]);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? 'dialcode-popper' : undefined;
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(anchorEl ? null : event.currentTarget);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const data = countries.find((item) => item.dialCode === (dialCode || '+1')) || countries[0];
|
||||
setSelectedCountry(data);
|
||||
if (!dialCode && onCountryChange) onCountryChange(data);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dialCode]);
|
||||
|
||||
const countryChange = (country) => {
|
||||
if (onCountryChange) {
|
||||
onCountryChange(country);
|
||||
}
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={'contact'}
|
||||
rules={contactSchema}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<OutlinedInput
|
||||
placeholder={placeholder}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
error={isError}
|
||||
{...(helpText && {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end" sx={{ '& svg': { cursor: 'default' } }}>
|
||||
<Tooltip title={helpText}>
|
||||
<IconHelp />
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
)
|
||||
})}
|
||||
disabled={isDisabled}
|
||||
aria-describedby="contact-field"
|
||||
slotProps={{ input: { 'aria-label': 'contact' } }}
|
||||
fullWidth={fullWidth}
|
||||
startAdornment={
|
||||
<>
|
||||
<Stack direction="row" sx={{ minHeight: 'inherit', height: 1, gap: 0.5, alignItems: 'center', mr: 0.75 }}>
|
||||
<Button
|
||||
endIcon={<IconChevronDown width={16} height={16} />}
|
||||
disabled={isDisabled || isCountryDisabled}
|
||||
color="secondary"
|
||||
sx={{
|
||||
...theme.typography.body2,
|
||||
height: 'auto',
|
||||
p: 0,
|
||||
borderRadius: 2,
|
||||
minWidth: 40,
|
||||
'&:hover': { bgcolor: 'transparent' },
|
||||
'&:before': { display: 'none' },
|
||||
'& .MuiInputBase-input:focus': { bgcolor: 'transparent' }
|
||||
}}
|
||||
disableRipple
|
||||
aria-describedby={id}
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{selectedCountry.countryCode}
|
||||
</Button>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
</Stack>
|
||||
<Popper
|
||||
placement="bottom-start"
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
transition
|
||||
popperOptions={{ modifiers: [{ name: 'offset', options: { offset: [-10, 11] } }] }}
|
||||
sx={{ zIndex: 1301 }}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Fade in={open} {...TransitionProps}>
|
||||
<Card elevation={0} sx={{ border: '1px solid', borderColor: theme.vars.palette.divider, borderRadius: 2 }}>
|
||||
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
|
||||
<Box sx={{ p: 0.5 }}>
|
||||
<List disablePadding>
|
||||
<Box style={{ maxHeight: 320, width: 280, overflow: 'auto' }}>
|
||||
{countries.map((country, index) => (
|
||||
<ListItemButton
|
||||
key={index}
|
||||
sx={{ borderRadius: 2, mb: 0.25 }}
|
||||
selected={country.dialCode === dialCode}
|
||||
onClick={() => countryChange(country)}
|
||||
>
|
||||
<ListItemAvatar sx={{ minWidth: 32 }}>
|
||||
<CardMedia
|
||||
image={`https://flagcdn.com/w20/${country.countryCode.toLowerCase()}.png`}
|
||||
component="img"
|
||||
sx={{ height: 'fit-content', width: 21 }}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={<Typography variant="body2">{country.name}</Typography>} />
|
||||
</ListItemButton>
|
||||
))}
|
||||
</Box>
|
||||
</List>
|
||||
</Box>
|
||||
</ClickAwayListener>
|
||||
</Card>
|
||||
</Fade>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Contact.propTypes = {
|
||||
dialCode: PropTypes.any,
|
||||
placeholder: PropTypes.string,
|
||||
helpText: PropTypes.any,
|
||||
isDisabled: PropTypes.bool,
|
||||
isCountryDisabled: PropTypes.bool,
|
||||
fullWidth: PropTypes.bool,
|
||||
control: PropTypes.any,
|
||||
isError: PropTypes.bool,
|
||||
onCountryChange: PropTypes.any
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @third-party
|
||||
import * as TablerIcons from '@tabler/icons-react';
|
||||
|
||||
/*************************** DYNAMIC - TABLER ICONS ***************************/
|
||||
|
||||
export default function DynamicIcon({ name, size = 24, color = 'black', stroke = 2 }) {
|
||||
// Dynamically get the icon component based on the `name` prop
|
||||
const IconComponent = TablerIcons[name];
|
||||
|
||||
// If the provided `name` does not match any icon in TablerIcons, return null to avoid rendering errors
|
||||
if (!IconComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <IconComponent {...{ size, color, stroke }} />;
|
||||
}
|
||||
|
||||
DynamicIcon.propTypes = { name: PropTypes.any, size: PropTypes.number, color: PropTypes.string, stroke: PropTypes.number };
|
||||
@@ -0,0 +1,50 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Container from '@mui/material/Container';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @assets
|
||||
import Error404 from '@/images/maintenance/Error404';
|
||||
|
||||
/*************************** ERROR 404 - PAGES ***************************/
|
||||
|
||||
export default function Error404Page({ primaryBtn, heading }) {
|
||||
const theme = useTheme();
|
||||
const upMD = useMediaQuery(theme.breakpoints.up('md'));
|
||||
const upXL = useMediaQuery(theme.breakpoints.up('xl'));
|
||||
const downMD = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const isDesktop = (upMD || upXL) && !downMD;
|
||||
|
||||
return (
|
||||
<Container {...(isDesktop && { maxWidth: upXL ? 'xl' : 'lg' })} sx={{ ...(downMD && { px: { xs: 2, sm: 4, md: 0 } }) }}>
|
||||
<Stack
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 1,
|
||||
height: '100vh',
|
||||
py: { xs: 4, sm: 5, md: 6 },
|
||||
minHeight: { xs: 450, sm: 600, md: 800 }
|
||||
}}
|
||||
>
|
||||
<Card sx={{ bgcolor: 'grey.100', borderRadius: { xs: 6, sm: 8, md: 10 }, width: 1, height: 1, boxShadow: 'none' }}>
|
||||
<Stack sx={{ justifyContent: 'center', height: 1, gap: { xs: 4, sm: 1 } }}>
|
||||
<Error404 />
|
||||
<Stack sx={{ gap: 2.25, alignItems: 'center', mt: { sm: -5, lg: -6.25 } }}>
|
||||
<Typography sx={{ width: { xs: 210, sm: 300 }, textAlign: 'center' }}>{heading}</Typography>
|
||||
{primaryBtn && <Button variant="contained" size="medium" {...primaryBtn} />}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
Error404Page.propTypes = { primaryBtn: PropTypes.any, heading: PropTypes.string };
|
||||
@@ -0,0 +1,53 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Container from '@mui/material/Container';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @assets
|
||||
import Error500 from '@/images/maintenance/Error500';
|
||||
import Error500Server from '@/images/maintenance/Error500Server';
|
||||
|
||||
/*************************** ERROR 500 - PAGES ***************************/
|
||||
|
||||
export default function Error500Page({ primaryBtn, heading }) {
|
||||
const theme = useTheme();
|
||||
const upMD = useMediaQuery(theme.breakpoints.up('md'));
|
||||
const upXL = useMediaQuery(theme.breakpoints.up('xl'));
|
||||
const downMD = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const isDesktop = (upMD || upXL) && !downMD;
|
||||
|
||||
return (
|
||||
<Container
|
||||
{...(isDesktop && { maxWidth: upXL ? 'xl' : 'lg' })}
|
||||
sx={{
|
||||
...(downMD && { px: { xs: 2, sm: 4, md: 0 } })
|
||||
}}
|
||||
>
|
||||
<Stack sx={{ width: 1, height: '100vh', py: { xs: 4, sm: 5, md: 6 }, minHeight: { xs: 450, sm: 600, md: 800 } }}>
|
||||
<Card
|
||||
sx={{ bgcolor: 'grey.100', borderRadius: { xs: 6, sm: 8, md: 10 }, width: 1, height: 1, position: 'relative', boxShadow: 'none' }}
|
||||
>
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center', gap: 2.25, height: '70%' }}>
|
||||
<Box sx={{ width: 1, maxWidth: { xs: 340, sm: 486, md: 728 }, p: 2 }}>
|
||||
<Error500 />
|
||||
</Box>
|
||||
<Typography sx={{ textAlign: 'center', width: { xs: 248, sm: 340, md: 448 } }}>{heading}</Typography>
|
||||
{primaryBtn && <Button variant="contained" size="medium" {...primaryBtn} sx={{ zIndex: 1 }} />}
|
||||
</Stack>
|
||||
<Box sx={{ width: { xs: '95%', md: '90%' }, position: 'absolute', left: -2, bottom: -6 }}>
|
||||
<Error500Server />
|
||||
</Box>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
Error500Page.propTypes = { primaryBtn: PropTypes.any, heading: PropTypes.string };
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { Link as MuiLink } from '@mui/material';
|
||||
|
||||
export const LinkComponent = ({ to, ...other }) => <RouterLink to={to} {...other} />;
|
||||
|
||||
const Link = ({ to, replace, ...other }) => {
|
||||
return <MuiLink component={RouterLink} to={to} replace={replace} {...other} />;
|
||||
};
|
||||
|
||||
export default Link;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Suspense } from 'react';
|
||||
|
||||
// @project
|
||||
import Loader from './Loader';
|
||||
|
||||
/*************************** LOADABLE - LAZY LOADING ***************************/
|
||||
|
||||
const Loadable = (Component) => (props) => {
|
||||
return (
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Component {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loadable;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Activity, useEffect, useState } from 'react';
|
||||
|
||||
// @mui
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
/*************************** LOADER ***************************/
|
||||
|
||||
export default function Loader() {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Activity mode={isClient ? 'visible' : 'hidden'}>
|
||||
<Box sx={{ position: 'fixed', top: 0, left: 0, zIndex: 2001, width: '100%' }}>
|
||||
<LinearProgress variant="indeterminate" color="primary" />
|
||||
</Box>
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import Card from '@mui/material/Card';
|
||||
|
||||
/*************************** MAIN CARD ***************************/
|
||||
|
||||
export default function MainCard({ children, sx = {}, ref, ...others }) {
|
||||
const defaultSx = (theme) => ({
|
||||
p: { xs: 1.75, sm: 2.25, md: 3 },
|
||||
border: `1px solid ${theme.vars.palette.divider}`,
|
||||
borderRadius: 4,
|
||||
boxShadow: theme.vars.customShadows.section
|
||||
});
|
||||
|
||||
const combinedSx = (theme) => ({
|
||||
...defaultSx(theme),
|
||||
...(typeof sx === 'function' ? sx(theme) : sx)
|
||||
});
|
||||
|
||||
return (
|
||||
<Card ref={ref} elevation={0} sx={combinedSx} {...others}>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
MainCard.propTypes = { children: PropTypes.any, sx: PropTypes.object, ref: PropTypes.any, others: PropTypes.any };
|
||||
@@ -0,0 +1,74 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { isValidElement } from 'react';
|
||||
|
||||
// @mui
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @project
|
||||
import { AvatarSize } from '@/enum';
|
||||
|
||||
/*************************** NOTIFICATION - LIST ***************************/
|
||||
|
||||
export default function NotificationItem({ avatar, badgeAvatar, title, subTitle, dateTime, isSeen = false }) {
|
||||
const ellipsis = { textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' };
|
||||
|
||||
const avatarContent = isValidElement(avatar) ? <Avatar color="default">{avatar}</Avatar> : <Avatar {...avatar} />;
|
||||
|
||||
return (
|
||||
<Stack direction="row" sx={{ width: 1, alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||
<Stack direction="row" sx={{ alignItems: 'center', gap: 1.25, flexShrink: 0 }}>
|
||||
{badgeAvatar ? (
|
||||
// Box component for badge position due to parent Stack component
|
||||
<Box>
|
||||
<Badge
|
||||
overlap="circular"
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
badgeContent={
|
||||
<Avatar
|
||||
color="default"
|
||||
size={AvatarSize.BADGE}
|
||||
sx={{ border: `1px solid`, borderColor: 'common.white' }}
|
||||
{...badgeAvatar}
|
||||
/>
|
||||
}
|
||||
slotProps={{ badge: { sx: { bottom: '22%' } } }}
|
||||
>
|
||||
{avatarContent}
|
||||
</Badge>
|
||||
</Box>
|
||||
) : (
|
||||
avatarContent
|
||||
)}
|
||||
</Stack>
|
||||
{/* minWidth: 0 -> Critical to ensure ellipsis works */}
|
||||
<Stack sx={{ flexGrow: 1, minWidth: 0, maxWidth: 1, gap: 0.25 }}>
|
||||
<Typography variant={isSeen ? 'body2' : 'subtitle2'} {...(isSeen && { color: 'grey.700' })} noWrap sx={ellipsis}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subTitle && (
|
||||
<Typography variant="caption" color="text.secondary" noWrap sx={ellipsis}>
|
||||
{subTitle}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
{dateTime && (
|
||||
<Typography variant="caption" sx={{ marginLeft: 'auto', flexShrink: 0 }} {...(isSeen && { color: 'grey.700' })}>
|
||||
{dateTime}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
NotificationItem.propTypes = {
|
||||
avatar: PropTypes.any,
|
||||
badgeAvatar: PropTypes.any,
|
||||
title: PropTypes.any,
|
||||
subTitle: PropTypes.any,
|
||||
dateTime: PropTypes.any,
|
||||
isSeen: PropTypes.bool
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @types
|
||||
|
||||
// @icons
|
||||
import { IconPhoto } from '@tabler/icons-react';
|
||||
|
||||
/*************************** PROFILE ***************************/
|
||||
|
||||
export default function Profile({ avatar, title, caption, label, sx, titleProps, captionProps, placeholderIfEmpty }) {
|
||||
return (
|
||||
<Stack direction="row" sx={{ alignItems: 'center', justifyContent: 'space-between', gap: 0.75, width: 'fit-content', ...sx }}>
|
||||
{(avatar?.src || placeholderIfEmpty) && (
|
||||
<Avatar
|
||||
{...avatar}
|
||||
alt="profile"
|
||||
sx={{ ...avatar?.sx, ...(placeholderIfEmpty && { fontSize: 20, '& svg': { width: 26, height: 26 } }) }}
|
||||
>
|
||||
{!avatar?.src && placeholderIfEmpty && <IconPhoto stroke={1} />}
|
||||
</Avatar>
|
||||
)}
|
||||
<Stack sx={{ gap: 0.25 }}>
|
||||
<Stack direction="row" sx={{ alignItems: 'center', gap: 0.5 }}>
|
||||
<Typography variant="subtitle2" {...titleProps} sx={{ color: 'text.primary', whiteSpace: 'nowrap', ...titleProps?.sx }}>
|
||||
{title || (placeholderIfEmpty && 'N/A')}
|
||||
</Typography>
|
||||
{label}
|
||||
</Stack>
|
||||
<Typography variant="caption" {...captionProps} sx={{ color: 'grey.700', ...captionProps?.sx }}>
|
||||
{caption || (placeholderIfEmpty && '---')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Profile.propTypes = {
|
||||
avatar: PropTypes.any,
|
||||
title: PropTypes.any,
|
||||
caption: PropTypes.any,
|
||||
label: PropTypes.any,
|
||||
sx: PropTypes.any,
|
||||
titleProps: PropTypes.any,
|
||||
captionProps: PropTypes.any,
|
||||
placeholderIfEmpty: PropTypes.any
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import MainCard from '@/components/MainCard';
|
||||
|
||||
/*************************** CARD - OVERVIEW ***************************/
|
||||
|
||||
export default function OverviewCard({ title, value, chip, compare, cardProps }) {
|
||||
const chipDefaultProps = { color: 'success', variant: 'text', size: 'small' };
|
||||
|
||||
return (
|
||||
<MainCard {...cardProps}>
|
||||
<Stack sx={{ gap: { xs: 3, md: 4 } }}>
|
||||
<Typography variant="subtitle1">{title}</Typography>
|
||||
<Stack sx={{ gap: 0.5 }}>
|
||||
<Stack direction="row" sx={{ gap: 1, alignItems: 'center' }}>
|
||||
<Typography variant="h4">{value}</Typography>
|
||||
<Chip {...{ ...chipDefaultProps, ...chip }} />
|
||||
</Stack>
|
||||
<Typography variant="caption" color="grey.700">
|
||||
{compare}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
);
|
||||
}
|
||||
|
||||
OverviewCard.propTypes = {
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
chip: PropTypes.any,
|
||||
compare: PropTypes.string,
|
||||
cardProps: PropTypes.any
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import MainCard from '@/components/MainCard';
|
||||
|
||||
/*************************** PRESENTATION CARD ***************************/
|
||||
|
||||
export default function PresentationCard({ title, children }) {
|
||||
return (
|
||||
<MainCard>
|
||||
<Stack sx={{ gap: 3.25 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 400 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{children}
|
||||
</Stack>
|
||||
</MainCard>
|
||||
);
|
||||
}
|
||||
|
||||
PresentationCard.propTypes = { title: PropTypes.string, children: PropTypes.any };
|
||||
@@ -0,0 +1,24 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import { LinearProgressType } from '@/enum';
|
||||
|
||||
/*************************** CARD - PROGRESS ***************************/
|
||||
|
||||
export default function ProgressCard({ title, value, progress }) {
|
||||
return (
|
||||
<Stack sx={{ gap: 0.5 }}>
|
||||
<Stack direction="row" sx={{ alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">{title}</Typography>
|
||||
<Typography variant="subtitle1">{value}</Typography>
|
||||
</Stack>
|
||||
<LinearProgress variant="determinate" type={LinearProgressType.LIGHT} {...progress} aria-label="progress" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
ProgressCard.propTypes = { title: PropTypes.string, value: PropTypes.string, progress: PropTypes.any };
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// @mui
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @images
|
||||
import { ReadingSideDoodle } from '@/images/illustration';
|
||||
|
||||
/*************************** HEADER - EMPTY NOTIFICATION ***************************/
|
||||
|
||||
export default function EmptyNotification() {
|
||||
return (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center', height: 236, textAlign: 'center', gap: 1, p: 2 }}>
|
||||
<ReadingSideDoodle />
|
||||
<Typography variant="h6" sx={{ fontWeight: 400, maxWidth: 232 }}>
|
||||
Nothing to see here! You're all up to date.
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @assets
|
||||
import { DumpingDoodle } from '@/images/illustration';
|
||||
|
||||
/*************************** HEADER - EMPTY SEARCH ***************************/
|
||||
|
||||
export default function EmptySearch({ props, ref }) {
|
||||
return (
|
||||
<Stack ref={ref} {...props} sx={{ width: 1, alignItems: 'center', justifyContent: 'center', textAlign: 'center', gap: 1.5, p: 1.5 }}>
|
||||
<Box sx={{ width: 230, height: 170 }}>
|
||||
<DumpingDoodle />
|
||||
</Box>
|
||||
<Stack sx={{ gap: 0.5, width: 220 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 400 }}>
|
||||
No search Result
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
We have searched more than 120 result but didn’t found anything
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
EmptySearch.propTypes = { props: PropTypes.any, ref: PropTypes.object };
|
||||
@@ -0,0 +1,40 @@
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import CardMedia from '@mui/material/CardMedia';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @project
|
||||
import branding from '@/branding.json';
|
||||
|
||||
/*************************** LOGO - ICON ***************************/
|
||||
|
||||
export default function LogoIcon() {
|
||||
const theme = useTheme();
|
||||
const logoIconPath = branding.logo.logoIcon;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: 25, sm: 33, md: 40 },
|
||||
height: 1,
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
display: 'block',
|
||||
'& svg': { display: 'block' }
|
||||
}}
|
||||
>
|
||||
{logoIconPath ? (
|
||||
<CardMedia src={logoIconPath} component="img" alt="logo" sx={{ height: 1 }} />
|
||||
) : (
|
||||
<svg viewBox="0 0 37 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M29.0507 0.657088C32.9601 -1.47888 37.5881 1.90736 36.7407 6.28379L31.081 35.5123C31.0417 35.7758 30.9823 36.0375 30.9023 36.2952C30.7256 36.8969 30.4697 37.3981 30.1515 37.802L30.1236 37.8405C28.4079 40.1894 25.1144 40.7015 22.7675 38.9843C21.6036 38.1327 20.8911 36.8926 20.6777 35.5724L20.6789 35.5732C20.0277 33.124 20.9582 26.5495 25.8412 16.0258L27.7227 18.1367L30.214 7.96335C30.3258 7.50659 29.8291 7.14315 29.4282 7.3884L20.4986 12.8509L23.1853 14.0825C18.1195 19.426 11.0662 24.4251 6.06551 24.9519C4.81627 25.0835 3.32109 24.7555 2.15767 23.9042C-0.18924 22.187 -0.700904 18.8907 1.01484 16.5418L1.02814 16.5237L1.0433 16.5032C1.33101 16.0776 1.73015 15.6819 2.24875 15.3311C2.4702 15.1762 2.70184 15.0398 2.9413 14.9222L29.0507 0.657088ZM9.83615 35.6327C11.3428 36.7129 13.4456 36.3571 14.5329 34.8379C15.2554 33.8285 15.7862 30.5405 16.0612 28.4476C16.1668 27.6438 15.3569 27.0632 14.6305 27.4219C12.739 28.3558 9.79931 29.9167 9.07685 30.9261C7.98955 32.4453 8.3295 34.5525 9.83615 35.6327Z"
|
||||
fill={theme.vars.palette.primary.main}
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
|
||||
// @project
|
||||
import LogoMain from './LogoMain';
|
||||
import LogoIcon from './LogoIcon';
|
||||
import { APP_DEFAULT_PATH } from '@/config';
|
||||
import RouterLink from '@/components/Link';
|
||||
import { generateFocusStyle } from '@/utils/generateFocusStyle';
|
||||
|
||||
/*************************** MAIN - LOGO ***************************/
|
||||
|
||||
export default function LogoSection({ isIcon, sx, to }) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<RouterLink to={!to ? APP_DEFAULT_PATH : to}>
|
||||
<ButtonBase disableRipple sx={{ ...sx, '&:focus-visible': generateFocusStyle(theme.vars.palette.primary.main) }} aria-label="logo">
|
||||
{isIcon ? <LogoIcon /> : <LogoMain />}
|
||||
</ButtonBase>
|
||||
</RouterLink>
|
||||
);
|
||||
}
|
||||
|
||||
LogoSection.propTypes = { isIcon: PropTypes.bool, sx: PropTypes.any, to: PropTypes.string };
|
||||
@@ -0,0 +1,114 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import Zoom from '@mui/material/Zoom';
|
||||
|
||||
// @third-party
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
|
||||
// @project
|
||||
import { useGetSnackbar } from '@/states/snackbar';
|
||||
import Loader from '@/components/Loader';
|
||||
|
||||
// @assets
|
||||
import { IconAlertTriangle, IconBug, IconChecks, IconInfoCircle, IconSpeakerphone } from '@tabler/icons-react';
|
||||
|
||||
// custom styles
|
||||
const StyledSnackbarProvider = styled(SnackbarProvider)(({ theme }) => ({
|
||||
'&.notistack-MuiContent': {
|
||||
color: theme.vars.palette.background.default
|
||||
},
|
||||
'&.notistack-MuiContent-default': {
|
||||
backgroundColor: theme.vars.palette.primary.main
|
||||
},
|
||||
'&.notistack-MuiContent-error': {
|
||||
backgroundColor: theme.vars.palette.error.main
|
||||
},
|
||||
'&.notistack-MuiContent-success': {
|
||||
backgroundColor: theme.vars.palette.success.main
|
||||
},
|
||||
'&.notistack-MuiContent-info': {
|
||||
backgroundColor: theme.vars.palette.info.main
|
||||
},
|
||||
'&.notistack-MuiContent-warning': {
|
||||
backgroundColor: theme.vars.palette.warning.main
|
||||
},
|
||||
'& #notistack-snackbar': {
|
||||
gap: 8
|
||||
}
|
||||
}));
|
||||
|
||||
/*************************** SNACKBAR - ANIMATION ***************************/
|
||||
|
||||
function TransitionSlideLeft(props) {
|
||||
return <Slide {...props} direction="left" />;
|
||||
}
|
||||
|
||||
function TransitionSlideUp(props) {
|
||||
return <Slide {...props} direction="up" />;
|
||||
}
|
||||
|
||||
function TransitionSlideRight(props) {
|
||||
return <Slide {...props} direction="right" />;
|
||||
}
|
||||
|
||||
function TransitionSlideDown(props) {
|
||||
return <Slide {...props} direction="down" />;
|
||||
}
|
||||
|
||||
function GrowTransition(props) {
|
||||
return <Grow {...props} />;
|
||||
}
|
||||
|
||||
function ZoomTransition(props) {
|
||||
return <Zoom {...props} />;
|
||||
}
|
||||
|
||||
const animation = {
|
||||
SlideLeft: TransitionSlideLeft,
|
||||
SlideUp: TransitionSlideUp,
|
||||
SlideRight: TransitionSlideRight,
|
||||
SlideDown: TransitionSlideDown,
|
||||
Grow: GrowTransition,
|
||||
Zoom: ZoomTransition,
|
||||
Fade
|
||||
};
|
||||
|
||||
const iconSX = { fontSize: '1.15rem' };
|
||||
|
||||
/*************************** SNACKBAR - NOTISTACK ***************************/
|
||||
|
||||
export default function Notistack({ children }) {
|
||||
const { snackbar } = useGetSnackbar();
|
||||
|
||||
if (snackbar === undefined) return <Loader />;
|
||||
|
||||
return (
|
||||
<StyledSnackbarProvider
|
||||
maxSnack={snackbar.maxStack}
|
||||
dense={snackbar.dense}
|
||||
anchorOrigin={snackbar.anchorOrigin}
|
||||
TransitionComponent={animation[snackbar.transition]}
|
||||
iconVariant={
|
||||
snackbar.iconVariant === 'useemojis'
|
||||
? {
|
||||
default: <IconSpeakerphone style={iconSX} />,
|
||||
success: <IconChecks style={iconSX} />,
|
||||
error: <IconBug style={iconSX} />,
|
||||
warning: <IconAlertTriangle style={iconSX} />,
|
||||
info: <IconInfoCircle style={iconSX} />
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
hideIconVariant={snackbar.iconVariant === 'hide' ? true : false}
|
||||
>
|
||||
{children}
|
||||
</StyledSnackbarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Notistack.propTypes = { children: PropTypes.node };
|
||||
@@ -0,0 +1,50 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @third-party
|
||||
import MainSimpleBar from 'simplebar-react';
|
||||
import { BrowserView, MobileView } from 'react-device-detect';
|
||||
|
||||
// @project
|
||||
import { withAlpha } from '@/utils/colorUtils';
|
||||
|
||||
// root style
|
||||
const RootStyle = styled(BrowserView)({ flexGrow: 1, height: '100%', overflow: 'hidden' });
|
||||
|
||||
// scroll bar wrapper
|
||||
const SimpleBarStyle = styled(MainSimpleBar)(({ theme }) => ({
|
||||
maxHeight: '100%',
|
||||
'& .simplebar-scrollbar': {
|
||||
'&:before': {
|
||||
background: withAlpha(theme.vars.palette.grey[500], 0.48)
|
||||
},
|
||||
'&.simplebar-visible:before': { opacity: 1 }
|
||||
},
|
||||
'& .simplebar-track.simplebar-vertical': { width: 10 },
|
||||
'& .simplebar-track.simplebar-horizontal .simplebar-scrollbar': { height: 6 },
|
||||
'& .simplebar-mask': { zIndex: 'inherit' }
|
||||
}));
|
||||
|
||||
/*************************** SIMPLE SCROLL BAR ***************************/
|
||||
|
||||
export default function SimpleBar({ children, sx, ...other }) {
|
||||
return (
|
||||
<>
|
||||
<RootStyle>
|
||||
<SimpleBarStyle clickOnTrack={false} sx={sx} data-simplebar-direction="ltr" {...other}>
|
||||
{children}
|
||||
</SimpleBarStyle>
|
||||
</RootStyle>
|
||||
<MobileView>
|
||||
<Box sx={{ overflowX: 'auto', ...sx }} {...other}>
|
||||
{children}
|
||||
</Box>
|
||||
</MobileView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
SimpleBar.propTypes = { children: PropTypes.any, sx: PropTypes.any, other: PropTypes.any };
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
/*************************** CHART - LEGEND ***************************/
|
||||
|
||||
export default function Legend({ items, onToggle }) {
|
||||
return (
|
||||
<Stack direction="row" sx={{ justifyContent: 'flex-end', gap: 1.5 }}>
|
||||
{items.map((item) => (
|
||||
<Stack key={item.id} direction="row" sx={{ alignItems: 'center', gap: 0.5, cursor: 'pointer' }} onClick={() => onToggle(item.id)}>
|
||||
<Box sx={{ width: 15, height: 15, bgcolor: item.visible ? item.color : 'grey.600', borderRadius: '50%' }} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.label}
|
||||
</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Legend.propTypes = { items: PropTypes.object, onToggle: PropTypes.func };
|
||||
Reference in New Issue
Block a user