initial commit

This commit is contained in:
2026-05-13 17:48:36 +05:30
commit 5a80256856
305 changed files with 80994 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
import PropTypes from 'prop-types';
// third-party
import { motion, useCycle } from 'framer-motion';
// ==============================|| ANIMATION BUTTON ||============================== //
export default function AnimateButton({
children,
type = 'scale',
direction = 'right',
offset = 10,
scale = {
hover: 1.05,
tap: 0.95
}
}) {
let offset1;
let offset2;
switch (direction) {
case 'up':
case 'left':
offset1 = offset;
offset2 = 0;
break;
case 'right':
case 'down':
default:
offset1 = 0;
offset2 = offset;
break;
}
const [x, cycleX] = useCycle(offset1, offset2);
const [y, cycleY] = useCycle(offset1, offset2);
switch (type) {
case 'rotate':
return (
<motion.div
animate={{ rotate: 360 }}
transition={{
repeat: Infinity,
repeatType: 'loop',
duration: 2,
repeatDelay: 0
}}
>
{children}
</motion.div>
);
case 'slide':
if (direction === 'up' || direction === 'down') {
return (
<motion.div animate={{ y: y !== undefined ? y : '' }} onHoverEnd={() => cycleY()} onHoverStart={() => cycleY()}>
{children}
</motion.div>
);
}
return (
<motion.div animate={{ x: x !== undefined ? x : '' }} onHoverEnd={() => cycleX()} onHoverStart={() => cycleX()}>
{children}
</motion.div>
);
case 'scale':
default:
if (typeof scale === 'number') {
scale = {
hover: scale,
tap: scale
};
}
return (
<motion.div whileHover={{ scale: scale?.hover }} whileTap={{ scale: scale?.tap }}>
{children}
</motion.div>
);
}
}
AnimateButton.propTypes = {
children: PropTypes.node,
offset: PropTypes.number,
type: PropTypes.oneOf(['slide', 'scale', 'rotate']),
direction: PropTypes.oneOf(['up', 'down', 'left', 'right']),
scale: PropTypes.oneOfType([PropTypes.number, PropTypes.object])
};

View File

@@ -0,0 +1,119 @@
import PropTypes from 'prop-types';
// material-ui
import { styled, useTheme } from '@mui/material/styles';
import MuiAvatar from '@mui/material/Avatar';
// project import
import getColors from 'utils/getColors';
// ==============================|| AVATAR - COLOR STYLE ||============================== //
function getColorStyle({ theme, color, type }) {
const colors = getColors(theme, color);
const { lighter, light, main, contrastText } = colors;
switch (type) {
case 'filled':
return {
color: contrastText,
backgroundColor: main
};
case 'outlined':
return {
color: main,
border: '1px solid',
borderColor: main,
backgroundColor: 'transparent'
};
case 'combined':
return {
color: main,
border: '1px solid',
borderColor: light,
backgroundColor: lighter
};
default:
return {
color: main,
backgroundColor: lighter
};
}
}
// ==============================|| AVATAR - SIZE STYLE ||============================== //
function getSizeStyle(size) {
switch (size) {
case 'badge':
return {
border: '2px solid',
fontSize: '0.675rem',
width: 20,
height: 20
};
case 'xs':
return {
fontSize: '0.75rem',
width: 24,
height: 24
};
case 'sm':
return {
fontSize: '0.875rem',
width: 32,
height: 32
};
case 'lg':
return {
fontSize: '1.2rem',
width: 52,
height: 52
};
case 'xl':
return {
fontSize: '1.5rem',
width: 64,
height: 64
};
case 'md':
default:
return {
fontSize: '1rem',
width: 40,
height: 40
};
}
}
// ==============================|| STYLED - AVATAR ||============================== //
const AvatarStyle = styled(MuiAvatar, { shouldForwardProp: (prop) => prop !== 'color' && prop !== 'type' && prop !== 'size' })(
({ theme, variant, color, type, size }) => ({
...getSizeStyle(size),
...getColorStyle({ variant, theme, color, type }),
...(size === 'badge' && {
borderColor: theme.palette.background.default
})
})
);
// ==============================|| EXTENDED - AVATAR ||============================== //
export default function Avatar({ variant = 'circular', children, color = 'primary', type, size = 'md', ...others }) {
const theme = useTheme();
return (
<AvatarStyle variant={variant} theme={theme} color={color} type={type} size={size} {...others}>
{children}
</AvatarStyle>
);
}
Avatar.propTypes = {
children: PropTypes.node,
color: PropTypes.string,
type: PropTypes.string,
size: PropTypes.string,
variant: PropTypes.string
};

View File

@@ -0,0 +1,222 @@
import PropTypes from 'prop-types';
import { useEffect, useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
// material-ui
import MuiBreadcrumbs from '@mui/material/Breadcrumbs';
import { useTheme } from '@mui/material/styles';
import { Divider, Grid, Typography } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// assets
import { ApartmentOutlined, HomeFilled, HomeOutlined } from '@ant-design/icons';
// ==============================|| BREADCRUMBS ||============================== //
const Breadcrumbs = ({
card,
divider = true,
icon,
icons,
maxItems,
navigation,
rightAlign,
separator,
title,
titleBottom,
sx,
...others
}) => {
const theme = useTheme();
const location = useLocation();
const [main, setMain] = useState();
const [item, setItem] = useState();
let currentPath = location.pathname;
// only used for component demo breadcrumbs
if (currentPath.includes('/components-overview/breadcrumbs')) {
currentPath = '/apps/kanban/board';
}
if (currentPath.includes('/apps/kanban/backlogs')) {
currentPath = '/apps/kanban/board';
}
useEffect(() => {
if (currentPath.includes('/apps/profiles/user/payment')) {
setItem(undefined);
}
}, [item, currentPath]);
const iconSX = {
marginRight: theme.spacing(0.75),
marginTop: `-${theme.spacing(0.25)}`,
width: '1rem',
height: '1rem',
color: theme.palette.secondary.main
};
// set active item state
const getCollapse = (menu) => {
if (menu.children) {
menu.children.filter((collapse) => {
if (collapse.type && collapse.type === 'collapse') {
getCollapse(collapse);
if (collapse.url === currentPath) {
setMain(collapse);
setItem(collapse);
}
} else if (collapse.type && collapse.type === 'item') {
if (currentPath.includes(collapse.url)) {
setMain(menu);
setItem(collapse);
}
}
return false;
});
}
};
useEffect(() => {
navigation?.items?.map((menu) => {
if (menu.type && menu.type === 'group') {
getCollapse(menu);
}
return false;
});
});
// item separator
const SeparatorIcon = separator;
const separatorIcon = separator ? <SeparatorIcon style={{ fontSize: '0.75rem', marginTop: 2 }} /> : '/';
let mainContent;
let itemContent;
let breadcrumbContent = <Typography />;
let itemTitle = '';
let CollapseIcon;
let ItemIcon;
// collapse item
if (main && main.type === 'collapse' && main.breadcrumbs === true) {
CollapseIcon = main.icon ? main.icon : ApartmentOutlined;
mainContent = (
<Typography component={Link} to={document.location.pathname} variant="h6" sx={{ textDecoration: 'none' }} color="textSecondary">
{icons && <CollapseIcon style={iconSX} />}
{main.title}
</Typography>
);
breadcrumbContent = (
<MainCard
border={card}
sx={card === false ? { mb: 3, bgcolor: 'transparent', ...sx } : { mb: 3, ...sx }}
{...others}
content={card}
shadow="none"
>
<Grid
container
direction={rightAlign ? 'row' : 'column'}
justifyContent={rightAlign ? 'space-between' : 'flex-start'}
alignItems={rightAlign ? 'center' : 'flex-start'}
spacing={1}
>
<Grid item>
<MuiBreadcrumbs aria-label="breadcrumb" maxItems={maxItems || 8} separator={separatorIcon}>
<Typography component={Link} to="/" color="textSecondary" variant="h6" sx={{ textDecoration: 'none' }}>
{icons && <HomeOutlined style={iconSX} />}
{icon && !icons && <HomeFilled style={{ ...iconSX, marginRight: 0 }} />}
{(!icon || icons) && 'Home'}
</Typography>
{mainContent}
</MuiBreadcrumbs>
</Grid>
{title && titleBottom && (
<Grid item sx={{ mt: card === false ? 0.25 : 1 }}>
<Typography variant="h2">{main.title}</Typography>
</Grid>
)}
</Grid>
{card === false && divider !== false && <Divider sx={{ mt: 2 }} />}
</MainCard>
);
}
// items
if (item && item.type === 'item') {
itemTitle = item.title;
ItemIcon = item.icon ? item.icon : ApartmentOutlined;
itemContent = (
<Typography variant="subtitle1" color="textPrimary">
{icons && <ItemIcon style={iconSX} />}
{itemTitle}
</Typography>
);
// main
if (item.breadcrumbs !== false) {
breadcrumbContent = (
<MainCard
border={card}
sx={card === false ? { mb: 3, bgcolor: 'transparent', ...sx } : { mb: 3, ...sx }}
{...others}
content={card}
shadow="none"
>
<Grid
container
direction={rightAlign ? 'row' : 'column'}
justifyContent={rightAlign ? 'space-between' : 'flex-start'}
alignItems={rightAlign ? 'center' : 'flex-start'}
spacing={1}
>
{title && !titleBottom && (
<Grid item>
<Typography variant="h2">{item.title}</Typography>
</Grid>
)}
<Grid item>
<MuiBreadcrumbs aria-label="breadcrumb" maxItems={maxItems || 8} separator={separatorIcon}>
<Typography component={Link} to="/" color="textSecondary" variant="h6" sx={{ textDecoration: 'none' }}>
{icons && <HomeOutlined style={iconSX} />}
{icon && !icons && <HomeFilled style={{ ...iconSX, marginRight: 0 }} />}
{(!icon || icons) && 'Home'}
</Typography>
{mainContent}
{itemContent}
</MuiBreadcrumbs>
</Grid>
{title && titleBottom && (
<Grid item sx={{ mt: card === false ? 0.25 : 1 }}>
<Typography variant="h2">{item.title}</Typography>
</Grid>
)}
</Grid>
{card === false && divider !== false && <Divider sx={{ mt: 2 }} />}
</MainCard>
);
}
}
return breadcrumbContent;
};
Breadcrumbs.propTypes = {
card: PropTypes.bool,
divider: PropTypes.bool,
icon: PropTypes.bool,
icons: PropTypes.bool,
maxItems: PropTypes.number,
navigation: PropTypes.object,
rightAlign: PropTypes.bool,
separator: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
title: PropTypes.bool,
titleBottom: PropTypes.bool,
sx: PropTypes.oneOfType([PropTypes.object, PropTypes.string])
};
export default Breadcrumbs;

View File

@@ -0,0 +1,39 @@
import PropTypes from 'prop-types';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box } from '@mui/material';
// project import
import getColors from 'utils/getColors';
const Dot = ({ color, size, variant, sx }) => {
const theme = useTheme();
const colors = getColors(theme, color || 'primary');
const { main } = colors;
return (
<Box
component="span"
sx={{
width: size || 8,
height: size || 8,
borderRadius: '50%',
bgcolor: variant === 'outlined' ? '' : main,
...(variant === 'outlined' && {
border: `1px solid ${main}`
}),
...sx
}}
/>
);
};
Dot.propTypes = {
color: PropTypes.string,
size: PropTypes.number,
variant: PropTypes.string,
sx: PropTypes.oneOfType([PropTypes.object, PropTypes.string])
};
export default Dot;

View File

