initial commit
This commit is contained in:
88
src/components/@extended/AnimateButton.js
Normal file
88
src/components/@extended/AnimateButton.js
Normal 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])
|
||||
};
|
||||
119
src/components/@extended/Avatar.js
Normal file
119
src/components/@extended/Avatar.js
Normal 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
|
||||
};
|
||||
222
src/components/@extended/Breadcrumbs.js
Normal file
222
src/components/@extended/Breadcrumbs.js
Normal 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;
|
||||
39
src/components/@extended/Dot.js
Normal file
39
src/components/@extended/Dot.js
Normal 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;
|
||||
162
src/components/@extended/IconButton.js
Normal file
162
src/components/@extended/IconButton.js
Normal 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;
|
||||
231
src/components/@extended/LoadingButton.js
Normal file
231
src/components/@extended/LoadingButton.js
Normal 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;
|
||||
128
src/components/@extended/Snackbar.js
Normal file
128
src/components/@extended/Snackbar.js
Normal 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;
|
||||
67
src/components/@extended/Tooltip.js
Normal file
67
src/components/@extended/Tooltip.js
Normal 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
|
||||
};
|
||||
116
src/components/@extended/Transitions.js
Normal file
116
src/components/@extended/Transitions.js
Normal 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} />;
|
||||
});
|
||||
32
src/components/@extended/progress/CircularWithLabel.js
Normal file
32
src/components/@extended/progress/CircularWithLabel.js
Normal 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
|
||||
};
|
||||
65
src/components/@extended/progress/CircularWithPath.js
Normal file
65
src/components/@extended/progress/CircularWithPath.js
Normal 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
|
||||
};
|
||||
22
src/components/@extended/progress/LinearWithIcon.js
Normal file
22
src/components/@extended/progress/LinearWithIcon.js
Normal 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
|
||||
};
|
||||
23
src/components/@extended/progress/LinearWithLabel.js
Normal file
23
src/components/@extended/progress/LinearWithLabel.js
Normal 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
|
||||
};
|
||||
47
src/components/CircularLoader.js
Normal file
47
src/components/CircularLoader.js
Normal 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;
|
||||
66
src/components/DateFilterDialog.js
Normal file
66
src/components/DateFilterDialog.js
Normal 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;
|
||||
15
src/components/Loadable.js
Normal file
15
src/components/Loadable.js
Normal 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
25
src/components/Loader.js
Normal 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
43
src/components/Locales.js
Normal 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
141
src/components/MainCard.js
Normal 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;
|
||||
37
src/components/RTLLayout.js
Normal file
37
src/components/RTLLayout.js
Normal 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;
|
||||
24
src/components/ScrollTop.js
Normal file
24
src/components/ScrollTop.js
Normal 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
10
src/components/ScrollX.js
Normal 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;
|
||||
65
src/components/SecondaryAction.js
Normal file
65
src/components/SecondaryAction.js
Normal 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;
|
||||
61
src/components/cards/AuthFooter.js
Normal file
61
src/components/cards/AuthFooter.js
Normal 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;
|
||||
49
src/components/cards/ComponentHeader.js
Normal file
49
src/components/cards/ComponentHeader.js
Normal 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;
|
||||
52
src/components/cards/e-commerce/FloatingCart.js
Normal file
52
src/components/cards/e-commerce/FloatingCart.js
Normal 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;
|
||||
176
src/components/cards/e-commerce/ProductCard.js
Normal file
176
src/components/cards/e-commerce/ProductCard.js
Normal 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;
|
||||
52
src/components/cards/e-commerce/ProductReview.js
Normal file
52
src/components/cards/e-commerce/ProductReview.js
Normal 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;
|
||||
44
src/components/cards/skeleton/ProductPlaceholder.js
Normal file
44
src/components/cards/skeleton/ProductPlaceholder.js
Normal 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;
|
||||
66
src/components/cards/statistics/AnalyticEcommerce.js
Normal file
66
src/components/cards/statistics/AnalyticEcommerce.js
Normal 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;
|
||||
56
src/components/cards/statistics/AnalyticsDataCard.js
Normal file
56
src/components/cards/statistics/AnalyticsDataCard.js
Normal 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;
|
||||
70
src/components/cards/statistics/HoverSocialCard.js
Normal file
70
src/components/cards/statistics/HoverSocialCard.js
Normal 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;
|
||||
56
src/components/logo/LogoIcon.js
Normal file
56
src/components/logo/LogoIcon.js
Normal 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;
|
||||
68
src/components/logo/LogoMain.js
Normal file
68
src/components/logo/LogoMain.js
Normal 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;
|
||||
27
src/components/logo/index.js
Normal file
27
src/components/logo/index.js
Normal 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;
|
||||
66
src/components/nearle_components/DebounceSearchBar.js
Normal file
66
src/components/nearle_components/DebounceSearchBar.js
Normal 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;
|
||||
30
src/components/nearle_components/GlobalToast.js
Normal file
30
src/components/nearle_components/GlobalToast.js
Normal 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);
|
||||
}
|
||||
};
|
||||
34
src/components/nearle_components/LoaderWithImage.js
Normal file
34
src/components/nearle_components/LoaderWithImage.js
Normal 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>
|
||||
);
|
||||
}
|
||||
51
src/components/nearle_components/LocationAutocomplete.js
Normal file
51
src/components/nearle_components/LocationAutocomplete.js
Normal 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;
|
||||
77
src/components/nearle_components/SearchBar.js
Normal file
77
src/components/nearle_components/SearchBar.js
Normal 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;
|
||||
18
src/components/nearle_components/TableLoader.js
Normal file
18
src/components/nearle_components/TableLoader.js
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
22
src/components/nearle_components/TitleCard.js
Normal file
22
src/components/nearle_components/TitleCard.js
Normal 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
65
src/components/third-party/Notistack.js
vendored
Normal 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;
|
||||
9
src/components/third-party/OpenToast.js
vendored
Normal file
9
src/components/third-party/OpenToast.js
vendored
Normal 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
595
src/components/third-party/ReactTable.js
vendored
Normal 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
62
src/components/third-party/SimpleBar.js
vendored
Normal 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
|
||||
};
|
||||
96
src/components/updateNetworkStatus.js
Normal file
96
src/components/updateNetworkStatus.js
Normal 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;
|
||||
Reference in New Issue
Block a user