First Commit

This commit is contained in:
cesnimda
2026-03-21 11:55:27 +01:00
commit 2e8a29b4d0
1757 changed files with 166084 additions and 0 deletions
@@ -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 };
@@ -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&apos;re all up to date.
</Typography>
</Stack>
);
}
@@ -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 didnt 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 };
@@ -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 };