@@ -0,0 +1,162 @@
import PropTypes from 'prop-types';
import { forwardRef } from 'react';
// material-ui
import MuiIconButton from '@mui/material/IconButton';
import { alpha, styled, useTheme } from '@mui/material/styles';
// project imports
import getColors from 'utils/getColors';
import getShadow from 'utils/getShadow';
// ==============================|| ICON BUTTON - COLOR STYLE ||============================== //
function getColorStyle({ variant, theme, color }) {
const colors = getColors(theme, color);
const { lighter, light, dark, main, contrastText } = colors;
const buttonShadow = `${color}Button`;
const shadows = getShadow(theme, buttonShadow);
const commonShadow = {
'&::after': {
boxShadow: `0 0 6px 6px ${alpha(main, 0.9)}`
},
'&:active::after': {
boxShadow: `0 0 0 0 ${alpha(main, 0.9)}`
},
'&:focus-visible': {
outline: `2px solid ${dark}`,
outlineOffset: 2
}
};
switch (variant) {
case 'contained':
return {
color: contrastText,
backgroundColor: main,
'&:hover': {
backgroundColor: dark
},
...commonShadow
};
case 'light':
return {
color: main,
backgroundColor: lighter,
'&:hover': {
backgroundColor: light
},
...commonShadow
};
case 'shadow':
return {
boxShadow: shadows,
color: contrastText,
backgroundColor: main,
'&:hover': {
boxShadow: 'none',
backgroundColor: dark
},
...commonShadow
};
case 'outlined':
return {
'&:hover': {
backgroundColor: 'transparent',
color: dark,
borderColor: dark
},
...commonShadow
};
case 'dashed':
return {
backgroundColor: lighter,
'&:hover': {
color: dark,
borderColor: dark
},
...commonShadow
};
case 'text':
default:
return {
'&:hover': {
color: dark,
backgroundColor: lighter
},
...commonShadow
};
}
}
// ==============================|| STYLED - ICON BUTTON ||============================== //
const IconButtonStyle = styled(MuiIconButton, { shouldForwardProp: (prop) => prop !== 'variant' && prop !== 'shape' })(
({ theme, variant, shape, color }) => ({
position: 'relative',
'::after': {
content: '""',
display: 'block',
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
borderRadius: shape === 'rounded' ? '50%' : 4,
opacity: 0,
transition: 'all 0.5s'
},
':active::after': {
position: 'absolute',
borderRadius: shape === 'rounded' ? '50%' : 4,
left: 0,
top: 0,
opacity: 1,
transition: '0s'
},
...(shape === 'rounded' && {
borderRadius: '50%'
}),
...(variant === 'outlined' && {
border: '1px solid',
borderColor: 'inherit'
}),
...(variant === 'dashed' && {
border: '1px dashed',
borderColor: 'inherit'
}),
...(variant !== 'text' && {
'&.Mui-disabled': {
backgroundColor: theme.palette.grey[200]
}
}),
...getColorStyle({ variant, theme, color })
})
);
// ==============================|| EXTENDED - ICON BUTTON ||============================== //
const IconButton = forwardRef(({ variant = 'text', shape = 'square', children, color = 'primary', ...others }, ref) => {
const theme = useTheme();
return (
<IconButtonStyle ref={ref} disableRipple variant={variant} shape={shape} theme={theme} color={color} {...others}>
{children}
</IconButtonStyle>
);
});
IconButton.propTypes = {
variant: PropTypes.string,
shape: PropTypes.string,
children: PropTypes.node,
color: PropTypes.string
};
IconButton.displayName = 'IconButton';
export default IconButton;

View File

@@ -0,0 +1,231 @@
import PropTypes from 'prop-types';
import { forwardRef } from 'react';
// material-ui
import MuiLoadingButton from '@mui/lab/LoadingButton';
import { alpha, styled, useTheme } from '@mui/material/styles';
// project imports
import getColors from 'utils/getColors';
import getShadow from 'utils/getShadow';
// ==============================|| LOADING BUTTON - COLOR STYLE ||============================== //
function getColorStyle({ variant, theme, color, loadingPosition }) {
const colors = getColors(theme, color);
const { lighter, main, dark, contrastText } = colors;
const buttonShadow = `${color}Button`;
const shadows = getShadow(theme, buttonShadow);
const loadingIndicator = {
'& .MuiLoadingButton-loadingIndicator': {
color: main
}
};
const loadingColor = {
...(loadingPosition &&
loadingPosition !== 'center' && {
color: main
})
};
const commonShadow = {
'&::after': {
boxShadow: `0 0 6px 6px ${alpha(main, 0.9)}`
},
'&:active::after': {
boxShadow: `0 0 0 0 ${alpha(main, 0.9)}`
},
'&:focus-visible': {
outline: `2px solid ${dark}`,
outlineOffset: 2
}
};
switch (variant) {
case 'contained':
return {
backgroundColor: main,
...(loadingPosition &&
loadingPosition !== 'center' && {
color: contrastText
}),
'& .MuiLoadingButton-loadingIndicator': {
color: contrastText
},
'&:hover': {
backgroundColor: dark,
color: contrastText
},
...commonShadow
};
case 'light':
return {
backgroundColor: main,
...(loadingPosition &&
loadingPosition !== 'center' && {
color: contrastText
}),
'& .MuiLoadingButton-loadingIndicator': {
color: contrastText
},
'&:hover': {
backgroundColor: dark,
color: contrastText
},
...commonShadow
};
case 'shadow':
return {
boxShadow: shadows,
backgroundColor: main,
...(loadingPosition &&
loadingPosition !== 'center' && {
color: contrastText
}),
'& .MuiLoadingButton-loadingIndicator': {
color: contrastText
},
'&:hover': {
boxShadow: 'none',
backgroundColor: dark,
color: contrastText
},
...commonShadow
};
case 'outlined':
return {
backgroundColor: 'transparent',
borderColor: main,
...loadingColor,
...loadingIndicator
};
case 'dashed':
return {
backgroundColor: lighter,
borderColor: main,
...loadingColor,
...loadingIndicator,
...commonShadow
};
case 'text':
default:
return {
color: main,
...loadingIndicator,
...commonShadow
};
}
}
// ==============================|| STYLED - LOADING BUTTON ||============================== //
const LoadingButtonStyle = styled(MuiLoadingButton, { shouldForwardProp: (prop) => prop !== 'shape' && prop !== 'variant' })(
({ theme, variant, shape, color, loading, loadingPosition }) => ({
'::after': {
content: '""',
display: 'block',
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
borderRadius: shape === 'rounded' ? '50%' : 4,
opacity: 0,
transition: 'all 0.5s'
},
':active::after': {
position: 'absolute',
borderRadius: shape === 'rounded' ? '50%' : 4,
left: 0,
top: 0,
opacity: 1,
transition: '0s'
},
...(variant === 'text' && {
...getColorStyle({ variant, theme, color, loadingPosition }),
'&.MuiButton-sizeMedium': {
height: 36
},
'&.MuiButton-sizeSmall': {
height: 30
},
'&.MuiButton-sizeLarge': {
height: 44
}
}),
...(shape && {
minWidth: 0,
'&.MuiButton-sizeMedium': {
width: 36,
height: 36
},
'&.MuiButton-sizeSmall': {
width: 30,
height: 30
},
'&.MuiButton-sizeLarge': {
width: 44,
height: 44
},
...(shape === 'rounded' && {
borderRadius: '50%'
})
}),
...(variant === 'outlined' && {
border: '1px solid'
}),
...(variant === 'dashed' && {
border: '1px dashed'
}),
...((variant === 'contained' || variant === 'shadow') &&
!loading && {
color: '#fff'
}),
...(variant !== 'text' && {
...getColorStyle({ variant, theme, color, loadingPosition })
}),
'&.Mui-disabled': {
...(variant !== 'text' && {
...getColorStyle({ variant, theme, color, loadingPosition })
})
}
})
);
// ==============================|| EXTENDED - LOADING BUTTON ||============================== //
const LoadingButton = forwardRef(({ variant = 'text', shape, children, color = 'primary', ...others }, ref) => {
const theme = useTheme();
return (
<LoadingButtonStyle
ref={ref}
variant={variant}
shape={shape}
theme={theme}
loadingPosition={others.loadingPosition}
loading={others.loading}
color={color}
{...others}
>
{children}
</LoadingButtonStyle>
);
});
LoadingButton.propTypes = {
variant: PropTypes.string,
shape: PropTypes.string,
children: PropTypes.node,
color: PropTypes.string
};
LoadingButton.displayName = 'LoadingButton';
export default LoadingButton;

View File

@@ -0,0 +1,128 @@
// material-ui
import { Alert, Button, Fade, Grow, Slide } from '@mui/material';
import MuiSnackbar from '@mui/material/Snackbar';
// project-import
import IconButton from './IconButton';
import { dispatch, useSelector } from 'store';
import { closeSnackbar } from 'store/reducers/snackbar';
// assets
import { CloseOutlined } from '@ant-design/icons';
// animation function
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} />;
}
// animation options
const animation = {
SlideLeft: TransitionSlideLeft,
SlideUp: TransitionSlideUp,
SlideRight: TransitionSlideRight,
SlideDown: TransitionSlideDown,
Grow: GrowTransition,
Fade
};
// ==============================|| SNACKBAR ||============================== //
const Snackbar = () => {
const snackbar = useSelector((state) => state.snackbar);
const { actionButton, anchorOrigin, alert, close, message, open, transition, variant } = snackbar;
const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
dispatch(closeSnackbar());
};
return (
<>
{/* default snackbar */}
{variant === 'default' && (
<MuiSnackbar
anchorOrigin={anchorOrigin}
open={open}
autoHideDuration={6000}
onClose={handleClose}
message={message}
TransitionComponent={animation[transition]}
action={
<>
<Button color="secondary" size="small" onClick={handleClose}>
UNDO
</Button>
<IconButton size="small" aria-label="close" color="inherit" onClick={handleClose} sx={{ mt: 0.25 }}>
<CloseOutlined />
</IconButton>
</>
}
/>
)}
{/* alert snackbar */}
{variant === 'alert' && (
<MuiSnackbar
TransitionComponent={animation[transition]}
anchorOrigin={anchorOrigin}
open={open}
autoHideDuration={6000}
onClose={handleClose}
>
<Alert
variant={alert.variant}
color={alert.color}
action={
<>
{actionButton !== false && (
<Button color={alert.color} size="small" onClick={handleClose}>
UNDO
</Button>
)}
{close !== false && (
<IconButton
sx={{ mt: 0.25 }}
size="small"
aria-label="close"
variant="contained"
color={alert.color}
onClick={handleClose}
>
<CloseOutlined />
</IconButton>
)}
</>
}
sx={{
...(alert.variant === 'outlined' && {
bgcolor: 'grey.0'
})
}}
>
{message}
</Alert>
</MuiSnackbar>
)}
</>
);
};
export default Snackbar;

View File

@@ -0,0 +1,67 @@
import PropTypes from 'prop-types';
// material-ui
import { styled, useTheme } from '@mui/material/styles';
import { Box, tooltipClasses, Tooltip as MuiTooltip } from '@mui/material';
// project import
import getColors from 'utils/getColors';
// ==============================|| TOOLTIP - VARIANT ||============================== //
function getVariantStyle({ color, theme, labelColor }) {
const colors = getColors(theme, color);
const { main, contrastText } = colors;
let colorValue = color ? color : '';
if (['primary', 'secondary', 'info', 'success', 'warning', 'error'].includes(colorValue)) {
return {
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: main,
color: labelColor ? labelColor : contrastText
},
[`& .${tooltipClasses.arrow}`]: {
color: main
}
};
} else {
return {
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: colorValue,
color: labelColor ? labelColor : contrastText,
boxShadow: theme.shadows[1]
},
[`& .${tooltipClasses.arrow}`]: {
color: colorValue
}
};
}
}
// ==============================|| STYLED - TOOLTIP COLOR ||============================== //
const TooltipStyle = styled(({ className, ...props }) => <MuiTooltip {...props} classes={{ popper: className }} />, {
shouldForwardProp: (prop) => prop !== 'color' && prop !== 'labelColor'
})(({ theme, color, labelColor }) => ({
...(color && getVariantStyle({ color, theme, labelColor }))
}));
// ==============================|| EXTENDED - TOOLTIP ||============================== //
export default function CustomTooltip({ children, arrow, labelColor = '', ...rest }) {
const theme = useTheme();
return (
<Box display="flex">
<TooltipStyle arrow={arrow} {...rest} theme={theme} labelColor={labelColor}>
{children}
</TooltipStyle>
</Box>
);
}
CustomTooltip.propTypes = {
children: PropTypes.element,
arrow: PropTypes.bool,
labelColor: PropTypes.string,
rest: PropTypes.array
};

View File

@@ -0,0 +1,116 @@
import PropTypes from 'prop-types';
import { forwardRef } from 'react';
// material-ui
import { Collapse, Fade, Box, Grow, Slide, Zoom } from '@mui/material';
// ==============================|| TRANSITIONS ||============================== //
const Transitions = forwardRef(({ children, position = 'top-left', type = 'grow', direction = 'up', ...others }, ref) => {
let positionSX = {
transformOrigin: '0 0 0'
};
switch (position) {
case 'top-right':
positionSX = {
transformOrigin: 'top right'
};
break;
case 'top':
positionSX = {
transformOrigin: 'top'
};
break;
case 'bottom-left':
positionSX = {
transformOrigin: 'bottom left'
};
break;
case 'bottom-right':
positionSX = {
transformOrigin: 'bottom right'
};
break;
case 'bottom':
positionSX = {
transformOrigin: 'bottom'
};
break;
case 'top-left':
default:
positionSX = {
transformOrigin: '0 0 0'
};
break;
}
return (
<Box ref={ref}>
{type === 'grow' && (
<Grow
{...others}
timeout={{
appear: 0,
enter: 150,
exit: 150
}}
>
<Box sx={positionSX}>{children}</Box>
</Grow>
)}
{type === 'collapse' && (
<Collapse {...others} sx={positionSX}>
{children}
</Collapse>
)}
{type === 'fade' && (
<Fade
{...others}
timeout={{
appear: 0,
enter: 300,
exit: 150
}}
>
<Box sx={positionSX}>{children}</Box>
</Fade>
)}
{type === 'slide' && (
<Slide
{...others}
timeout={{
appear: 0,
enter: 150,
exit: 150
}}
direction={direction}
>
<Box sx={positionSX}>{children}</Box>
</Slide>
)}
{type === 'zoom' && (
<Zoom {...others}>
<Box sx={positionSX}>{children}</Box>
</Zoom>
)}
</Box>
);
});
Transitions.propTypes = {
children: PropTypes.node,
type: PropTypes.oneOf(['grow', 'fade', 'collapse', 'slide', 'zoom']),
position: PropTypes.oneOf(['top-left', 'top-right', 'top', 'bottom-left', 'bottom-right', 'bottom']),
direction: PropTypes.oneOf(['up', 'down', 'left', 'right'])
};
export default Transitions;
export const PopupTransition = forwardRef(function Transition(props, ref) {
return <Zoom ref={ref} timeout={200} {...props} />;
});

View File

@@ -0,0 +1,32 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, CircularProgress, Typography } from '@mui/material';
// ==============================|| PROGRESS - CIRCULAR LABEL ||============================== //
export default function CircularWithLabel({ value, ...others }) {
return (
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<CircularProgress variant="determinate" value={value} {...others} />
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography variant="caption" component="div" color="text.secondary">{`${Math.round(value)}%`}</Typography>
</Box>
</Box>
);
}
CircularWithLabel.propTypes = {
value: PropTypes.number
};

View File

@@ -0,0 +1,65 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, CircularProgress, Typography, circularProgressClasses } from '@mui/material';
// ==============================|| PROGRESS - CIRCULAR PATH ||============================== //
export default function CircularWithPath({ value, size, variant, thickness, showLabel, pathColor, sx, ...others }) {
return (
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<CircularProgress
variant="determinate"
sx={{ color: pathColor ? pathColor : 'grey.200' }}
size={size}
thickness={thickness}
{...others}
value={100}
/>
{showLabel && (
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography variant="caption" component="div" color="text.secondary">
{value ? `${Math.round(value)}%` : '0%'}
</Typography>
</Box>
)}
<CircularProgress
variant={variant}
sx={{
...sx,
position: 'absolute',
left: 0,
[`& .${circularProgressClasses.circle}`]: {
strokeLinecap: 'round'
}
}}
size={size}
thickness={thickness}
value={value}
{...others}
/>
</Box>
);
}
CircularWithPath.propTypes = {
value: PropTypes.number,
size: PropTypes.number,
variant: PropTypes.string,
thickness: PropTypes.number,
showLabel: PropTypes.bool,
pathColor: PropTypes.string,
sx: PropTypes.array,
others: PropTypes.array
};

View File

@@ -0,0 +1,22 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, LinearProgress } from '@mui/material';
// ==============================|| PROGRESS - LINEAR ICON ||============================== //
export default function LinearWithIcon({ icon, value, ...others }) {
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant="determinate" value={value} {...others} />
</Box>
<Box sx={{ minWidth: 35 }}>{icon}</Box>
</Box>
);
}
LinearWithIcon.propTypes = {
icon: PropTypes.node,
value: PropTypes.number
};

View File

@@ -0,0 +1,23 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, LinearProgress, Typography } from '@mui/material';
// ==============================|| PROGRESS - LINEAR WITH LABEL ||============================== //
export default function LinearWithLabel({ value, ...others }) {
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant="determinate" value={value} {...others} />
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography variant="body2" color="text.secondary">{`${Math.round(value)}%`}</Typography>
</Box>
</Box>
);
}
LinearWithLabel.propTypes = {
value: PropTypes.number
};

View File

@@ -0,0 +1,47 @@
import { styled } from '@mui/material/styles';
import CircularProgress from '@mui/material/CircularProgress';
import nelogo from '../assets/images/logo-sm.png';
// Styled Loader Wrapper
const LoaderWrapper = styled('div')(() => ({
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 2001,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(255, 255, 255, 0.7)', // Light overlay effect
padding: '20px',
borderRadius: '10px' // Soft rounded border
}));
// Styled Logo Wrapper
const LogoWrapper = styled('div')(() => ({
position: 'absolute',
width: '40px', // Adjust based on your logo size
height: '40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}));
// ==============================|| Custom Circular Loader with Logo ||============================== //
const CircularLoader = () => (
<LoaderWrapper>
{/* Circular Loader */}
<CircularProgress
color="primary"
size={80} // Increase size
thickness={6} // Thicker border
/>
{/* Logo Positioned at the Center */}
<LogoWrapper>
<img src={nelogo} alt="Logo" style={{ width: '100%', height: '100%' }} />
</LogoWrapper>
</LoaderWrapper>
);
export default CircularLoader;

View File

@@ -0,0 +1,66 @@
import React, { useState } from 'react';
import { Dialog, DialogTitle, DialogContent, Button, Stack, Typography } from '@mui/material';
import { DateRangePicker } from 'mui-daterange-picker';
import dayjs from 'dayjs';
import { addDays, addWeeks, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addMonths } from 'date-fns';
const DateFilterDialog = ({ open, onClose, onSelect }) => {
const [selectedRange, setSelectedRange] = useState(null);
const handleDateChange = (range) => {
setSelectedRange({
startDate: dayjs(range.startDate).format('YYYY-MM-DD'),
endDate: dayjs(range.endDate).format('YYYY-MM-DD'),
label: range.label || 'custom'
});
};
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle align="left">
<Typography variant="h4">Select Filter Options</Typography>
</DialogTitle>
<DialogContent sx={{ width: '100%' }} className="datedialog">
<DateRangePicker
open={open}
toggle={onClose}
id="daterange1"
onChange={handleDateChange}
definedRanges={[
{ label: 'Today', startDate: new Date(), endDate: new Date() },
{ label: 'Yesterday', startDate: addDays(new Date(), -1), endDate: addDays(new Date(), -1) },
{ label: 'Tomorrow', startDate: addDays(new Date(), +1), endDate: addDays(new Date(), +1) },
{ label: 'This Week', startDate: startOfWeek(new Date()), endDate: endOfWeek(new Date()) },
{ label: 'Last Week', startDate: startOfWeek(addWeeks(new Date(), -1)), endDate: endOfWeek(addWeeks(new Date(), -1)) },
{ label: 'Last 7 Days', startDate: addWeeks(new Date(), -1), endDate: new Date() },
{ label: 'This Month', startDate: startOfMonth(new Date()), endDate: endOfMonth(new Date()) },
{ label: 'Last Month', startDate: startOfMonth(addMonths(new Date(), -1)), endDate: endOfMonth(addMonths(new Date(), -1)) }
]}
/>
</DialogContent>
<Stack direction="row" justifyContent="flex-end" sx={{ width: '100%', p: 2 }}>
<Button
variant="contained"
size="small"
onClick={() => {
// If no date selected → close dialog only
if (!selectedRange) {
onClose();
return;
}
// If date selected → return range + close
onSelect(selectedRange);
onClose();
}}
>
OK
</Button>
</Stack>
</Dialog>
);
};
export default DateFilterDialog;

View File

@@ -0,0 +1,15 @@
import { Suspense } from 'react';
// project import
import Loader from './Loader';
// ==============================|| LOADABLE - LAZY LOADING ||============================== //
const Loadable = (Component) => (props) =>
(
<Suspense fallback={<Loader />}>
<Component {...props} />
</Suspense>
);
export default Loadable;

25
src/components/Loader.js Normal file
View File

@@ -0,0 +1,25 @@
// material-ui
import { styled } from '@mui/material/styles';
import LinearProgress from '@mui/material/LinearProgress';
// loader style
const LoaderWrapper = styled('div')(({ theme }) => ({
position: 'fixed',
top: 0,
left: 0,
zIndex: 2001,
width: '100%',
'& > * + *': {
marginTop: theme.spacing(2)
}
}));
// ==============================|| Loader ||============================== //
const Loader = () => (
<LoaderWrapper>
<LinearProgress color="primary" />
</LoaderWrapper>
);
export default Loader;

43
src/components/Locales.js Normal file
View File

@@ -0,0 +1,43 @@
import PropTypes from 'prop-types';
import { useEffect, useState } from 'react';
// third-party
import { IntlProvider } from 'react-intl';
// project import
import useConfig from 'hooks/useConfig';
// load locales files
const loadLocaleData = () => {
return import('utils/locales/en.json');
};
// ==============================|| LOCALIZATION ||============================== //
const Locales = ({ children }) => {
const { i18n } = useConfig();
const [messages, setMessages] = useState();
useEffect(() => {
loadLocaleData(i18n).then((d) => {
setMessages(d.default);
});
}, [i18n]);
return (
<>
{messages && (
<IntlProvider locale={i18n} defaultLocale="en" messages={messages}>
{children}
</IntlProvider>
)}
</>
);
};
Locales.propTypes = {
children: PropTypes.node
};
export default Locales;

141
src/components/MainCard.js Normal file
View File

@@ -0,0 +1,141 @@
import PropTypes from 'prop-types';
import { forwardRef } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Card, CardContent, CardHeader, Divider, Typography } from '@mui/material';
// project-imports
import { ThemeMode } from 'config';
// ==============================|| CUSTOM - MAIN CARD ||============================== //
const MainCard = forwardRef(
(
{
border = true,
boxShadow,
children,
subheader,
content = true,
contentSX = {},
headerSX,
darkTitle,
divider = true,
elevation,
secondary,
shadow,
sx = {},
title,
modal = false,
...others
},
ref
) => {
const theme = useTheme();
boxShadow = theme.palette.mode === ThemeMode.DARK ? boxShadow || true : boxShadow;
return (
<Card
elevation={elevation || 0}
ref={ref}
{...others}
sx={{
position: 'relative',
border: border ? '1px solid' : 'none',
borderRadius: 1,
borderColor: theme.palette.mode === ThemeMode.DARK ? theme.palette.divider : theme.palette.grey.A800,
boxShadow: boxShadow && (!border || theme.palette.mode === ThemeMode.DARK) ? shadow || theme.customShadows.z1 : 'inherit',
':hover': {
boxShadow: boxShadow ? shadow || theme.customShadows.z1 : 'inherit'
},
...(modal && {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: `calc( 100% - 50px)`, sm: 'auto' },
'& .MuiCardContent-root': {
overflowY: 'auto',
minHeight: 'auto',
maxHeight: `calc(100vh - 200px)`
}
}),
...sx
}}
>
{/* card header and action */}
{/* {!darkTitle && title && ( */}
{!darkTitle && (
<CardHeader
sx={{ p: 0, '& .MuiCardHeader-action': { m: '0px auto', alignSelf: 'center' }, ...headerSX }}
titleTypographyProps={{ variant: 'subtitle1' }}
title={title}
action={secondary}
subheader={subheader}
/>
)}
{darkTitle && title && (
<CardHeader
sx={{ p: 1, '& .MuiCardHeader-action': { m: '0px auto', alignSelf: 'center' }, ...headerSX }}
title={<Typography variant="h4">{title || ''}</Typography>}
action={secondary}
/>
)}
{/* content & header divider */}
{title && divider && <Divider />}
{/* card content */}
{content && (
<CardContent
sx={{
overflow: 'auto',
'&::-webkit-scrollbar': {
width: '12px', // scroll bar width
cursor: 'pointer'
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.primary.main, // thumb color
borderRadius: '8px',
cursor: 'pointer'
},
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: theme.palette.primary.dark, // hover color
cursor: 'pointer'
},
'&::-webkit-scrollbar-track': {
backgroundColor: theme.palette.primary.lighter,
cursor: 'pointer'
},
...contentSX
}}
>
{children}
</CardContent>
)}
{!content && children}
</Card>
);
}
);
MainCard.propTypes = {
border: PropTypes.bool,
boxShadow: PropTypes.bool,
children: PropTypes.node,
subheader: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
content: PropTypes.bool,
contentClass: PropTypes.string,
contentSX: PropTypes.object,
darkTitle: PropTypes.bool,
divider: PropTypes.bool,
elevation: PropTypes.number,
secondary: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
shadow: PropTypes.string,
sx: PropTypes.object,
title: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
modal: PropTypes.bool
};
export default MainCard;

View File

@@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import { useEffect } from 'react';
// material-ui
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
// third-party
import rtlPlugin from 'stylis-plugin-rtl';
// project import
import { ThemeDirection } from 'config';
import useConfig from 'hooks/useConfig';
// ==============================|| RTL LAYOUT ||============================== //
const RTLLayout = ({ children }) => {
const { themeDirection } = useConfig();
useEffect(() => {
document.dir = themeDirection;
}, [themeDirection]);
const cacheRtl = createCache({
key: themeDirection === ThemeDirection.RTL ? 'rtl' : 'css',
prepend: true,
stylisPlugins: themeDirection === ThemeDirection.RTL ? [rtlPlugin] : []
});
return <CacheProvider value={cacheRtl}>{children}</CacheProvider>;
};
RTLLayout.propTypes = {
children: PropTypes.node
};
export default RTLLayout;

View File

@@ -0,0 +1,24 @@
import PropTypes from 'prop-types';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const ScrollTop = ({ children }) => {
const { pathname } = useLocation();
useEffect(() => {
// Scroll to top
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
});
}, [pathname]);
return children || null;
};
ScrollTop.propTypes = {
children: PropTypes.node
};
export default ScrollTop;

10
src/components/ScrollX.js Normal file
View File

@@ -0,0 +1,10 @@
// material-ui
import { styled } from '@mui/material/styles';
const ScrollX = styled('div')({
width: '100%',
overflowX: 'auto',
display: 'block'
});
export default ScrollX;

View File

@@ -0,0 +1,65 @@
import PropTypes from 'prop-types';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Avatar, ButtonBase, Link, Tooltip } from '@mui/material';
// ==============================|| CARD - SECONDARY ACTION ||============================== //
const CardSecondaryAction = ({ title, link, icon }) => {
const theme = useTheme();
return (
<Tooltip title={title || 'Reference'} placement="left">
<ButtonBase disableRipple>
{!icon && (
<Avatar
component={Link}
href={link}
target="_blank"
alt="MUI Logo"
sx={{ width: 28, height: 28, bgcolor: 'transparent', border: `2px solid ${theme.palette.primary.light}` }}
>
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0)">
<path d="M100 260.9V131L212.5 195.95V239.25L137.5 195.95V282.55L100 260.9Z" fill={theme.palette.primary.dark} />
<path
d="M212.5 195.95L325 131V260.9L250 304.2L212.5 282.55L287.5 239.25V195.95L212.5 239.25V195.95Z"
fill={theme.palette.primary.main}
/>
<path d="M212.5 282.55V325.85L287.5 369.15V325.85L212.5 282.55Z" fill={theme.palette.primary.dark} />
<path
d="M287.5 369.15L400 304.2V217.6L362.5 239.25V282.55L287.5 325.85V369.15ZM362.5 195.95V152.65L400 131V174.3L362.5 195.95Z"
fill={theme.palette.primary.main}
/>
</g>
<defs>
<clipPath id="clip0">
<rect width="300" height="238.3" fill="white" transform="translate(100 131)" />
</clipPath>
</defs>
</svg>
</Avatar>
)}
{icon && (
<Avatar
component={Link}
href={link}
target="_blank"
sx={{ width: 28, height: 28, bgcolor: 'transparent', border: `2px solid ${theme.palette.primary.light}` }}
>
{icon}
</Avatar>
)}
</ButtonBase>
</Tooltip>
);
};
CardSecondaryAction.propTypes = {
title: PropTypes.string,
link: PropTypes.string,
icon: PropTypes.oneOf([PropTypes.node, PropTypes.string])
};
export default CardSecondaryAction;

View File

@@ -0,0 +1,61 @@
// material-ui
import { Container, Link, Stack, Typography, useMediaQuery } from '@mui/material';
// ==============================|| FOOTER - AUTHENTICATION ||============================== //
const AuthFooter = () => {
const matchDownSM = useMediaQuery((theme) => theme.breakpoints.down('sm'));
return (
<Container maxWidth="xl">
<Stack
direction={matchDownSM ? 'column' : 'row'}
justifyContent={matchDownSM ? 'center' : 'space-between'}
spacing={2}
textAlign={matchDownSM ? 'center' : 'inherit'}
>
<Typography variant="subtitle2" color="secondary" component="span">
This site is protected by{' '}
<Typography component={Link} variant="subtitle2" href="#mantis-privacy" target="_blank" underline="hover">
Privacy Policy
</Typography>
</Typography>
<Stack direction={matchDownSM ? 'column' : 'row'} spacing={matchDownSM ? 1 : 3} textAlign={matchDownSM ? 'center' : 'inherit'}>
<Typography
variant="subtitle2"
color="secondary"
component={Link}
href="https://codedthemes.com"
target="_blank"
underline="hover"
>
Terms and Conditions
</Typography>
<Typography
variant="subtitle2"
color="secondary"
component={Link}
href="https://codedthemes.com"
target="_blank"
underline="hover"
>
Privacy Policy
</Typography>
<Typography
variant="subtitle2"
color="secondary"
component={Link}
href="https://codedthemes.com"
target="_blank"
underline="hover"
>
CA Privacy Notice
</Typography>
</Stack>
</Stack>
</Container>
);
};
export default AuthFooter;

View File

@@ -0,0 +1,49 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, Grid, Link, Stack, Typography } from '@mui/material';
// assets
import { GlobalOutlined, NodeExpandOutlined } from '@ant-design/icons';
// ==============================|| COMPONENTS - BREADCRUMBS ||============================== //
const ComponentHeader = ({ title, caption, directory, link }) => (
<Box sx={{ pl: 3 }}>
<Stack spacing={1.25}>
<Typography variant="h2">{title}</Typography>
{caption && (
<Typography variant="h6" color="textSecondary">
{caption}
</Typography>
)}
</Stack>
<Grid container spacing={0.75} sx={{ mt: 1.75 }}>
{directory && (
<Grid item xs={12}>
<Typography variant="caption" color="textSecondary">
<NodeExpandOutlined style={{ marginRight: 10 }} />
{directory}
</Typography>
</Grid>
)}
{link && (
<Grid item xs={12}>
<Link variant="caption" color="primary" href={link} target="_blank">
<GlobalOutlined style={{ marginRight: 10 }} />
{link}
</Link>
</Grid>
)}
</Grid>
</Box>
);
ComponentHeader.propTypes = {
title: PropTypes.string,
caption: PropTypes.string,
directory: PropTypes.string,
link: PropTypes.string
};
export default ComponentHeader;

View File

@@ -0,0 +1,52 @@
// material-ui
import { useTheme } from '@mui/material/styles';
import { Fab, Badge } from '@mui/material';
// project import
// assets
// ==============================|| CART ITEMS - FLOATING BUTTON ||============================== //
const FloatingCart = ({ element, count = 0, onClick, sx }) => {
const theme = useTheme();
// const cart = useSelector((state) => state.cart);
// const totalQuantity = sum(cart.checkout.products.map((item) => item.quantity));
return (
<Fab
// component={Link}
// to="/apps/e-commerce/checkout"
onClick={onClick} // ← important
size="large"
sx={{
top: '75%',
position: 'fixed',
right: 0,
zIndex: theme.zIndex.speedDial,
boxShadow: theme.customShadows.primary,
bgcolor: 'primary.lighter',
color: 'primary.main',
borderRadius: '25%',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
'&:hover': {
bgcolor: 'primary.100',
boxShadow: theme.customShadows.primary
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.primary.dark}`,
outlineOffset: 2
},
...sx
}}
>
<Badge showZero badgeContent={count} color="error">
{element}
</Badge>
</Fab>
);
};
export default FloatingCart;

View File

@@ -0,0 +1,176 @@
import PropTypes from 'prop-types';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box, Button, CardContent, CardMedia, Chip, Divider, Grid, Rating, Stack, Typography } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
import IconButton from 'components/@extended/IconButton';
import SkeletonProductPlaceholder from 'components/cards/skeleton/ProductPlaceholder';
import { useDispatch, useSelector } from 'store';
import { addProduct } from 'store/reducers/cart';
import { openSnackbar } from 'store/reducers/snackbar';
// assets
import { HeartOutlined, HeartFilled } from '@ant-design/icons';
const prodImage = require.context('assets/images/e-commerce', true);
// ==============================|| PRODUCT CARD ||============================== //
const ProductCard = ({ id, color, name, brand, offer, isStock, image, description, offerPrice, salePrice, rating }) => {
const theme = useTheme();
const dispatch = useDispatch();
const prodProfile = image && prodImage(`./${image}`);
const [productRating] = useState(rating);
const [wishlisted, setWishlisted] = useState(false);
const cart = useSelector((state) => state.cart);
const addCart = () => {
dispatch(addProduct({ id, name, image, salePrice, offerPrice, color, size: 8, quantity: 1, description }, cart.checkout.products));
dispatch(
openSnackbar({
open: true,
message: 'Add To Cart Success',
variant: 'alert',
alert: {
color: 'success'
},
close: false
})
);
};
const addToFavourite = () => {
setWishlisted(!wishlisted);
dispatch(
openSnackbar({
open: true,
message: 'Added to favourites',
variant: 'alert',
alert: {
color: 'success'
},
close: false
})
);
};
const [isLoading, setLoading] = useState(true);
useEffect(() => {
setLoading(false);
}, []);
return (
<>
{isLoading ? (
<SkeletonProductPlaceholder />
) : (
<MainCard
content={false}
boxShadow
sx={{
'&:hover': {
transform: 'scale3d(1.02, 1.02, 1)',
transition: 'all .4s ease-in-out'
}
}}
>
<Box sx={{ width: 250, m: 'auto' }}>
<CardMedia
sx={{ height: 250, textDecoration: 'none', opacity: isStock ? 1 : 0.25 }}
image={prodProfile}
component={Link}
to={`/apps/e-commerce/product-details/${id}`}
/>
</Box>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{ width: '100%', position: 'absolute', top: 0, pt: 1.75, pl: 2, pr: 1 }}
>
{!isStock && <Chip variant="light" color="error" size="small" label="Sold out" />}
{offer && <Chip label={offer} variant="combined" color="success" size="small" />}
<IconButton color="secondary" sx={{ ml: 'auto', '&:hover': { background: 'transparent' } }} onClick={addToFavourite}>
{wishlisted ? (
<HeartFilled style={{ fontSize: '1.15rem', color: theme.palette.error.main }} />
) : (
<HeartOutlined style={{ fontSize: '1.15rem' }} />
)}
</IconButton>
</Stack>
<Divider />
<CardContent sx={{ p: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack>
<Typography
component={Link}
to={`/apps/e-commerce/product-details/${id}`}
color="textPrimary"
variant="h5"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'block',
textDecoration: 'none'
}}
>
{name}
</Typography>
<Typography variant="h6" color="textSecondary">
{brand}
</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-end" flexWrap="wrap" rowGap={1.75}>
<Stack>
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="h5">${offerPrice}</Typography>
{salePrice && (
<Typography variant="h6" color="textSecondary" sx={{ textDecoration: 'line-through' }}>
${salePrice}
</Typography>
)}
</Stack>
<Stack direction="row" alignItems="flex-start">
<Rating precision={0.5} name="size-small" value={productRating} size="small" readOnly />
<Typography variant="caption">({productRating?.toFixed(1)})</Typography>
</Stack>
</Stack>
<Button variant="contained" onClick={addCart} disabled={!isStock}>
{!isStock ? 'Sold Out' : 'Add to Cart'}
</Button>
</Stack>
</Grid>
</Grid>
</CardContent>
</MainCard>
)}
</>
);
};
ProductCard.propTypes = {
id: PropTypes.number,
color: PropTypes.string,
name: PropTypes.string,
brand: PropTypes.string,
isStock: PropTypes.bool,
image: PropTypes.string,
description: PropTypes.string,
offerPrice: PropTypes.number,
salePrice: PropTypes.number,
offer: PropTypes.string,
rating: PropTypes.number
};
export default ProductCard;

View File

@@ -0,0 +1,52 @@
import PropTypes from 'prop-types';
// material-ui
import { Grid, Rating, Stack, Typography } from '@mui/material';
// project imports
import Avatar from 'components/@extended/Avatar';
// assets
import { StarFilled, StarOutlined } from '@ant-design/icons';
const avatarImage = require.context('assets/images/users', true);
// ==============================|| PRODUCT DETAILS - REVIEW ||============================== //
const ProductReview = ({ avatar, date, name, rating, review }) => (
<Grid item xs={12}>
<Stack direction="row" spacing={1}>
<Avatar alt={name} src={avatar && avatarImage(`./${avatar}`)} />
<Stack spacing={2}>
<Stack>
<Typography variant="subtitle1" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>
{name}
</Typography>
<Typography variant="caption" color="textSecondary">
{date}
</Typography>
<Rating
size="small"
name="simple-controlled"
value={rating < 4 ? rating + 1 : rating}
icon={<StarFilled style={{ fontSize: 'inherit' }} />}
emptyIcon={<StarOutlined style={{ fontSize: 'inherit' }} />}
precision={0.1}
readOnly
/>
</Stack>
<Typography variant="body2">{review}</Typography>
</Stack>
</Stack>
</Grid>
);
ProductReview.propTypes = {
avatar: PropTypes.string,
date: PropTypes.string,
name: PropTypes.string,
rating: PropTypes.number,
review: PropTypes.string
};
export default ProductReview;

View File

@@ -0,0 +1,44 @@
// material-ui
import { CardContent, Grid, Skeleton, Stack } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// ===========================|| SKELETON - PRODUCT CARD ||=========================== //
const ProductPlaceholder = () => (
<MainCard content={false} boxShadow>
<Skeleton variant="rectangular" height={220} />
<CardContent sx={{ p: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Skeleton variant="rectangular" height={20} />
</Grid>
<Grid item xs={12}>
<Skeleton variant="rectangular" height={45} />
</Grid>
<Grid item xs={12} sx={{ pt: '8px !important' }}>
<Stack direction="row" alignItems="center" spacing={1}>
<Skeleton variant="rectangular" height={20} width={90} />
<Skeleton variant="rectangular" height={20} width={38} />
</Stack>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Grid container spacing={1}>
<Grid item>
<Skeleton variant="rectangular" height={20} width={40} />
</Grid>
<Grid item>
<Skeleton variant="rectangular" height={17} width={20} />
</Grid>
</Grid>
<Skeleton variant="rectangular" height={32} width={47} />
</Stack>
</Grid>
</Grid>
</CardContent>
</MainCard>
);
export default ProductPlaceholder;

View File

@@ -0,0 +1,66 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, Chip, Grid, Stack, Typography } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// assets
import { FallOutlined, RiseOutlined } from '@ant-design/icons';
// ==============================|| STATISTICS - ECOMMERCE CARD ||============================== //
const AnalyticEcommerce = ({ color = 'primary', title, count, percentage, isLoss, extra }) => (
<MainCard contentSX={{ p: 2.25 }}>
<Stack spacing={0.5}>
<Typography variant="h6" color="textSecondary">
{title}
</Typography>
<Grid container alignItems="center">
<Grid item>
<Typography variant="h4" color="inherit">
{count}
</Typography>
</Grid>
{percentage && (
<Grid item>
<Chip
variant="combined"
color={color}
icon={
<>
{!isLoss && <RiseOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
{isLoss && <FallOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
</>
}
label={`${percentage}%`}
sx={{ ml: 1.25, pl: 1 }}
size="small"
/>
</Grid>
)}
</Grid>
</Stack>
<Box sx={{ pt: 2.25 }}>
<Typography variant="caption" color="textSecondary">
You made an extra{' '}
<Typography component="span" variant="caption" sx={{ color: `${color || 'primary'}.main` }}>
{extra}
</Typography>{' '}
this year
</Typography>
</Box>
</MainCard>
);
AnalyticEcommerce.propTypes = {
title: PropTypes.string,
count: PropTypes.string,
percentage: PropTypes.number,
isLoss: PropTypes.bool,
color: PropTypes.string,
extra: PropTypes.string
};
export default AnalyticEcommerce;

View File

@@ -0,0 +1,56 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, Chip, Stack, Typography } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// assets
import { RiseOutlined, FallOutlined } from '@ant-design/icons';
// ==============================|| STATISTICS - ECOMMERCE CARD ||============================== //
const AnalyticsDataCard = ({ color = 'primary', title, count, percentage, isLoss, children }) => (
<MainCard content={false}>
<Box sx={{ p: 2.25 }}>
<Stack spacing={0.5}>
<Typography variant="h6" color="textSecondary">
{title}
</Typography>
<Stack direction="row" alignItems="center">
<Typography variant="h4" color="inherit">
{count}
</Typography>
{percentage && (
<Chip
variant="combined"
color={color}
icon={
<>
{!isLoss && <RiseOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
{isLoss && <FallOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
</>
}
label={`${percentage}%`}
sx={{ ml: 1.25, pl: 1 }}
size="small"
/>
)}
</Stack>
</Stack>
</Box>
{children}
</MainCard>
);
AnalyticsDataCard.propTypes = {
title: PropTypes.string,
count: PropTypes.string,
percentage: PropTypes.number,
isLoss: PropTypes.bool,
color: PropTypes.string,
children: PropTypes.node
};
export default AnalyticsDataCard;

View File

@@ -0,0 +1,70 @@
// material-ui
import { Box, Card, CardContent, Grid, Typography } from '@mui/material';
// ===========================|| HOVER SOCIAL CARD ||=========================== //
const HoverSocialCard = ({
primary,
secondary,
percentage,
// iconPrimary,
color,
sx
}) => {
// const IconPrimary = iconPrimary;
// const primaryIcon = iconPrimary ? <IconPrimary /> : null;
return (
<Card
elevation={0}
sx={{
background: color,
position: 'relative',
color: '#fff',
'&:hover svg': {
opacity: 1,
transform: 'scale(1.1)'
},
...sx
}}
>
<CardContent>
<Box
sx={{
position: 'absolute',
right: 15,
top: 25,
color: '#fff',
'& svg': {
width: 36,
height: 36,
opacity: 0.5,
transition: 'all .3s ease-in-out'
}
}}
>
<Typography variant="h2" color="inherit" sx={{ fontSize: { xs: 20, md: 25 } }}>
{/* {percentage.toString()} % */}
{percentage && typeof percentage === 'number' ? `${percentage.toString()} %` : percentage}
</Typography>
{/* {primaryIcon} */}
</Box>
<Grid container spacing={0}>
<Grid item xs={12}>
<Typography variant="h3" color="inherit" sx={{ fontSize: { xs: 16, md: 20 } }}>
{secondary}
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="subtitle2" color="inherit">
{primary}
</Typography>
</Grid>
</Grid>
</CardContent>
</Card>
);
};
export default HoverSocialCard;

View File

@@ -0,0 +1,56 @@
// material-ui
import { useTheme } from '@mui/material/styles';
/**
* if you want to use image instead of <svg> uncomment following.
*
* import logoIconDark from 'assets/images/logo-icon-dark.svg';
* import logoIcon from 'assets/images/logo-icon.svg';
* import { ThemeMode } from 'config';
*
*/
// ==============================|| LOGO ICON SVG ||============================== //
const LogoIcon = () => {
const theme = useTheme();
return (
/**
* if you want to use image instead of svg uncomment following, and comment out <svg> element.
*
* <img src={theme.palette.mode === ThemeMode.DARK? logoIconDark : logoIcon} alt="Mantis" width="100" />
*
*/
<svg width="129" height="129" viewBox="0 0 129 129" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.27577 57.2242L17.5616 46.9384L17.5724 46.9276H36.9234L29.2238 54.6273L27.2358 56.6152L19.3511 64.5L20.3276 65.4792L64.5 109.649L109.649 64.5L101.761 56.6152L101.206 56.0572L92.0766 46.9276H111.428L111.438 46.9384L119.5 55.0002L129 64.5L64.5 129L0 64.5L7.27577 57.2242ZM64.5 0L101.77 37.2695H82.4185L64.5 19.3511L46.5816 37.2695H27.2305L64.5 0Z"
fill={theme.palette.primary.dark}
/>
<path
d="M19.3509 64.5L27.2357 56.6152L29.2236 54.6273L21.5267 46.9276H17.5722L17.5615 46.9384L7.27561 57.2242L17.1483 67.0487L19.3509 64.5Z"
fill="url(#paint0_linear)"
/>
<path
d="M101.762 56.6152L109.649 64.5L108.868 65.2807L108.871 65.2834L119.5 55.0002L111.438 46.9384L111.428 46.9276H110.644L101.206 56.0572L101.762 56.6152Z"
fill="url(#paint1_linear)"
/>
<path
d="M17.5508 46.9276L17.5615 46.9384L27.2357 56.6152L64.4999 93.8767L111.449 46.9276H17.5508Z"
fill={theme.palette.primary.main}
/>
<defs>
<linearGradient id="paint0_linear" x1="25.0225" y1="49.3259" x2="11.4189" y2="62.9295" gradientUnits="userSpaceOnUse">
<stop stopColor={theme.palette.primary.darker} />
<stop offset="0.9637" stopColor={theme.palette.primary.dark} stopOpacity="0" />
</linearGradient>
<linearGradient id="paint1_linear" x1="103.5" y1="49.5" x2="114.5" y2="62" gradientUnits="userSpaceOnUse">
<stop stopColor={theme.palette.primary.darker} />
<stop offset="1" stopColor={theme.palette.primary.dark} stopOpacity="0" />
</linearGradient>
</defs>
</svg>
);
};
export default LogoIcon;

View File

@@ -0,0 +1,68 @@
import PropTypes from 'prop-types';
// material-ui
import { useTheme } from '@mui/material/styles';
import { ThemeMode } from 'config';
/**
* if you want to use image instead of <svg> uncomment following.
*
* import logoDark from 'assets/images/logo-dark.svg';
* import logo from 'assets/images/logo.svg';
*
*/
// ==============================|| LOGO SVG ||============================== //
const LogoMain = ({ reverse }) => {
const theme = useTheme();
return (
/**
* if you want to use image instead of svg uncomment following, and comment out <svg> element.
*
* <img src={theme.palette.mode === ThemeMode.DARK ? logoDark : logo} alt="Mantis" width="100" />
*
*/
<>
<svg width="118" height="35" viewBox="0 0 118 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4.63564 15.8644L6.94797 13.552L6.95038 13.5496H11.3006L9.56969 15.2806L9.12278 15.7275L7.35024 17.5L7.56977 17.7201L17.5 27.6498L27.6498 17.5L25.8766 15.7275L25.7518 15.602L23.6994 13.5496H28.0496L28.052 13.552L29.8644 15.3644L32 17.5L17.5 32L3 17.5L4.63564 15.8644ZM17.5 3L25.8784 11.3784H21.5282L17.5 7.35024L13.4718 11.3784H9.12158L17.5 3Z"
fill={theme.palette.primary.dark}
/>
<path
d="M7.35025 17.5L9.1228 15.7275L9.5697 15.2805L7.83937 13.5496H6.95039L6.94798 13.552L4.63564 15.8644L6.8551 18.073L7.35025 17.5Z"
fill="url(#paint0_linear)"
/>
<path
d="M25.8767 15.7275L27.6498 17.5L27.4743 17.6755L27.4749 17.6761L29.8644 15.3644L28.0521 13.552L28.0497 13.5496H27.8736L25.7518 15.602L25.8767 15.7275Z"
fill="url(#paint1_linear)"
/>
<path
d="M6.94549 13.5496L6.9479 13.552L9.12272 15.7275L17.4999 24.1041L28.0544 13.5496H6.94549Z"
fill={theme.palette.primary.main}
/>
<path
d="M46.5781 10V26H49.3594V14.9844H49.5078L53.9297 25.9531H56.0078L60.4297 15.0078H60.5781V26H63.3594V10H59.8125L55.0625 21.5937H54.875L50.125 10H46.5781ZM69.8438 26.2422C71.7266 26.2422 72.8516 25.3594 73.3672 24.3516H73.4609V26H76.1797V17.9687C76.1797 14.7969 73.5937 13.8438 71.3047 13.8438C68.7813 13.8438 66.8437 14.9687 66.2188 17.1562L68.8594 17.5312C69.1406 16.7109 69.9375 16.0078 71.3203 16.0078C72.6328 16.0078 73.3516 16.6797 73.3516 17.8594V17.9062C73.3516 18.7188 72.5 18.7578 70.3828 18.9844C68.0547 19.2344 65.8281 19.9297 65.8281 22.6328C65.8281 24.9922 67.5547 26.2422 69.8438 26.2422ZM70.5781 24.1641C69.3984 24.1641 68.5547 23.625 68.5547 22.5859C68.5547 21.5 69.5 21.0469 70.7656 20.8672C71.5078 20.7656 72.9922 20.5781 73.3594 20.2812V21.6953C73.3594 23.0312 72.2813 24.1641 70.5781 24.1641ZM81.8516 18.9687C81.8516 17.2344 82.8984 16.2344 84.3906 16.2344C85.8516 16.2344 86.7266 17.1953 86.7266 18.7969V26H89.5547V18.3594C89.5625 15.4844 87.9219 13.8438 85.4453 13.8438C83.6484 13.8438 82.4141 14.7031 81.8672 16.0391H81.7266V14H79.0234V26H81.8516V18.9687ZM98.4219 14H96.0547V11.125H93.2266V14H91.5234V16.1875H93.2266V22.8594C93.2109 25.1172 94.8516 26.2266 96.9766 26.1641C97.7813 26.1406 98.3359 25.9844 98.6406 25.8828L98.1641 23.6719C98.0078 23.7109 97.6875 23.7812 97.3359 23.7812C96.625 23.7812 96.0547 23.5312 96.0547 22.3906V16.1875H98.4219V14ZM100.787 26H103.615V14H100.787V26ZM102.209 12.2969C103.107 12.2969 103.842 11.6094 103.842 10.7656C103.842 9.91406 103.107 9.22656 102.209 9.22656C101.303 9.22656 100.568 9.91406 100.568 10.7656C100.568 11.6094 101.303 12.2969 102.209 12.2969ZM116.008 17.1719C115.617 15.1406 113.992 13.8438 111.18 13.8438C108.289 13.8438 106.32 15.2656 106.328 17.4844C106.32 19.2344 107.398 20.3906 109.703 20.8672L111.75 21.2969C112.852 21.5391 113.367 21.9844 113.367 22.6641C113.367 23.4844 112.477 24.1016 111.133 24.1016C109.836 24.1016 108.992 23.5391 108.75 22.4609L105.992 22.7266C106.344 24.9297 108.195 26.2344 111.141 26.2344C114.141 26.2344 116.258 24.6797 116.266 22.4062C116.258 20.6953 115.156 19.6484 112.891 19.1562L110.844 18.7188C109.625 18.4453 109.141 18.0234 109.148 17.3281C109.141 16.5156 110.039 15.9531 111.219 15.9531C112.523 15.9531 113.211 16.6641 113.43 17.4531L116.008 17.1719Z"
fill={theme.palette.mode === ThemeMode.DARK || reverse ? theme.palette.common.white : theme.palette.common.black}
fillOpacity="0.85"
/>
<defs>
<linearGradient id="paint0_linear" x1="8.62526" y1="14.0888" x2="5.56709" y2="17.1469" gradientUnits="userSpaceOnUse">
<stop stopColor={theme.palette.primary.darker} />
<stop offset="0.9637" stopColor={theme.palette.primary.dark} stopOpacity="0" />
</linearGradient>
<linearGradient id="paint1_linear" x1="26.2675" y1="14.1279" x2="28.7404" y2="16.938" gradientUnits="userSpaceOnUse">
<stop stopColor={theme.palette.primary.darker} />
<stop offset="1" stopColor={theme.palette.primary.dark} stopOpacity="0" />
</linearGradient>
</defs>
</svg>
</>
);
};
LogoMain.propTypes = {
reverse: PropTypes.bool
};
export default LogoMain;

View File

@@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
// material-ui
import { ButtonBase } from '@mui/material';
// project import
import LogoMain from './LogoMain';
import LogoIcon from './LogoIcon';
import { APP_DEFAULT_PATH } from 'config';
// ==============================|| MAIN LOGO ||============================== //
const LogoSection = ({ reverse, isIcon, sx, to }) => (
<ButtonBase disableRipple component={Link} to={!to ? APP_DEFAULT_PATH : to} sx={sx}>
{isIcon ? <LogoIcon /> : <LogoMain reverse={reverse} />}
</ButtonBase>
);
LogoSection.propTypes = {
reverse: PropTypes.bool,
isIcon: PropTypes.bool,
sx: PropTypes.object,
to: PropTypes.string
};
export default LogoSection;

View File

@@ -0,0 +1,66 @@
/* eslint-disable react/prop-types */
import React, { useEffect, useRef } from 'react';
import { OutlinedInput, InputAdornment, Tooltip, IconButton } from '@mui/material';
import { SearchOutlined } from '@mui/icons-material';
import ClearIcon from '@mui/icons-material/Clear';
import { useDebounce } from 'use-debounce';
const DebounceSearchBar = ({
value,
onChange,
onDebouncedChange, // 🔹 NEW
debounceTime = 500,
placeholder = 'Search (ctrl+k)',
sx
}) => {
const textFieldRef = useRef(null);
const [debouncedValue] = useDebounce(value, debounceTime);
// fire debounced callback whenever debouncedValue changes
useEffect(() => {
if (onDebouncedChange) {
onDebouncedChange(debouncedValue);
}
}, [debouncedValue, onDebouncedChange]);
useEffect(() => {
const handleKeyPress = (event) => {
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
textFieldRef.current?.focus();
}
if (event.key === 'Escape' && document.activeElement === textFieldRef.current) {
textFieldRef.current.blur();
}
};
document.addEventListener('keydown', handleKeyPress);
return () => document.removeEventListener('keydown', handleKeyPress);
}, []);
return (
<OutlinedInput
sx={{ ...sx }}
inputRef={textFieldRef}
placeholder={placeholder}
autoComplete="off"
value={value}
fullWidth
onChange={(e) => onChange(e.target.value)}
startAdornment={
<InputAdornment position="start" sx={{ mr: -0.5 }}>
<SearchOutlined />
</InputAdornment>
}
endAdornment={
<Tooltip title="Clear">
<IconButton sx={{ visibility: value ? 'visible' : 'hidden' }} onClick={() => onChange('')}>
<ClearIcon />
</IconButton>
</Tooltip>
}
/>
);
};
export default DebounceSearchBar;

View File

@@ -0,0 +1,30 @@
import { enqueueSnackbar, closeSnackbar } from 'notistack';
import { setSnackbarId } from '../../store/reducers/toastSlice';
import { store, dispatch } from 'store';
export const GlobalToast = (message, color = 'default', vertical = 'top') => {
const id = enqueueSnackbar(message, {
variant: color,
anchorOrigin: { vertical, horizontal: 'right' },
autoHideDuration: null,
action: (snackbarId) => (
<button onClick={() => closeSnackbar(snackbarId)} style={{ background: 'none', border: 'none', cursor: 'pointer' }}>
</button>
)
});
// Save snackbarId globally
dispatch(setSnackbarId(id));
return id;
};
// GLOBAL close function
export const closeGlobalToast = () => {
const id = store.getState().toastSlice.snackbarId;
if (id) {
closeSnackbar(id);
}
};

View File

@@ -0,0 +1,34 @@
// LoaderWithImage.jsx
import React from 'react';
import { Box, CircularProgress } from '@mui/material';
import nelogo from '../../assets/images/logo-sm.png';
export default function LoaderWithImage({ size = 70, imgSize = 40, alt = 'loader' }) {
return (
<Box position="relative" display="inline-flex" justifyContent="center" alignItems="center">
<CircularProgress size={size} />
<Box
position="absolute"
display="flex"
justifyContent="center"
alignItems="center"
sx={{
width: imgSize,
height: imgSize
}}
>
<img
src={nelogo}
alt={alt}
style={{
width: '100%',
height: '100%',
borderRadius: '50%',
objectFit: 'contain'
}}
/>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,51 @@
import React, { forwardRef, useEffect, useState } from 'react';
import { Autocomplete, TextField } from '@mui/material';
import axios from 'axios';
const LocationAutocomplete = forwardRef(({ setAppId, setLocoName, setPage, sx, textfeildSx }, ref) => {
const [locations, setLocations] = useState(JSON.parse(localStorage.getItem('applocations') || '[]'));
useEffect(() => {
const fetchLocations = async () => {
try {
const userid = localStorage.getItem('userid');
if (!userid) return;
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
if (response.data.status) {
const updatedLocations = [...response.data.details, { locationname: 'All', applocationid: 0 }];
localStorage.setItem('applocations', JSON.stringify(updatedLocations));
setLocations(updatedLocations);
}
} catch (err) {
console.error('Error fetching locations in LocationAutocomplete:', err);
}
};
if (locations.length === 0) {
fetchLocations();
}
}, [locations.length]);
return (
<Autocomplete
id="location-autocomplete"
options={locations || []}
getOptionLabel={(option) => option?.locationname ?? ''}
sx={{ ...sx }}
onChange={(event, value, reason) => {
if (reason === 'clear') {
setAppId?.(0);
setLocoName?.('');
setPage?.(0);
} else if (value) {
setAppId?.(value.applocationid);
setLocoName?.(value.locationname);
setPage?.(0);
}
}}
renderInput={(params) => <TextField {...params} inputRef={ref} label={'Select Zones'} sx={{ ...textfeildSx }} />}
/>
);
});
export default LocationAutocomplete;

View File

@@ -0,0 +1,77 @@
// DebouncedSearchBar.jsx
import React, { useEffect, useRef, useState } from 'react';
import { FormControl, OutlinedInput, InputAdornment, IconButton, useTheme } from '@mui/material';
import SearchOutlined from '@mui/icons-material/SearchOutlined';
import ClearIcon from '@mui/icons-material/Clear';
const SearchBar = ({ value, onChange, sx, placeholder = 'Search (Ctrl + K)', delay = 300 }) => {
const theme = useTheme();
const inputRef = useRef(null);
// Local input state for immediate UI update
const [inputValue, setInputValue] = useState(value || '');
/* Sync external value when it changes */
useEffect(() => {
setInputValue(value || '');
}, [value]);
/* ================================
🔥 Debounce Logic
================================ */
useEffect(() => {
const handler = setTimeout(() => {
if (inputValue !== value) {
onChange({ target: { value: inputValue } });
}
}, delay);
return () => clearTimeout(handler);
}, [inputValue]);
/* CTRL + K / CMD + K & ESC behavior */
useEffect(() => {
const handleKeyPress = (event) => {
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
inputRef.current?.focus();
}
if (event.key === 'Escape' && document.activeElement === inputRef.current) {
inputRef.current.blur();
}
};
document.addEventListener('keydown', handleKeyPress);
return () => document.removeEventListener('keydown', handleKeyPress);
}, []);
return (
<FormControl fullWidth>
<OutlinedInput
inputRef={inputRef}
placeholder={placeholder}
sx={{
borderRadius: 0,
...sx
}}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)} // No direct onChange
autoComplete="off"
size="large"
startAdornment={
<InputAdornment position="start" sx={{ mr: -0.5, color: theme.palette.secondary.main }}>
<SearchOutlined />
</InputAdornment>
}
endAdornment={
<IconButton sx={{ visibility: inputValue ? 'visible' : 'hidden' }} onClick={() => setInputValue('')}>
<ClearIcon style={{ fontSize: 'large', color: theme.palette.primary.main }} />
</IconButton>
}
/>
</FormControl>
);
};
export default SearchBar;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { TableRow, TableCell, Skeleton } from '@mui/material';
export default function TableLoader({ rows = 5, columns = 6, height = 40 }) {
return (
<>
{[...Array(rows)].map((_, rowIndex) => (
<TableRow key={rowIndex}>
{[...Array(columns)].map((_, colIndex) => (
<TableCell key={colIndex}>
<Skeleton variant="rectangular" height={height} width={70} animation="wave" />
</TableCell>
))}
</TableRow>
))}
</>
);
}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Stack, Typography, Box } from '@mui/material';
const TitleCard = ({ sx, title, children, starticon }) => {
return (
<Box
sx={{
...sx
}}
>
<Stack direction="row" flexWrap={'wrap'} alignItems="center" justifyContent="space-between" gap={1}>
<Stack>
{starticon && starticon}
<Typography variant="h3">{title}</Typography>
</Stack>
{children}
</Stack>
</Box>
);
};
export default TitleCard;

65
src/components/third-party/Notistack.js vendored Normal file
View File

@@ -0,0 +1,65 @@
import PropTypes from 'prop-types';
//material-ui
import { styled } from '@mui/material/styles';
// third-party
import { SnackbarProvider } from 'notistack';
// project import
import { useSelector } from 'store';
// assets
import { CheckCircleOutlined, CloseCircleOutlined, InfoCircleOutlined, WarningOutlined } from '@ant-design/icons';
// custom styles
const StyledSnackbarProvider = styled(SnackbarProvider)(({ theme }) => ({
'&.notistack-MuiContent-default': {
backgroundColor: theme.palette.primary.main
},
'&.notistack-MuiContent-error': {
backgroundColor: theme.palette.error.main
},
'&.notistack-MuiContent-success': {
backgroundColor: theme.palette.success.main
},
'&.notistack-MuiContent-info': {
backgroundColor: theme.palette.info.main
},
'&.notistack-MuiContent-warning': {
backgroundColor: theme.palette.warning.main
}
}));
// ===========================|| SNACKBAR - NOTISTACK ||=========================== //
const Notistack = ({ children }) => {
const snackbar = useSelector((state) => state.snackbar);
const iconSX = { marginRight: 8, fontSize: '1.15rem' };
return (
<StyledSnackbarProvider
maxSnack={snackbar.maxStack}
dense={snackbar.dense}
iconVariant={
snackbar.iconVariant === 'useemojis'
? {
success: <CheckCircleOutlined style={iconSX} />,
error: <CloseCircleOutlined style={iconSX} />,
warning: <WarningOutlined style={iconSX} />,
info: <InfoCircleOutlined style={iconSX} />
}
: undefined
}
hideIconVariant={snackbar.iconVariant === 'hide' ? true : false}
>
{children}
</StyledSnackbarProvider>
);
};
Notistack.propTypes = {
children: PropTypes.node
};
export default Notistack;

View File

@@ -0,0 +1,9 @@
import { enqueueSnackbar } from 'notistack';
export const OpenToast = (message, color, time) => {
return enqueueSnackbar(message, {
variant: color,
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: time || 2000
});
};

595
src/components/third-party/ReactTable.js vendored Normal file
View File

@@ -0,0 +1,595 @@
import PropTypes from 'prop-types';
import React, { forwardRef, useEffect, useRef, useState } from 'react';
// material-ui
import { styled, useTheme } from '@mui/material/styles';
import {
Box,
Button,
Checkbox,
Chip,
CircularProgress,
FormControl,
Grid,
ListItemText,
MenuItem,
OutlinedInput,
Pagination,
Select,
Stack,
TableCell,
TableRow,
TextField,
Tooltip,
Typography
} from '@mui/material';
// project-import
import { ThemeMode } from 'config';
// third-party
import { CSVLink } from 'react-csv';
import { getEmptyImage } from 'react-dnd-html5-backend';
import { useDrop, useDrag, useDragLayer } from 'react-dnd';
// assets
import { CaretUpOutlined, CaretDownOutlined, DragOutlined, CheckOutlined, DownloadOutlined } from '@ant-design/icons';
// ==============================|| HEADER HEADER ||============================== //
export const HeaderSort = ({ column, sort }) => {
const theme = useTheme();
return (
<Stack direction="row" spacing={1} alignItems="center">
<Box sx={{ width: 'max-content' }}>{column.render('Header')}</Box>
{!column.disableSortBy && (
<Stack sx={{ color: 'secondary.light' }} {...(sort && { ...column.getHeaderProps(column.getSortByToggleProps()) })}>
<CaretUpOutlined
style={{
fontSize: '0.625rem',
color: column.isSorted && !column.isSortedDesc ? theme.palette.text.secondary : 'inherit'
}}
/>
<CaretDownOutlined
style={{
fontSize: '0.625rem',
marginTop: -2,
color: column.isSortedDesc ? theme.palette.text.secondary : 'inherit'
}}
/>
</Stack>
)}
</Stack>
);
};
HeaderSort.propTypes = {
column: PropTypes.any,
sort: PropTypes.bool
};
// ==============================|| TABLE PAGINATION ||============================== //
export const TablePagination = ({ gotoPage, rows, setPageSize, pageSize, pageIndex }) => {
const [open, setOpen] = useState(false);
const handleClose = () => {
setOpen(false);
};
const handleOpen = () => {
setOpen(true);
};
const handleChangePagination = (event, value) => {
gotoPage(value - 1);
};
const handleChange = (event) => {
setPageSize(+event.target.value);
};
return (
<Grid container alignItems="center" justifyContent="space-between" sx={{ width: 'auto' }}>
<Grid item>
<Stack direction="row" spacing={1} alignItems="center">
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="caption" color="secondary">
Row per page
</Typography>
<FormControl sx={{ m: 1 }}>
<Select
id="demo-controlled-open-select"
open={open}
onClose={handleClose}
onOpen={handleOpen}
value={pageSize}
onChange={handleChange}
size="small"
sx={{ '& .MuiSelect-select': { py: 0.75, px: 1.25 } }}
>
<MenuItem value={5}>5</MenuItem>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={25}>25</MenuItem>
<MenuItem value={50}>50</MenuItem>
<MenuItem value={100}>100</MenuItem>
</Select>
</FormControl>
</Stack>
<Typography variant="caption" color="secondary">
Go to
</Typography>
<TextField
size="small"
type="number"
value={pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) : 0;
gotoPage(page - 1);
}}
sx={{ '& .MuiOutlinedInput-input': { py: 0.75, px: 1.25, width: 36 } }}
/>
</Stack>
</Grid>
<Grid item sx={{ mt: { xs: 2, sm: 0 } }}>
<Pagination
count={Math.ceil(rows.length / pageSize)}
page={pageIndex + 1}
onChange={handleChangePagination}
color="primary"
variant="combined"
showFirstButton
showLastButton
/>
</Grid>
</Grid>
);
};
TablePagination.propTypes = {
gotoPage: PropTypes.func,
setPageSize: PropTypes.func,
pageIndex: PropTypes.number,
pageSize: PropTypes.number,
rows: PropTypes.array
};
// ==============================|| SELECTION - PREVIEW ||============================== //
export const IndeterminateCheckbox = forwardRef(({ indeterminate, ...rest }, ref) => {
const defaultRef = useRef();
const resolvedRef = ref || defaultRef;
return <Checkbox indeterminate={indeterminate} ref={resolvedRef} {...rest} />;
});
IndeterminateCheckbox.propTypes = {
indeterminate: PropTypes.bool
};
export const TableRowSelection = ({ selected }) => (
<>
{selected > 0 && (
<Chip
size="small"
label={`${selected} row(s) selected`}
color="secondary"
variant="light"
sx={{
position: 'absolute',
right: -1,
top: -1,
borderRadius: '0 4px 0 4px'
}}
/>
)}
</>
);
TableRowSelection.propTypes = {
selected: PropTypes.number
};
// ==============================|| DRAG & DROP - DRAGGABLE HEADR ||============================== //
export const DraggableHeader = ({ children, column, index, reorder }) => {
const theme = useTheme();
const ref = useRef();
const { id, Header } = column;
const DND_ITEM_TYPE = 'column';
const [{ isOverCurrent }, drop] = useDrop({
accept: DND_ITEM_TYPE,
drop: (item) => {
reorder(item, index);
},
collect: (monitor) => ({
isOver: monitor.isOver(),
isOverCurrent: monitor.isOver({ shallow: true })
})
});
const [{ isDragging }, drag, preview] = useDrag({
type: DND_ITEM_TYPE,
item: () => ({
id,
index,
header: Header
}),
collect: (monitor) => ({
isDragging: monitor.isDragging()
})
});
useEffect(() => {
preview(getEmptyImage(), { captureDraggingState: true });
}, [preview]);
drag(drop(ref));
let borderColor = theme.palette.text.primary;
if (isOverCurrent) {
borderColor = theme.palette.primary.main;
}
return (
<Box sx={{ cursor: 'move', opacity: isDragging ? 0.5 : 1, color: borderColor }} ref={ref} {...isDragging}>
{children}
</Box>
);
};
DraggableHeader.propTypes = {
column: PropTypes.any,
sort: PropTypes.bool,
reorder: PropTypes.func,
index: PropTypes.number,
children: PropTypes.node
};
// ==============================|| DRAG & DROP - DRAG PREVIEW ||============================== //
const DragHeader = styled('div')(({ theme, x, y }) => ({
color: theme.palette.text.secondary,
position: 'fixed',
pointerEvents: 'none',
left: 12,
top: 24,
transform: `translate(${x}px, ${y}px)`,
opacity: 0.6
}));
export const DragPreview = () => {
const theme = useTheme();
const { isDragging, item, currentOffset } = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging()
}));
const { x, y } = currentOffset || {};
return isDragging ? (
<DragHeader theme={theme} x={x} y={y}>
{item.header && (
<Stack direction="row" spacing={1} alignItems="center">
<DragOutlined style={{ fontSize: '1rem' }} />
<Typography>{item.header}</Typography>
</Stack>
)}
</DragHeader>
) : null;
};
// ==============================|| DRAG & DROP - DRAGGABLE ROW ||============================== //
export const DraggableRow = ({ index, moveRow, children }) => {
const DND_ITEM_TYPE = 'row';
const dropRef = useRef(null);
const dragRef = useRef(null);
const [, drop] = useDrop({
accept: DND_ITEM_TYPE,
hover(item, monitor) {
if (!dropRef.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
if (dragIndex === hoverIndex) {
return;
}
const hoverBoundingRect = dropRef.current.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
moveRow(dragIndex, hoverIndex);
item.index = hoverIndex;
}
});
const [{ isDragging }, drag, preview] = useDrag({
type: DND_ITEM_TYPE,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging()
})
});
const opacity = isDragging ? 0 : 1;
preview(drop(dropRef));
drag(dragRef);
return (
<TableRow ref={dropRef} style={{ opacity, backgroundColor: isDragging ? 'red' : 'inherit' }}>
<TableCell ref={dragRef} sx={{ cursor: 'pointer', textAlign: 'center' }}>
<DragOutlined />
</TableCell>
{children}
</TableRow>
);
};
DraggableRow.propTypes = {
moveRow: PropTypes.func,
index: PropTypes.number,
children: PropTypes.node
};
// ==============================|| COLUMN HIDING - SELECT ||============================== //
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 200
}
}
};
export const HidingSelect = ({ hiddenColumns, setHiddenColumns, allColumns }) => {
const handleChange = (event) => {
const {
target: { value }
} = event;
setHiddenColumns(typeof value === 'string' ? value.split(',') : value);
};
const theme = useTheme();
let visible = allColumns.filter((c) => !hiddenColumns.includes(c.id)).length;
return (
<FormControl sx={{ width: 200 }}>
<Select
id="column-hiding"
multiple
displayEmpty
value={hiddenColumns}
onChange={handleChange}
input={<OutlinedInput id="select-column-hiding" placeholder="select column" />}
renderValue={(selected) => {
if (selected.length === 0) {
return <Typography variant="subtitle1">All columns visible</Typography>;
}
if (selected.length > 0 && selected.length === allColumns.length) {
return <Typography variant="subtitle1">All columns visible</Typography>;
}
return <Typography variant="subtitle1">{visible} column(s) visible</Typography>;
}}
MenuProps={MenuProps}
size="small"
>
{allColumns.map((column) => {
let ToggleChecked = column.id === '#' ? true : hiddenColumns.indexOf(column.id) > -1 ? false : true;
return (
<MenuItem
key={column.id}
value={column.id}
sx={{ bgcolor: 'success.lighter', '&.Mui-selected': { bgcolor: 'background.paper' } }}
>
<Checkbox
checked={ToggleChecked}
color="success"
checkedIcon={
<Box
className="icon"
sx={{
width: 16,
height: 16,
border: '1px solid',
borderColor: 'inherit',
borderRadius: 0.25,
position: 'relative',
backgroundColor: theme.palette.success.main
}}
>
<CheckOutlined className="filled" style={{ position: 'absolute', color: theme.palette.common.white }} />
</Box>
}
/>
<ListItemText primary={typeof column.Header === 'string' ? column.Header : column?.title} />
</MenuItem>
);
})}
</Select>
</FormControl>
);
};
HidingSelect.propTypes = {
setHiddenColumns: PropTypes.func,
hiddenColumns: PropTypes.array,
allColumns: PropTypes.array
};
// ==============================|| COLUMN SORTING - SELECT ||============================== //
export const SortingSelect = ({ sortBy, setSortBy, allColumns }) => {
const [sort, setSort] = useState(sortBy);
const handleChange = (event) => {
const {
target: { value }
} = event;
setSort(value);
setSortBy([{ id: value, desc: false }]);
};
return (
<FormControl sx={{ width: 200 }}>
<Select
id="column-hiding"
displayEmpty
value={sort}
onChange={handleChange}
input={<OutlinedInput id="select-column-hiding" placeholder="Sort by" />}
renderValue={(selected) => {
const selectedColumn = allColumns.filter((column) => column.id === selected)[0];
if (!selected) {
return <Typography variant="subtitle1">Sort By</Typography>;
}
return (
<Typography variant="subtitle2">
Sort by ({typeof selectedColumn.Header === 'string' ? selectedColumn.Header : selectedColumn?.title})
</Typography>
);
}}
size="small"
>
{allColumns
.filter((column) => column.canSort)
.map((column) => (
<MenuItem key={column.id} value={column.id}>
<ListItemText primary={typeof column.Header === 'string' ? column.Header : column?.title} />
</MenuItem>
))}
</Select>
</FormControl>
);
};
SortingSelect.propTypes = {
setSortBy: PropTypes.func,
sortBy: PropTypes.string,
allColumns: PropTypes.array
};
// ==============================|| CSV EXPORT ||============================== //
export const CSVExport = ({ data, filename, headers, label, style, btnLoading, onClick }) => {
return (
<CSVLink data={data} filename={filename} headers={headers}>
<Tooltip title="CSV Export">
<Button
startIcon={!btnLoading && <DownloadOutlined />}
variant={btnLoading ? 'outlined' : 'contained'}
sx={{ ...style }}
disabled={btnLoading}
onClick={(e) => {
// allow CSV download
onClick?.(e); // 🔑 call parent click
}}
>
{btnLoading ? <CircularProgress size={20} thickness={5} /> : label || 'Download'}
</Button>
</Tooltip>
</CSVLink>
);
};
export default CSVExport;
CSVExport.propTypes = {
data: PropTypes.array,
headers: PropTypes.any,
filename: PropTypes.string,
style: PropTypes.any,
label: PropTypes.any,
btnLoading: PropTypes.any
};
// ==============================|| EMPTY TABLE - NO DATA ||============================== //
const StyledGridOverlay = styled(Stack)(({ theme }) => ({
height: '400px',
'& .ant-empty-img-1': {
fill: theme.palette.mode === ThemeMode.DARK ? theme.palette.secondary[200] : theme.palette.secondary[400]
},
'& .ant-empty-img-2': {
fill: theme.palette.secondary.light
},
'& .ant-empty-img-3': {
fill: theme.palette.mode === ThemeMode.DARK ? theme.palette.secondary.A200 : theme.palette.secondary[200]
},
'& .ant-empty-img-4': {
fill: theme.palette.mode === ThemeMode.DARK ? theme.palette.secondary.A300 : theme.palette.secondary.A100
},
'& .ant-empty-img-5': {
fillOpacity: theme.palette.mode === ThemeMode.DARK ? '0.09' : '0.95',
fill: theme.palette.mode === ThemeMode.DARK ? theme.palette.secondary.darker : theme.palette.secondary.light
}
}));
export const EmptyTable = ({ msg, colSpan }) => {
return (
<TableRow>
<TableCell colSpan={colSpan}>
<StyledGridOverlay alignItems="center" justifyContent="center" spacing={1}>
<svg width="120" height="100" viewBox="0 0 184 152" aria-hidden focusable="false">
<g fill="none" fillRule="evenodd">
<g transform="translate(24 31.67)">
<ellipse className="ant-empty-img-5" cx="67.797" cy="106.89" rx="67.797" ry="12.668" />
<path
className="ant-empty-img-1"
d="M122.034 69.674L98.109 40.229c-1.148-1.386-2.826-2.225-4.593-2.225h-51.44c-1.766 0-3.444.839-4.592 2.225L13.56 69.674v15.383h108.475V69.674z"
/>
<path
className="ant-empty-img-2"
d="M33.83 0h67.933a4 4 0 0 1 4 4v93.344a4 4 0 0 1-4 4H33.83a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"
/>
<path
className="ant-empty-img-3"
d="M42.678 9.953h50.237a2 2 0 0 1 2 2V36.91a2 2 0 0 1-2 2H42.678a2 2 0 0 1-2-2V11.953a2 2 0 0 1 2-2zM42.94 49.767h49.713a2.262 2.262 0 1 1 0 4.524H42.94a2.262 2.262 0 0 1 0-4.524zM42.94 61.53h49.713a2.262 2.262 0 1 1 0 4.525H42.94a2.262 2.262 0 0 1 0-4.525zM121.813 105.032c-.775 3.071-3.497 5.36-6.735 5.36H20.515c-3.238 0-5.96-2.29-6.734-5.36a7.309 7.309 0 0 1-.222-1.79V69.675h26.318c2.907 0 5.25 2.448 5.25 5.42v.04c0 2.971 2.37 5.37 5.277 5.37h34.785c2.907 0 5.277-2.421 5.277-5.393V75.1c0-2.972 2.343-5.426 5.25-5.426h26.318v33.569c0 .617-.077 1.216-.221 1.789z"
/>
</g>
<path
className="ant-empty-img-3"
d="M149.121 33.292l-6.83 2.65a1 1 0 0 1-1.317-1.23l1.937-6.207c-2.589-2.944-4.109-6.534-4.109-10.408C138.802 8.102 148.92 0 161.402 0 173.881 0 184 8.102 184 18.097c0 9.995-10.118 18.097-22.599 18.097-4.528 0-8.744-1.066-12.28-2.902z"
/>
<g className="ant-empty-img-4" transform="translate(149.65 15.383)">
<ellipse cx="20.654" cy="3.167" rx="2.849" ry="2.815" />
<path d="M5.698 5.63H0L2.898.704zM9.259.704h4.985V5.63H9.259z" />
</g>
</g>
</svg>
<Typography align="center" color="secondary">
{msg}
</Typography>
</StyledGridOverlay>
</TableCell>
</TableRow>
);
};
EmptyTable.propTypes = {
msg: PropTypes.string,
colSpan: PropTypes.number
};

62
src/components/third-party/SimpleBar.js vendored Normal file
View File

@@ -0,0 +1,62 @@
import PropTypes from 'prop-types';
// material-ui
import { alpha, styled } from '@mui/material/styles';
import { Box } from '@mui/material';
// third-party
import SimpleBar from 'simplebar-react';
import { BrowserView, MobileView } from 'react-device-detect';
// root style
const RootStyle = styled(BrowserView)({
flexGrow: 1,
height: '100%',
overflow: 'hidden'
});
// scroll bar wrapper
const SimpleBarStyle = styled(SimpleBar)(({ theme }) => ({
maxHeight: '100%',
'& .simplebar-scrollbar': {
'&:before': {
backgroundColor: alpha(theme.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 SimpleBarScroll({ children, sx, ...other }) {
return (
<>
<RootStyle>
<SimpleBarStyle clickOnTrack={false} sx={sx} {...other}>
{children}
</SimpleBarStyle>
</RootStyle>
<MobileView>
<Box sx={{ overflowX: 'auto', ...sx }} {...other}>
{children}
</Box>
</MobileView>
</>
);
}
SimpleBarScroll.propTypes = {
children: PropTypes.node,
sx: PropTypes.object
};

View File

@@ -0,0 +1,96 @@
import { useEffect, useState, useRef } from 'react';
import { useTheme } from '@mui/material/styles';
import WifiOffIcon from '@mui/icons-material/WifiOff';
import WifiIcon from '@mui/icons-material/Wifi';
const NOTIFICATION_STYLE = {
position: 'fixed',
top: '10px', // Keep top spacing
left: '50%', // Center horizontally
transform: 'translateX(-50%)', // Offset to truly center
padding: '12px 40px',
borderRadius: '8px',
color: 'white',
fontWeight: '500',
fontSize: '14px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
zIndex: 9999,
maxWidth: '90%',
minWidth: 'auto',
textAlign: 'center'
};
const InternetStatus = () => {
const theme = useTheme();
const [isOnline, setIsOnline] = useState(navigator.onLine);
const [showBackOnline, setShowBackOnline] = useState(false);
const wasOffline = useRef(!navigator.onLine);
useEffect(() => {
const handleNetworkChange = () => {
const online = navigator.onLine;
setIsOnline(online);
if (online && wasOffline.current) {
console.log('✅ Back online');
setShowBackOnline(true);
wasOffline.current = false;
// window.location.reload();
setTimeout(() => {
setShowBackOnline(false);
}, 2000);
}
if (!online) {
console.log('❌ No internet connection');
wasOffline.current = true;
setShowBackOnline(false);
}
};
window.addEventListener('online', handleNetworkChange);
window.addEventListener('offline', handleNetworkChange);
return () => {
window.removeEventListener('online', handleNetworkChange);
window.removeEventListener('offline', handleNetworkChange);
};
}, []);
return (
<>
{!isOnline && (
<div
style={{
...NOTIFICATION_STYLE,
backgroundColor: theme.palette.error.main,
display: 'flex',
alignItems: 'center',
gap: '10px'
}}
>
<WifiOffIcon style={{ color: '#fff' }} />
No Internet Connection
</div>
)}
{showBackOnline && (
<div
style={{
...NOTIFICATION_STYLE,
backgroundColor: theme.palette.success.main,
display: 'flex',
alignItems: 'center',
gap: '10px'
}}
>
<WifiIcon style={{ color: '#fff' }} />
Back Online
</div>
)}
</>
);
};
export default InternetStatus;