Separate Client Portal & Dashboard

This commit is contained in:
2022-05-23 10:38:16 +07:00
parent f2e84e6244
commit 89bb57f357
569 changed files with 60252 additions and 280 deletions

View File

@@ -0,0 +1,100 @@
// @mui
import { Theme, useTheme, styled } from '@mui/material/styles';
import { BoxProps } from '@mui/material';
// ----------------------------------------------------------------------
export type BadgeStatusEnum =
| 'away'
| 'busy'
| 'unread'
| 'online'
| 'offline'
| 'invisible'
| string;
type BadgeSize = 'small' | 'medium' | 'large';
const RootStyle = styled('span')(
({
theme,
ownerState,
}: {
theme: Theme;
ownerState: {
size: BadgeSize;
status: BadgeStatusEnum;
};
}) => {
const { status, size } = ownerState;
return {
width: 10,
height: 10,
display: 'flex',
borderRadius: '50%',
alignItems: 'center',
justifyContent: 'center',
'&:before, &:after': {
content: "''",
borderRadius: 1,
backgroundColor: theme.palette.common.white,
},
...(size === 'small' && { width: 8, height: 8 }),
...(size === 'large' && { width: 12, height: 12 }),
...(status === 'offline' && { backgroundColor: 'transparent' }),
...(status === 'away' && {
backgroundColor: theme.palette.warning.main,
'&:before': {
width: 2,
height: 4,
transform: 'translateX(1px) translateY(-1px)',
},
'&:after': {
width: 2,
height: 4,
transform: 'translateY(1px) rotate(125deg)',
},
}),
...(status === 'busy' && {
backgroundColor: theme.palette.error.main,
'&:before': { width: 6, height: 2 },
}),
...(status === 'online' && {
backgroundColor: theme.palette.success.main,
}),
...(status === 'invisible' && {
backgroundColor: theme.palette.text.disabled,
'&:before': {
width: 6,
height: 6,
borderRadius: '50%',
},
}),
...(status === 'unread' && {
backgroundColor: theme.palette.info.main,
}),
};
}
);
// ----------------------------------------------------------------------
interface Props extends BoxProps {
size?: BadgeSize;
status?: BadgeStatusEnum;
}
export default function BadgeStatus({ size = 'medium', status = 'offline', sx }: Props) {
const theme = useTheme();
return <RootStyle ownerState={{ status, size }} sx={sx} theme={theme} />;
}

View File

@@ -0,0 +1,15 @@
// icons
import { Icon, IconifyIcon } from '@iconify/react';
// @mui
import { Box, BoxProps, SxProps } from '@mui/material';
// ----------------------------------------------------------------------
interface Props extends BoxProps {
sx?: SxProps;
icon: IconifyIcon | string;
}
export default function Iconify({ icon, sx, ...other }: Props) {
return <Box component={Icon} icon={icon} sx={{ ...sx }} {...other} />;
}

View File

@@ -0,0 +1,97 @@
import { LazyLoadImage, LazyLoadImageProps } from 'react-lazy-load-image-component';
// @mui
import { Theme } from '@mui/material/styles';
import { Box, BoxProps, SxProps } from '@mui/material';
// ----------------------------------------------------------------------
export type ImageRato = '4/3' | '3/4' | '6/4' | '4/6' | '16/9' | '9/16' | '21/9' | '9/21' | '1/1';
type IProps = BoxProps & LazyLoadImageProps;
interface Props extends IProps {
sx?: SxProps<Theme>;
ratio?: ImageRato;
disabledEffect?: boolean;
}
export default function Image({
ratio,
disabledEffect = false,
effect = 'blur',
sx,
...other
}: Props) {
if (ratio) {
return (
<Box
component="span"
sx={{
width: 1,
lineHeight: 0,
display: 'block',
overflow: 'hidden',
position: 'relative',
pt: getRatio(ratio),
'& .wrapper': {
top: 0,
left: 0,
right: 0,
bottom: 0,
lineHeight: 0,
position: 'absolute',
backgroundSize: 'cover !important',
},
...sx,
}}
>
<Box
component={LazyLoadImage}
wrapperClassName="wrapper"
effect={disabledEffect ? undefined : effect}
placeholderSrc="https://zone-assets-api.vercel.app/assets/img_placeholder.svg"
sx={{ width: 1, height: 1, objectFit: 'cover' }}
{...other}
/>
</Box>
);
}
return (
<Box
component="span"
sx={{
lineHeight: 0,
display: 'block',
overflow: 'hidden',
'& .wrapper': { width: 1, height: 1, backgroundSize: 'cover !important' },
...sx,
}}
>
<Box
component={LazyLoadImage}
wrapperClassName="wrapper"
effect={disabledEffect ? undefined : effect}
placeholderSrc="https://zone-assets-api.vercel.app/assets/img_placeholder.svg"
sx={{ width: 1, height: 1, objectFit: 'cover' }}
{...other}
/>
</Box>
);
}
// ----------------------------------------------------------------------
function getRatio(ratio = '1/1') {
return {
'4/3': 'calc(100% / 4 * 3)',
'3/4': 'calc(100% / 3 * 4)',
'6/4': 'calc(100% / 6 * 4)',
'4/6': 'calc(100% / 4 * 6)',
'16/9': 'calc(100% / 16 * 9)',
'9/16': 'calc(100% / 9 * 16)',
'21/9': 'calc(100% / 21 * 9)',
'9/21': 'calc(100% / 9 * 21)',
'1/1': '100%',
}[ratio];
}

View File

@@ -0,0 +1,94 @@
import { m } from 'framer-motion';
// @mui
import { alpha, styled } from '@mui/material/styles';
import { Box, SxProps } from '@mui/material';
//
import Logo from './Logo';
import ProgressBar from './ProgressBar';
// ----------------------------------------------------------------------
const RootStyle = styled('div')(({ theme }) => ({
right: 0,
bottom: 0,
zIndex: 99999,
width: '100%',
height: '100%',
position: 'fixed',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: theme.palette.background.default,
}));
// ----------------------------------------------------------------------
type Props = {
isDashboard?: boolean;
sx?: SxProps;
};
export default function LoadingScreen({ isDashboard, ...other }: Props) {
return (
<>
<ProgressBar />
{!isDashboard && (
<RootStyle {...other}>
<m.div
initial={{ rotateY: 0 }}
animate={{ rotateY: 360 }}
transition={{
duration: 2,
ease: 'easeInOut',
repeatDelay: 1,
repeat: Infinity,
}}
>
<Logo disabledLink sx={{ width: 64, height: 64 }} />
</m.div>
<Box
component={m.div}
animate={{
scale: [1.2, 1, 1, 1.2, 1.2],
rotate: [270, 0, 0, 270, 270],
opacity: [0.25, 1, 1, 1, 0.25],
borderRadius: ['25%', '25%', '50%', '50%', '25%'],
}}
transition={{ ease: 'linear', duration: 3.2, repeat: Infinity }}
sx={{
width: 100,
height: 100,
borderRadius: '25%',
position: 'absolute',
border: (theme) => `solid 3px ${alpha(theme.palette.primary.dark, 0.24)}`,
}}
/>
<Box
component={m.div}
animate={{
scale: [1, 1.2, 1.2, 1, 1],
rotate: [0, 270, 270, 0, 0],
opacity: [1, 0.25, 0.25, 0.25, 1],
borderRadius: ['25%', '25%', '50%', '50%', '25%'],
}}
transition={{
ease: 'linear',
duration: 3.2,
repeat: Infinity,
}}
sx={{
width: 120,
height: 120,
borderRadius: '25%',
position: 'absolute',
border: (theme) => `solid 8px ${alpha(theme.palette.primary.dark, 0.24)}`,
}}
/>
</RootStyle>
)}
</>
);
}

View File

@@ -0,0 +1,58 @@
import { Link as RouterLink } from 'react-router-dom';
// @mui
import { useTheme } from '@mui/material/styles';
import { Box, BoxProps } from '@mui/material';
// ----------------------------------------------------------------------
interface Props extends BoxProps {
disabledLink?: boolean;
}
export default function Logo({ disabledLink = false, sx }: Props) {
const theme = useTheme();
const PRIMARY_LIGHT = theme.palette.primary.light;
const PRIMARY_MAIN = theme.palette.primary.main;
const PRIMARY_DARK = theme.palette.primary.dark;
const logo = (
<Box sx={{ width: 40, height: 40, ...sx }}>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 512 512">
<defs>
<linearGradient id="BG1" x1="100%" x2="50%" y1="9.946%" y2="50%">
<stop offset="0%" stopColor={PRIMARY_DARK} />
<stop offset="100%" stopColor={PRIMARY_MAIN} />
</linearGradient>
<linearGradient id="BG2" x1="50%" x2="50%" y1="0%" y2="100%">
<stop offset="0%" stopColor={PRIMARY_LIGHT} />
<stop offset="100%" stopColor={PRIMARY_MAIN} />
</linearGradient>
<linearGradient id="BG3" x1="50%" x2="50%" y1="0%" y2="100%">
<stop offset="0%" stopColor={PRIMARY_LIGHT} />
<stop offset="100%" stopColor={PRIMARY_MAIN} />
</linearGradient>
</defs>
<g fill={PRIMARY_MAIN} fillRule="evenodd" stroke="none" strokeWidth="1">
<path
fill="url(#BG1)"
d="M183.168 285.573l-2.918 5.298-2.973 5.363-2.846 5.095-2.274 4.043-2.186 3.857-2.506 4.383-1.6 2.774-2.294 3.939-1.099 1.869-1.416 2.388-1.025 1.713-1.317 2.18-.95 1.558-1.514 2.447-.866 1.38-.833 1.312-.802 1.246-.77 1.18-.739 1.111-.935 1.38-.664.956-.425.6-.41.572-.59.8-.376.497-.537.69-.171.214c-10.76 13.37-22.496 23.493-36.93 29.334-30.346 14.262-68.07 14.929-97.202-2.704l72.347-124.682 2.8-1.72c49.257-29.326 73.08 1.117 94.02 40.927z"
/>
<path
fill="url(#BG2)"
d="M444.31 229.726c-46.27-80.956-94.1-157.228-149.043-45.344-7.516 14.384-12.995 42.337-25.267 42.337v-.142c-12.272 0-17.75-27.953-25.265-42.337C189.79 72.356 141.96 148.628 95.69 229.584c-3.483 6.106-6.828 11.932-9.69 16.996 106.038-67.127 97.11 135.667 184 137.278V384c86.891-1.611 77.962-204.405 184-137.28-2.86-5.062-6.206-10.888-9.69-16.994"
/>
<path
fill="url(#BG3)"
d="M450 384c26.509 0 48-21.491 48-48s-21.491-48-48-48-48 21.491-48 48 21.491 48 48 48"
/>
</g>
</svg>
</Box>
);
if (disabledLink) {
return <>{logo}</>;
}
return <RouterLink to="/">{logo}</RouterLink>;
}

View File

@@ -0,0 +1,120 @@
// @mui
import { styled } from '@mui/material/styles';
import { Popover, PopoverProps } from '@mui/material';
// ----------------------------------------------------------------------
type Arrow =
| 'top-left'
| 'top-center'
| 'top-right'
| 'bottom-left'
| 'bottom-center'
| 'bottom-right'
| 'left-top'
| 'left-center'
| 'left-bottom'
| 'right-top'
| 'right-center'
| 'right-bottom';
type ArrowStyleProps = {
arrow: Arrow;
};
const ArrowStyle = styled('span')<ArrowStyleProps>(({ arrow, theme }) => {
const SIZE = 12;
const POSITION = -(SIZE / 2);
const borderStyle = `solid 1px ${theme.palette.grey[500_12]}`;
const topStyle = {
borderRadius: '0 0 3px 0',
top: POSITION,
borderBottom: borderStyle,
borderRight: borderStyle,
};
const bottomStyle = {
borderRadius: '3px 0 0 0',
bottom: POSITION,
borderTop: borderStyle,
borderLeft: borderStyle,
};
const leftStyle = {
borderRadius: '0 3px 0 0',
left: POSITION,
borderTop: borderStyle,
borderRight: borderStyle,
};
const rightStyle = {
borderRadius: '0 0 0 3px',
right: POSITION,
borderBottom: borderStyle,
borderLeft: borderStyle,
};
return {
[theme.breakpoints.up('sm')]: {
zIndex: 1,
width: SIZE,
height: SIZE,
content: "''",
position: 'absolute',
transform: 'rotate(-135deg)',
background: theme.palette.background.paper,
},
// Top
...(arrow === 'top-left' && { ...topStyle, left: 20 }),
...(arrow === 'top-center' && { ...topStyle, left: 0, right: 0, margin: 'auto' }),
...(arrow === 'top-right' && { ...topStyle, right: 20 }),
// Bottom
...(arrow === 'bottom-left' && { ...bottomStyle, left: 20 }),
...(arrow === 'bottom-center' && { ...bottomStyle, left: 0, right: 0, margin: 'auto' }),
...(arrow === 'bottom-right' && { ...bottomStyle, right: 20 }),
// Left
...(arrow === 'left-top' && { ...leftStyle, top: 20 }),
...(arrow === 'left-center' && { ...leftStyle, top: 0, bottom: 0, margin: 'auto' }),
...(arrow === 'left-bottom' && { ...leftStyle, bottom: 20 }),
// Right
...(arrow === 'right-top' && { ...rightStyle, top: 20 }),
...(arrow === 'right-center' && { ...rightStyle, top: 0, bottom: 0, margin: 'auto' }),
...(arrow === 'right-bottom' && { ...rightStyle, bottom: 20 }),
};
});
// ----------------------------------------------------------------------
interface Props extends PopoverProps {
arrow?: Arrow;
disabledArrow?: boolean;
}
export default function MenuPopover({
children,
arrow = 'top-right',
disabledArrow,
sx,
...other
}: Props) {
return (
<Popover
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
PaperProps={{
sx: {
p: 1,
width: 200,
overflow: 'inherit',
...sx,
},
}}
{...other}
>
{!disabledArrow && <ArrowStyle arrow={arrow} />}
{children}
</Popover>
);
}

View File

@@ -0,0 +1,27 @@
import { Helmet } from 'react-helmet-async';
import { forwardRef, ReactNode } from 'react';
// @mui
import { Box, BoxProps } from '@mui/material';
// ----------------------------------------------------------------------
interface Props extends BoxProps {
children: ReactNode;
meta?: ReactNode;
title: string;
}
const Page = forwardRef<HTMLDivElement, Props>(({ children, title = '', meta, ...other }, ref) => (
<>
<Helmet>
<title>{`${title} | Minimal-UI`}</title>
{meta}
</Helmet>
<Box ref={ref} {...other}>
{children}
</Box>
</>
));
export default Page;

View File

@@ -0,0 +1,57 @@
import NProgress from 'nprogress';
import { useEffect, useMemo } from 'react';
// @mui
import { useTheme } from '@mui/material/styles';
import { GlobalStyles } from '@mui/material';
// ----------------------------------------------------------------------
export function ProgressBarStyle() {
const theme = useTheme();
return (
<GlobalStyles
styles={{
'#nprogress': {
pointerEvents: 'none',
'& .bar': {
top: 0,
left: 0,
height: 2,
width: '100%',
position: 'fixed',
zIndex: theme.zIndex.snackbar,
backgroundColor: theme.palette.primary.main,
boxShadow: `0 0 2px ${theme.palette.primary.main}`
},
'& .peg': {
right: 0,
opacity: 1,
width: 100,
height: '100%',
display: 'block',
position: 'absolute',
transform: 'rotate(3deg) translate(0px, -4px)',
boxShadow: `0 0 10px ${theme.palette.primary.main}, 0 0 5px ${theme.palette.primary.main}`
}
}
}}
/>
);
}
export default function ProgressBar() {
NProgress.configure({
showSpinner: false
});
useMemo(() => {
NProgress.start();
}, []);
useEffect(() => {
NProgress.done();
}, []);
return null;
}

View File

@@ -0,0 +1,29 @@
import { useEffect, ReactNode } from 'react';
// rtl
import rtlPlugin from 'stylis-plugin-rtl';
// emotion
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
// @mui
import { useTheme } from '@mui/material/styles';
// ----------------------------------------------------------------------
type Props = {
children: ReactNode;
};
export default function RtlLayout({ children }: Props) {
const theme = useTheme();
useEffect(() => {
document.dir = theme.direction;
}, [theme.direction]);
const cacheRtl = createCache({
key: theme.direction === 'rtl' ? 'rtl' : 'css',
stylisPlugins: theme.direction === 'rtl' ? [rtlPlugin] : [],
});
return <CacheProvider value={cacheRtl}>{children}</CacheProvider>;
}

View File

@@ -0,0 +1,14 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
// ----------------------------------------------------------------------
export default function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}

View File

@@ -0,0 +1,61 @@
import SimpleBarReact, { Props as ScrollbarProps } from 'simplebar-react';
// @mui
import { alpha, styled } from '@mui/material/styles';
import { Box, SxProps } from '@mui/material';
// ----------------------------------------------------------------------
const RootStyle = styled('div')(() => ({
flexGrow: 1,
height: '100%',
overflow: 'hidden',
}));
const SimpleBarStyle = styled(SimpleBarReact)(({ theme }) => ({
maxHeight: '100%',
'& .simplebar-scrollbar': {
'&:before': {
backgroundColor: alpha(theme.palette.grey[600], 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',
},
}));
// ----------------------------------------------------------------------
interface Props extends ScrollbarProps {
sx?: SxProps;
}
export default function Scrollbar({ children, sx, ...other }: Props) {
const userAgent = typeof navigator === 'undefined' ? 'SSR' : navigator.userAgent;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
if (isMobile) {
return (
<Box sx={{ overflowX: 'auto', ...sx }} {...other}>
{children}
</Box>
);
}
return (
<RootStyle>
<SimpleBarStyle timeout={500} clickOnTrack={false} sx={sx} {...other}>
{children}
</SimpleBarStyle>
</RootStyle>
);
}

View File

@@ -0,0 +1,24 @@
import { Box, BoxProps } from '@mui/material';
// ----------------------------------------------------------------------
interface Props extends BoxProps {
src: string;
}
export default function SvgIconStyle({ src, sx }: Props) {
return (
<Box
component="span"
sx={{
width: 24,
height: 24,
display: 'inline-block',
bgcolor: 'currentColor',
mask: `url(${src}) no-repeat center / contain`,
WebkitMask: `url(${src}) no-repeat center / contain`,
...sx,
}}
/>
);
}

View File

@@ -0,0 +1,38 @@
import { ReactNode, useMemo } from 'react';
// @mui
import { alpha, ThemeProvider, createTheme, useTheme } from '@mui/material/styles';
// hooks
import useSettings from '../hooks/useSettings';
//
import componentsOverride from '../theme/overrides';
// ----------------------------------------------------------------------
type Props = {
children: ReactNode;
};
export default function ThemeColorPresets({ children }: Props) {
const defaultTheme = useTheme();
const { setColor } = useSettings();
const themeOptions = useMemo(
() => ({
...defaultTheme,
palette: {
...defaultTheme.palette,
primary: setColor,
},
customShadows: {
...defaultTheme.customShadows,
primary: `0 8px 16px 0 ${alpha(setColor.main, 0.24)}`,
},
}),
[setColor, defaultTheme]
);
const theme = createTheme(themeOptions);
theme.components = componentsOverride(theme);
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}

View File

@@ -0,0 +1,61 @@
import { m, AnimatePresence } from 'framer-motion';
// @mui
import { Dialog, Box, Paper, DialogProps } from '@mui/material';
//
import { varFade } from './variants';
// ----------------------------------------------------------------------
export interface Props extends DialogProps {
variants?: Record<string, unknown>;
onClose?: VoidFunction;
}
export default function DialogAnimate({
open = false,
variants,
onClose,
children,
sx,
...other
}: Props) {
return (
<AnimatePresence>
{open && (
<Dialog
fullWidth
maxWidth="xs"
open={open}
onClose={onClose}
PaperComponent={(props) => (
<Box
component={m.div}
{...(variants ||
varFade({
distance: 120,
durationIn: 0.32,
durationOut: 0.24,
easeIn: 'easeInOut',
}).inUp)}
sx={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Box onClick={onClose} sx={{ width: '100%', height: '100%', position: 'fixed' }} />
<Paper sx={sx} {...props}>
{props.children}
</Paper>
</Box>
)}
{...other}
>
{children}
</Dialog>
)}
</AnimatePresence>
);
}

View File

@@ -0,0 +1,107 @@
import { m } from 'framer-motion';
import { forwardRef, ReactNode } from 'react';
// @mui
import { useTheme } from '@mui/material/styles';
import { Box, Fab, FabProps, SxProps } from '@mui/material';
// ----------------------------------------------------------------------
interface Props extends Omit<FabProps, 'color'> {
sxWrap?: SxProps;
color?:
| 'inherit'
| 'default'
| 'primary'
| 'secondary'
| 'info'
| 'success'
| 'warning'
| 'error';
}
const FabButtonAnimate = forwardRef<HTMLButtonElement, Props>(
({ color = 'primary', size = 'large', children, sx, sxWrap, ...other }, ref) => {
const theme = useTheme();
if (
color === 'default' ||
color === 'inherit' ||
color === 'primary' ||
color === 'secondary'
) {
return (
<AnimateWrap size={size} sxWrap={sxWrap}>
<Fab ref={ref} size={size} color={color} sx={sx} {...other}>
{children}
</Fab>
</AnimateWrap>
);
}
return (
<AnimateWrap size={size} sxWrap={sxWrap}>
<Fab
ref={ref}
size={size}
sx={{
boxShadow: theme.customShadows[color],
color: theme.palette[color].contrastText,
bgcolor: theme.palette[color].main,
'&:hover': {
bgcolor: theme.palette[color].dark,
},
...sx,
}}
{...other}
>
{children}
</Fab>
</AnimateWrap>
);
}
);
export default FabButtonAnimate;
// ----------------------------------------------------------------------
type AnimateWrapProp = {
children: ReactNode;
size: 'small' | 'medium' | 'large';
sxWrap?: SxProps;
};
const varSmall = {
hover: { scale: 1.07 },
tap: { scale: 0.97 },
};
const varMedium = {
hover: { scale: 1.06 },
tap: { scale: 0.98 },
};
const varLarge = {
hover: { scale: 1.05 },
tap: { scale: 0.99 },
};
function AnimateWrap({ size, children, sxWrap }: AnimateWrapProp) {
const isSmall = size === 'small';
const isLarge = size === 'large';
return (
<Box
component={m.div}
whileTap="tap"
whileHover="hover"
variants={(isSmall && varSmall) || (isLarge && varLarge) || varMedium}
sx={{
display: 'inline-flex',
...sxWrap,
}}
>
{children}
</Box>
);
}

View File

@@ -0,0 +1,59 @@
import { m } from 'framer-motion';
import { forwardRef, ReactNode } from 'react';
// @mui
import { Box, IconButton, IconButtonProps } from '@mui/material';
// ----------------------------------------------------------------------
const IconButtonAnimate = forwardRef<HTMLButtonElement, IconButtonProps>(
({ children, size = 'medium', ...other }, ref) => (
<AnimateWrap size={size}>
<IconButton size={size} ref={ref} {...other}>
{children}
</IconButton>
</AnimateWrap>
)
);
export default IconButtonAnimate;
// ----------------------------------------------------------------------
type AnimateWrapProp = {
children: ReactNode;
size: 'small' | 'medium' | 'large';
};
const varSmall = {
hover: { scale: 1.1 },
tap: { scale: 0.95 },
};
const varMedium = {
hover: { scale: 1.09 },
tap: { scale: 0.97 },
};
const varLarge = {
hover: { scale: 1.08 },
tap: { scale: 0.99 },
};
function AnimateWrap({ size, children }: AnimateWrapProp) {
const isSmall = size === 'small';
const isLarge = size === 'large';
return (
<Box
component={m.div}
whileTap="tap"
whileHover="hover"
variants={(isSmall && varSmall) || (isLarge && varLarge) || varMedium}
sx={{
display: 'inline-flex',
}}
>
{children}
</Box>
);
}

View File

@@ -0,0 +1,43 @@
import { m, MotionProps } from 'framer-motion';
// @mui
import { Box, BoxProps } from '@mui/material';
//
import { varContainer } from './variants';
// ----------------------------------------------------------------------
type IProps = BoxProps & MotionProps;
export interface Props extends IProps {
animate?: boolean;
action?: boolean;
}
export default function MotionContainer({ animate, action = false, children, ...other }: Props) {
if (action) {
return (
<Box
component={m.div}
initial={false}
animate={animate ? 'animate' : 'exit'}
variants={varContainer()}
{...other}
>
{children}
</Box>
);
}
return (
<Box
component={m.div}
initial="initial"
animate="animate"
exit="exit"
variants={varContainer()}
{...other}
>
{children}
</Box>
);
}

View File

@@ -0,0 +1,50 @@
import { useEffect } from 'react';
import { m, useAnimation, MotionProps } from 'framer-motion';
import { useInView } from 'react-intersection-observer';
// @mui
import { Box, BoxProps } from '@mui/material';
// ----------------------------------------------------------------------
type Props = BoxProps & MotionProps;
interface MotionInViewProps extends Props {
threshold?: number | number[];
}
export default function MotionInView({
children,
variants,
transition,
threshold,
...other
}: MotionInViewProps) {
const controls = useAnimation();
const [ref, inView] = useInView({
threshold: threshold || 0,
triggerOnce: true,
});
useEffect(() => {
if (!variants) return;
if (inView) {
controls.start(Object.keys(variants)[1]);
} else {
controls.start(Object.keys(variants)[0]);
}
}, [controls, inView, variants]);
return (
<Box
ref={ref}
component={m.div}
initial={variants ? Object.keys(variants)[0] : false}
animate={controls}
variants={variants}
transition={transition}
{...other}
>
{children}
</Box>
);
}

View File

@@ -0,0 +1,20 @@
import { ReactNode } from 'react';
import { LazyMotion } from 'framer-motion';
import features from "./features";
// ----------------------------------------------------------------------
// eslint-disable-next-line import/extensions
// const loadFeatures = () => import('~/src/components/animate/features.js').then((res) => res.default);
type Props = {
children: ReactNode;
};
export default function MotionLazyContainer({ children }: Props) {
return (
<LazyMotion strict features={features}>
{children}
</LazyMotion>
);
}

View File

@@ -0,0 +1,34 @@
import { m, MotionProps } from 'framer-motion';
// @mui
import { Box, BoxProps } from '@mui/material';
//
import { varFade } from './variants';
// ----------------------------------------------------------------------
type Props = BoxProps & MotionProps;
interface TextAnimateProps extends Props {
text: string;
}
export default function TextAnimate({ text, variants, sx, ...other }: TextAnimateProps) {
return (
<Box
component={m.h1}
sx={{
typography: 'h1',
overflow: 'hidden',
display: 'inline-flex',
...sx,
}}
{...other}
>
{text.split('').map((letter, index) => (
<m.span key={index} variants={variants || varFade().inUp}>
{letter}
</m.span>
))}
</Box>
);
}

View File

@@ -0,0 +1,3 @@
import { domMax } from 'framer-motion';
export default domMax;

View File

@@ -0,0 +1,13 @@
// ----------------------------------------------------------------------
export * from './variants';
export { default as DialogAnimate } from './DialogAnimate';
export { default as TextAnimate } from './TextAnimate';
export { default as FabButtonAnimate } from './FabButtonAnimate';
export { default as IconButtonAnimate } from './IconButtonAnimate';
export { default as MotionInView } from './MotionInView';
export { default as MotionContainer } from './MotionContainer';
export { default as MotionLazyContainer } from './MotionLazyContainer';

View File

@@ -0,0 +1,44 @@
// ----------------------------------------------------------------------
type EaseType =
| 'linear'
| 'easeIn'
| 'easeOut'
| 'easeInOut'
| 'circIn'
| 'circOut'
| 'circInOut'
| 'backIn'
| 'backOut'
| 'backInOut'
| 'anticipate'
| number[];
export type VariantsType = {
distance?: number;
durationIn?: number;
durationOut?: number;
easeIn?: EaseType;
easeOut?: EaseType;
};
export type TranHoverType = {
duration?: number;
ease?: EaseType;
};
export type TranEnterType = {
durationIn?: number;
easeIn?: EaseType;
};
export type TranExitType = {
durationOut?: number;
easeOut?: EaseType;
};
export type BackgroundType = {
colors?: string[];
duration?: number;
ease?: EaseType;
};

View File

@@ -0,0 +1,7 @@
// ----------------------------------------------------------------------
export const varHover = (scale?: number) => ({
hover: {
scale: scale || 1.1,
},
});

View File

@@ -0,0 +1,106 @@
// @types
import { BackgroundType } from '../type';
// ----------------------------------------------------------------------
export const varBgColor = (props?: BackgroundType) => {
const colors = props?.colors || ['#19dcea', '#b22cff'];
const duration = props?.duration || 5;
const ease = props?.ease || 'linear';
return {
animate: {
background: colors,
transition: { duration, ease },
},
};
};
// ----------------------------------------------------------------------
export const varBgKenburns = (props?: BackgroundType) => {
const duration = props?.duration || 5;
const ease = props?.ease || 'easeOut';
return {
top: {
animate: {
scale: [1, 1.25],
y: [0, -15],
transformOrigin: ['50% 16%', 'top'],
transition: { duration, ease },
},
},
right: {
animate: {
scale: [1, 1.25],
x: [0, 20],
y: [0, -15],
transformOrigin: ['84% 50%', 'right'],
transition: { duration, ease },
},
},
bottom: {
animate: {
scale: [1, 1.25],
y: [0, 15],
transformOrigin: ['50% 84%', 'bottom'],
transition: { duration, ease },
},
},
left: {
animate: {
scale: [1, 1.25],
x: [0, -20],
y: [0, 15],
transformOrigin: ['16% 50%', 'left'],
transition: { duration, ease },
},
},
};
};
// ----------------------------------------------------------------------
export const varBgPan = (props?: BackgroundType) => {
const colors = props?.colors || ['#ee7752', '#e73c7e', '#23a6d5', '#23d5ab'];
const duration = props?.duration || 5;
const ease = props?.ease || 'linear';
const gradient = (deg: number) => `linear-gradient(${deg}deg, ${colors})`;
return {
top: {
animate: {
backgroundImage: [gradient(0), gradient(0)],
backgroundPosition: ['center 99%', 'center 1%'],
backgroundSize: ['100% 600%', '100% 600%'],
transition: { duration, ease },
},
},
right: {
animate: {
backgroundPosition: ['1% center', '99% center'],
backgroundImage: [gradient(270), gradient(270)],
backgroundSize: ['600% 100%', '600% 100%'],
transition: { duration, ease },
},
},
bottom: {
animate: {
backgroundImage: [gradient(0), gradient(0)],
backgroundPosition: ['center 1%', 'center 99%'],
backgroundSize: ['100% 600%', '100% 600%'],
transition: { duration, ease },
},
},
left: {
animate: {
backgroundPosition: ['99% center', '1% center'],
backgroundImage: [gradient(270), gradient(270)],
backgroundSize: ['600% 100%', '600% 100%'],
transition: { duration, ease },
},
},
};
};

View File

@@ -0,0 +1,106 @@
// @types
import { VariantsType } from '../type';
//
import { varTranEnter, varTranExit } from './transition';
// ----------------------------------------------------------------------
export const varBounce = (props?: VariantsType) => {
const durationIn = props?.durationIn;
const durationOut = props?.durationOut;
const easeIn = props?.easeIn;
const easeOut = props?.easeOut;
return {
// IN
in: {
initial: {},
animate: {
scale: [0.3, 1.1, 0.9, 1.03, 0.97, 1],
opacity: [0, 1, 1, 1, 1, 1],
transition: varTranEnter({ durationIn, easeIn }),
},
exit: {
scale: [0.9, 1.1, 0.3],
opacity: [1, 1, 0],
},
},
inUp: {
initial: {},
animate: {
y: [720, -24, 12, -4, 0],
scaleY: [4, 0.9, 0.95, 0.985, 1],
opacity: [0, 1, 1, 1, 1],
transition: { ...varTranEnter({ durationIn, easeIn }) },
},
exit: {
y: [12, -24, 720],
scaleY: [0.985, 0.9, 3],
opacity: [1, 1, 0],
transition: varTranExit({ durationOut, easeOut }),
},
},
inDown: {
initial: {},
animate: {
y: [-720, 24, -12, 4, 0],
scaleY: [4, 0.9, 0.95, 0.985, 1],
opacity: [0, 1, 1, 1, 1],
transition: varTranEnter({ durationIn, easeIn }),
},
exit: {
y: [-12, 24, -720],
scaleY: [0.985, 0.9, 3],
opacity: [1, 1, 0],
transition: varTranExit({ durationOut, easeOut }),
},
},
inLeft: {
initial: {},
animate: {
x: [-720, 24, -12, 4, 0],
scaleX: [3, 1, 0.98, 0.995, 1],
opacity: [0, 1, 1, 1, 1],
transition: varTranEnter({ durationIn, easeIn }),
},
exit: {
x: [0, 24, -720],
scaleX: [1, 0.9, 2],
opacity: [1, 1, 0],
transition: varTranExit({ durationOut, easeOut }),
},
},
inRight: {
initial: {},
animate: {
x: [720, -24, 12, -4, 0],
scaleX: [3, 1, 0.98, 0.995, 1],
opacity: [0, 1, 1, 1, 1],
transition: varTranEnter({ durationIn, easeIn }),
},
exit: {
x: [0, -24, 720],
scaleX: [1, 0.9, 2],
opacity: [1, 1, 0],
transition: varTranExit({ durationOut, easeOut }),
},
},
// OUT
out: {
animate: { scale: [0.9, 1.1, 0.3], opacity: [1, 1, 0] },
},
outUp: {
animate: { y: [-12, 24, -720], scaleY: [0.985, 0.9, 3], opacity: [1, 1, 0] },
},
outDown: {
animate: { y: [12, -24, 720], scaleY: [0.985, 0.9, 3], opacity: [1, 1, 0] },
},
outLeft: {
animate: { x: [0, 24, -720], scaleX: [1, 0.9, 2], opacity: [1, 1, 0] },
},
outRight: {
animate: { x: [0, -24, 720], scaleX: [1, 0.9, 2], opacity: [1, 1, 0] },
},
};
};

View File

@@ -0,0 +1,28 @@
// ----------------------------------------------------------------------
export type Props = {
staggerIn?: number;
delayIn?: number;
staggerOut?: number;
};
export const varContainer = (props?: Props) => {
const staggerIn = props?.staggerIn || 0.05;
const delayIn = props?.staggerIn || 0.05;
const staggerOut = props?.staggerIn || 0.05;
return {
animate: {
transition: {
staggerChildren: staggerIn,
delayChildren: delayIn,
},
},
exit: {
transition: {
staggerChildren: staggerOut,
staggerDirection: -1,
},
},
};
};

View File

@@ -0,0 +1,70 @@
// @types
import { VariantsType } from '../type';
//
import { varTranEnter, varTranExit } from './transition';
// ----------------------------------------------------------------------
export const varFade = (props?: VariantsType) => {
const distance = props?.distance || 120;
const durationIn = props?.durationIn;
const durationOut = props?.durationOut;
const easeIn = props?.easeIn;
const easeOut = props?.easeOut;
return {
// IN
in: {
initial: { opacity: 0 },
animate: { opacity: 1, transition: varTranEnter },
exit: { opacity: 0, transition: varTranExit },
},
inUp: {
initial: { y: distance, opacity: 0 },
animate: { y: 0, opacity: 1, transition: varTranEnter({ durationIn, easeIn }) },
exit: { y: distance, opacity: 0, transition: varTranExit({ durationOut, easeOut }) },
},
inDown: {
initial: { y: -distance, opacity: 0 },
animate: { y: 0, opacity: 1, transition: varTranEnter({ durationIn, easeIn }) },
exit: { y: -distance, opacity: 0, transition: varTranExit({ durationOut, easeOut }) },
},
inLeft: {
initial: { x: -distance, opacity: 0 },
animate: { x: 0, opacity: 1, transition: varTranEnter({ durationIn, easeIn }) },
exit: { x: -distance, opacity: 0, transition: varTranExit({ durationOut, easeOut }) },
},
inRight: {
initial: { x: distance, opacity: 0 },
animate: { x: 0, opacity: 1, transition: varTranEnter({ durationIn, easeIn }) },
exit: { x: distance, opacity: 0, transition: varTranExit({ durationOut, easeOut }) },
},
// OUT
out: {
initial: { opacity: 1 },
animate: { opacity: 0, transition: varTranEnter({ durationIn, easeIn }) },
exit: { opacity: 1, transition: varTranExit({ durationOut, easeOut }) },
},
outUp: {
initial: { y: 0, opacity: 1 },
animate: { y: -distance, opacity: 0, transition: varTranEnter({ durationIn, easeIn }) },
exit: { y: 0, opacity: 1, transition: varTranExit({ durationOut, easeOut }) },
},
outDown: {
initial: { y: 0, opacity: 1 },
animate: { y: distance, opacity: 0, transition: varTranEnter({ durationIn, easeIn }) },
exit: { y: 0, opacity: 1, transition: varTranExit({ durationOut, easeOut }) },
},
outLeft: {
initial: { x: 0, opacity: 1 },
animate: { x: -distance, opacity: 0, transition: varTranEnter({ durationIn, easeIn }) },
exit: { x: 0, opacity: 1, transition: varTranExit({ durationOut, easeOut }) },
},
outRight: {
initial: { x: 0, opacity: 1 },
animate: { x: distance, opacity: 0, transition: varTranEnter({ durationIn, easeIn }) },
exit: { x: 0, opacity: 1, transition: varTranExit({ durationOut, easeOut }) },
},
};
};

View File

@@ -0,0 +1,37 @@
// @types
import { VariantsType } from '../type';
//
import { varTranEnter, varTranExit } from './transition';
// ----------------------------------------------------------------------
export const varFlip = (props?: VariantsType) => {
const durationIn = props?.durationIn;
const durationOut = props?.durationOut;
const easeIn = props?.easeIn;
const easeOut = props?.easeOut;
return {
// IN
inX: {
initial: { rotateX: -180, opacity: 0 },
animate: { rotateX: 0, opacity: 1, transition: varTranEnter({ durationIn, easeIn }) },
exit: { rotateX: -180, opacity: 0, transition: varTranExit({ durationOut, easeOut }) },
},
inY: {
initial: { rotateY: -180, opacity: 0 },
animate: { rotateY: 0, opacity: 1, transition: varTranEnter({ durationIn, easeIn }) },
exit: { rotateY: -180, opacity: 0, transition: varTranExit({ durationOut, easeOut }) },
},
// OUT
outX: {
initial: { rotateX: 0, opacity: 1 },
animate: { rotateX: 70, opacity: 0, transition: varTranExit({ durationOut, easeOut }) },
},
outY: {
initial: { rotateY: 0, opacity: 1 },
animate: { rotateY: 70, opacity: 0, transition: varTranExit({ durationOut, easeOut }) },
},
};
};

View File

@@ -0,0 +1,12 @@
export * from './path';
export * from './fade';
export * from './zoom';
export * from './flip';
export * from './slide';
export * from './scale';
export * from './bounce';
export * from './rotate';
export * from './actions';
export * from './container';
export * from './transition';
export * from './background';

View File

@@ -0,0 +1,14 @@
// ----------------------------------------------------------------------
export const TRANSITION = {
duration: 2,
ease: [0.43, 0.13, 0.23, 0.96],
};
export const varPath = {
animate: {
fillOpacity: [0, 0, 1],
pathLength: [1, 0.4, 0],
transition: TRANSITION,
},
};

View File

@@ -0,0 +1,28 @@
// @types
import { VariantsType } from '../type';
//
import { varTranEnter, varTranExit } from './transition';
// ----------------------------------------------------------------------
export const varRotate = (props?: VariantsType) => {
const durationIn = props?.durationIn;
const durationOut = props?.durationOut;
const easeIn = props?.easeIn;
const easeOut = props?.easeOut;
return {
// IN
in: {
initial: { opacity: 0, rotate: -360 },
animate: { opacity: 1, rotate: 0, transition: varTranEnter({ durationIn, easeIn }) },
exit: { opacity: 0, rotate: -360, transition: varTranExit({ durationOut, easeOut }) },
},
// OUT
out: {
initial: { opacity: 1, rotate: 0 },
animate: { opacity: 0, rotate: -360, transition: varTranExit({ durationOut, easeOut }) },
},
};
};

View File

@@ -0,0 +1,37 @@
// @types
import { VariantsType } from '../type';
//
import { varTranEnter, varTranExit } from './transition';
// ----------------------------------------------------------------------
export const varScale = (props?: VariantsType) => {
const durationIn = props?.durationIn;
const durationOut = props?.durationOut;
const easeIn = props?.easeIn;
const easeOut = props?.easeOut;
return {
// IN
inX: {
initial: { scaleX: 0, opacity: 0 },
animate: { scaleX: 1, opacity: 1, transition: varTranEnter({ durationIn, easeIn }) },
exit: { scaleX: 0, opacity: 0, transition: varTranExit({ durationOut, easeOut }) },
},
inY: {
initial: { scaleY: 0, opacity: 0 },
animate: { scaleY: 1, opacity: 1, transition: varTranEnter({ durationIn, easeIn }) },
exit: { scaleY: 0, opacity: 0, transition: varTranExit({ durationOut, easeOut }) },
},
// OUT
outX: {
initial: { scaleX: 1, opacity: 1 },
animate: { scaleX: 0, opacity: 0, transition: varTranEnter({ durationIn, easeIn }) },
},
outY: {
initial: { scaleY: 1, opacity: 1 },
animate: { scaleY: 0, opacity: 0, transition: varTranEnter({ durationIn, easeIn }) },
},
};
};

View File

@@ -0,0 +1,60 @@
// @types
import { VariantsType } from '../type';
//
import { varTranEnter, varTranExit } from './transition';
// ----------------------------------------------------------------------
export const varSlide = (props?: VariantsType) => {
const distance = props?.distance || 160;
const durationIn = props?.durationIn;
const durationOut = props?.durationOut;
const easeIn = props?.easeIn;
const easeOut = props?.easeOut;
return {
// IN
inUp: {
initial: { y: distance },
animate: { y: 0, transition: varTranEnter({ durationIn, easeIn }) },
exit: { y: distance, transition: varTranExit({ durationOut, easeOut }) },
},
inDown: {
initial: { y: -distance },
animate: { y: 0, transition: varTranEnter({ durationIn, easeIn }) },
exit: { y: -distance, transition: varTranExit({ durationOut, easeOut }) },
},
inLeft: {
initial: { x: -distance },
animate: { x: 0, transition: varTranEnter({ durationIn, easeIn }) },
exit: { x: -distance, transition: varTranExit({ durationOut, easeOut }) },
},
inRight: {
initial: { x: distance },
animate: { x: 0, transition: varTranEnter({ durationIn, easeIn }) },
exit: { x: distance, transition: varTranExit({ durationOut, easeOut }) },
},
// OUT
outUp: {
initial: { y: 0 },
animate: { y: -distance, transition: varTranEnter({ durationIn, easeIn }) },
exit: { y: 0, transition: varTranExit({ durationOut, easeOut }) },
},
outDown: {
initial: { y: 0 },
animate: { y: distance, transition: varTranEnter({ durationIn, easeIn }) },
exit: { y: 0, transition: varTranExit({ durationOut, easeOut }) },
},
outLeft: {
initial: { x: 0 },
animate: { x: -distance, transition: varTranEnter({ durationIn, easeIn }) },
exit: { x: 0, transition: varTranExit({ durationOut, easeOut }) },
},
outRight: {
initial: { x: 0 },
animate: { x: distance, transition: varTranEnter({ durationIn, easeIn }) },
exit: { x: 0, transition: varTranExit({ durationOut, easeOut }) },
},
};
};

View File

@@ -0,0 +1,25 @@
// @types
import { TranHoverType, TranEnterType, TranExitType } from '../type';
// ----------------------------------------------------------------------
export const varTranHover = (props?: TranHoverType) => {
const duration = props?.duration || 0.32;
const ease = props?.ease || [0.43, 0.13, 0.23, 0.96];
return { duration, ease };
};
export const varTranEnter = (props?: TranEnterType) => {
const duration = props?.durationIn || 0.64;
const ease = props?.easeIn || [0.43, 0.13, 0.23, 0.96];
return { duration, ease };
};
export const varTranExit = (props?: TranExitType) => {
const duration = props?.durationOut || 0.48;
const ease = props?.easeOut || [0.43, 0.13, 0.23, 0.96];
return { duration, ease };
};

View File

@@ -0,0 +1,125 @@
// @types
import { VariantsType } from '../type';
//
import { varTranEnter, varTranExit } from './transition';
// ----------------------------------------------------------------------
export const varZoom = (props?: VariantsType) => {
const distance = props?.distance || 720;
const durationIn = props?.durationIn;
const durationOut = props?.durationOut;
const easeIn = props?.easeIn;
const easeOut = props?.easeOut;
return {
// IN
in: {
initial: { scale: 0, opacity: 0 },
animate: { scale: 1, opacity: 1, transition: varTranEnter({ durationIn, easeIn }) },
exit: { scale: 0, opacity: 0, transition: varTranExit({ durationOut, easeOut }) },
},
inUp: {
initial: { scale: 0, opacity: 0, translateY: distance },
animate: {
scale: 1,
opacity: 1,
translateY: 0,
transition: varTranEnter({ durationIn, easeIn }),
},
exit: {
scale: 0,
opacity: 0,
translateY: distance,
transition: varTranExit({ durationOut, easeOut }),
},
},
inDown: {
initial: { scale: 0, opacity: 0, translateY: -distance },
animate: {
scale: 1,
opacity: 1,
translateY: 0,
transition: varTranEnter({ durationIn, easeIn }),
},
exit: {
scale: 0,
opacity: 0,
translateY: -distance,
transition: varTranExit({ durationOut, easeOut }),
},
},
inLeft: {
initial: { scale: 0, opacity: 0, translateX: -distance },
animate: {
scale: 1,
opacity: 1,
translateX: 0,
transition: varTranEnter({ durationIn, easeIn }),
},
exit: {
scale: 0,
opacity: 0,
translateX: -distance,
transition: varTranExit({ durationOut, easeOut }),
},
},
inRight: {
initial: { scale: 0, opacity: 0, translateX: distance },
animate: {
scale: 1,
opacity: 1,
translateX: 0,
transition: varTranEnter({ durationIn, easeIn }),
},
exit: {
scale: 0,
opacity: 0,
translateX: distance,
transition: varTranExit({ durationOut, easeOut }),
},
},
// OUT
out: {
initial: { scale: 1, opacity: 1 },
animate: { scale: 0, opacity: 0, transition: varTranEnter({ durationIn, easeIn }) },
},
outUp: {
initial: { scale: 1, opacity: 1 },
animate: {
scale: 0,
opacity: 0,
translateY: -distance,
transition: varTranEnter({ durationIn, easeIn }),
},
},
outDown: {
initial: { scale: 1, opacity: 1 },
animate: {
scale: 0,
opacity: 0,
translateY: distance,
transition: varTranEnter({ durationIn, easeIn }),
},
},
outLeft: {
initial: { scale: 1, opacity: 1 },
animate: {
scale: 0,
opacity: 0,
translateX: -distance,
transition: varTranEnter({ durationIn, easeIn }),
},
},
outRight: {
initial: { scale: 1, opacity: 1 },
animate: {
scale: 0,
opacity: 0,
translateX: distance,
transition: varTranEnter({ durationIn, easeIn }),
},
},
};
};

View File

@@ -0,0 +1,177 @@
import { Quill } from 'react-quill';
// components
import Iconify from '../Iconify';
//
import EditorToolbarStyle from './EditorToolbarStyle';
// ----------------------------------------------------------------------
const FONT_FAMILY = ['Arial', 'Tahoma', 'Georgia', 'Impact', 'Verdana'];
const FONT_SIZE = [
'8px',
'9px',
'10px',
'12px',
'14px',
'16px',
'20px',
'24px',
'32px',
'42px',
'54px',
'68px',
'84px',
'98px',
];
const HEADINGS = ['Heading 1', 'Heading 2', 'Heading 3', 'Heading 4', 'Heading 5', 'Heading 6'];
export function undoChange() {
// @ts-ignore
this.quill.history.undo();
}
export function redoChange() {
// @ts-ignore
this.quill.history.redo();
}
const Size = Quill.import('attributors/style/size');
Size.whitelist = FONT_SIZE;
Quill.register(Size, true);
const Font = Quill.import('attributors/style/font');
Font.whitelist = FONT_FAMILY;
Quill.register(Font, true);
export const formats = [
'align',
'background',
'blockquote',
'bold',
'bullet',
'code',
'code-block',
'color',
'direction',
'font',
'formula',
'header',
'image',
'indent',
'italic',
'link',
'list',
'script',
'size',
'strike',
'table',
'underline',
'video',
];
type EditorToolbarProps = {
id: string;
isSimple?: boolean;
};
export default function EditorToolbar({ id, isSimple, ...other }: EditorToolbarProps) {
return (
<EditorToolbarStyle {...other}>
<div id={id}>
<div className="ql-formats">
{!isSimple && (
<select className="ql-font" defaultValue="">
<option value="">Font</option>
{FONT_FAMILY.map((font) => (
<option key={font} value={font}>
{font}
</option>
))}
</select>
)}
{!isSimple && (
<select className="ql-size" defaultValue="16px">
{FONT_SIZE.map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
)}
<select className="ql-header" defaultValue="">
{HEADINGS.map((heading, index) => (
<option key={heading} value={index + 1}>
{heading}
</option>
))}
<option value="">Normal</option>
</select>
</div>
<div className="ql-formats">
<button type="button" className="ql-bold" />
<button type="button" className="ql-italic" />
<button type="button" className="ql-underline" />
<button type="button" className="ql-strike" />
</div>
{!isSimple && (
<div className="ql-formats">
<select className="ql-color" />
<select className="ql-background" />
</div>
)}
<div className="ql-formats">
<button type="button" className="ql-list" value="ordered" />
<button type="button" className="ql-list" value="bullet" />
{!isSimple && <button type="button" className="ql-indent" value="-1" />}
{!isSimple && <button type="button" className="ql-indent" value="+1" />}
</div>
{!isSimple && (
<div className="ql-formats">
<button type="button" className="ql-script" value="super" />
<button type="button" className="ql-script" value="sub" />
</div>
)}
{!isSimple && (
<div className="ql-formats">
<button type="button" className="ql-code-block" />
<button type="button" className="ql-blockquote" />
</div>
)}
<div className="ql-formats">
<button type="button" className="ql-direction" value="rtl" />
<select className="ql-align" />
</div>
<div className="ql-formats">
<button type="button" className="ql-link" />
<button type="button" className="ql-image" />
<button type="button" className="ql-video" />
</div>
<div className="ql-formats">
{!isSimple && <button type="button" className="ql-formula" />}
<button type="button" className="ql-clean" />
</div>
{!isSimple && (
<div className="ql-formats">
<button type="button" className="ql-undo">
<Iconify icon={'ic:round-undo'} width={18} height={18} />
</button>
<button type="button" className="ql-redo">
<Iconify icon={'ic:round-redo'} width={18} height={18} />
</button>
</div>
)}
</div>
</EditorToolbarStyle>
);
}

View File

@@ -0,0 +1,133 @@
import { styled } from '@mui/material/styles';
// ----------------------------------------------------------------------
const EditorToolbarStyle = styled('div')(({ theme }) => {
const isRTL = theme.direction === 'rtl';
return {
'& .ql-snow.ql-toolbar button:hover .ql-fill, .ql-snow .ql-toolbar button:hover .ql-fill, .ql-snow.ql-toolbar button:focus .ql-fill, .ql-snow .ql-toolbar button:focus .ql-fill, .ql-snow.ql-toolbar button.ql-active .ql-fill, .ql-snow .ql-toolbar button.ql-active .ql-fill, .ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill, .ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill, .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill, .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill, .ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill, .ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill, .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill, .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill, .ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill, .ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill, .ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill, .ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill, .ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill, .ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill, .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill, .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill':
{
fill: theme.palette.primary.main,
},
'& .ql-snow.ql-toolbar button:hover, .ql-snow .ql-toolbar button:hover, .ql-snow.ql-toolbar button:focus, .ql-snow .ql-toolbar button:focus, .ql-snow.ql-toolbar button.ql-active, .ql-snow .ql-toolbar button.ql-active, .ql-snow.ql-toolbar .ql-picker-label:hover, .ql-snow .ql-toolbar .ql-picker-label:hover, .ql-snow.ql-toolbar .ql-picker-label.ql-active, .ql-snow .ql-toolbar .ql-picker-label.ql-active, .ql-snow.ql-toolbar .ql-picker-item:hover, .ql-snow .ql-toolbar .ql-picker-item:hover, .ql-snow.ql-toolbar .ql-picker-item.ql-selected, .ql-snow .ql-toolbar .ql-picker-item.ql-selected':
{
color: theme.palette.primary.main,
},
'& .ql-snow.ql-toolbar button:hover .ql-stroke, .ql-snow .ql-toolbar button:hover .ql-stroke, .ql-snow.ql-toolbar button:focus .ql-stroke, .ql-snow .ql-toolbar button:focus .ql-stroke, .ql-snow.ql-toolbar button.ql-active .ql-stroke, .ql-snow .ql-toolbar button.ql-active .ql-stroke, .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke, .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke, .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke, .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke, .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke, .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke, .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke, .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke, .ql-snow.ql-toolbar button:hover .ql-stroke-miter, .ql-snow .ql-toolbar button:hover .ql-stroke-miter, .ql-snow.ql-toolbar button:focus .ql-stroke-miter, .ql-snow .ql-toolbar button:focus .ql-stroke-miter, .ql-snow.ql-toolbar button.ql-active .ql-stroke-miter, .ql-snow .ql-toolbar button.ql-active .ql-stroke-miter, .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter, .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter, .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter, .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter, .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter, .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter':
{
stroke: theme.palette.primary.main,
},
'& .ql-stroke': {
stroke: theme.palette.text.primary,
},
'& .ql-fill, .ql-stroke.ql-fill': {
fill: theme.palette.text.primary,
},
'& .ql-picker, .ql-picker-options, .ql-picker-item, .ql-picker-label, button': {
'&:focus': { outline: 'none' },
},
'& .ql-toolbar.ql-snow': {
border: 'none',
borderBottom: `solid 1px ${theme.palette.grey[500_32]}`,
'& .ql-formats': {
'&:not(:last-of-type)': {
marginRight: theme.spacing(2),
},
},
// Button
'& button': {
padding: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 4,
color: theme.palette.text.primary,
},
// Icon svg
'& button svg, span svg': {
width: 20,
height: 20,
},
// Select
'& .ql-picker-label': {
...theme.typography.subtitle2,
color: theme.palette.text.primary,
'& .ql-stroke': {
stroke: theme.palette.text.primary,
},
},
'& .ql-picker-label svg': {
...(isRTL && {
right: '0 !important',
left: 'auto !important',
}),
},
'& .ql-color,& .ql-background,& .ql-align ': {
'& .ql-picker-label': {
padding: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
},
'& .ql-expanded': {
'& .ql-picker-label': {
borderRadius: 4,
color: theme.palette.text.disabled,
borderColor: 'transparent !important',
backgroundColor: theme.palette.action.focus,
'& .ql-stroke': { stroke: theme.palette.text.disabled },
},
'& .ql-picker-options': {
padding: 0,
marginTop: 4,
border: 'none',
maxHeight: 200,
overflow: 'auto',
boxShadow: theme.customShadows.z20,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.paper,
},
'& .ql-picker-item': {
color: theme.palette.text.primary,
},
// Align
'&.ql-align': {
'& .ql-picker-options': { padding: 0, display: 'flex' },
'& .ql-picker-item': {
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
},
// Color & Background
'&.ql-color, &.ql-background': {
'& .ql-picker-options': { padding: 8 },
'& .ql-picker-item': {
margin: 3,
borderRadius: 4,
'&.ql-selected': { border: 'solid 1px black' },
},
},
// Font, Size, Header
'&.ql-font, &.ql-size, &.ql-header': {
'& .ql-picker-options': {
padding: theme.spacing(1, 0),
},
'& .ql-picker-item': {
padding: theme.spacing(0.5, 1.5),
},
},
},
},
};
});
export default EditorToolbarStyle;

View File

@@ -0,0 +1,97 @@
import { ReactNode } from 'react';
import ReactQuill, { ReactQuillProps } from 'react-quill';
// @mui
import { styled } from '@mui/material/styles';
import { Box, BoxProps } from '@mui/material';
//
import EditorToolbar, { formats, redoChange, undoChange } from './EditorToolbar';
// ----------------------------------------------------------------------
const RootStyle = styled(Box)(({ theme }) => ({
borderRadius: theme.shape.borderRadius,
border: `solid 1px ${theme.palette.grey[500_32]}`,
'& .ql-container.ql-snow': {
borderColor: 'transparent',
...theme.typography.body1,
fontFamily: theme.typography.fontFamily,
},
'& .ql-editor': {
minHeight: 200,
'&.ql-blank::before': {
fontStyle: 'normal',
color: theme.palette.text.disabled,
},
'& pre.ql-syntax': {
...theme.typography.body2,
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.grey[900],
},
},
}));
// ----------------------------------------------------------------------
export interface Props extends ReactQuillProps {
id?: string;
error?: boolean;
simple?: boolean;
helperText?: ReactNode;
sx?: BoxProps;
}
export default function Editor({
id = 'minimal-quill',
error,
value,
onChange,
simple = false,
helperText,
sx,
...other
}: Props) {
const modules = {
toolbar: {
container: `#${id}`,
handlers: {
undo: undoChange,
redo: redoChange,
},
},
history: {
delay: 500,
maxStack: 100,
userOnly: true,
},
syntax: true,
clipboard: {
matchVisual: false,
},
};
return (
<div>
<RootStyle
sx={{
...(error && {
border: (theme) => `solid 1px ${theme.palette.error.main}`,
}),
...sx,
}}
>
<EditorToolbar id={id} isSimple={simple} />
<ReactQuill
value={value}
onChange={onChange}
modules={modules}
formats={formats}
placeholder="Write something awesome..."
{...other}
/>
</RootStyle>
{helperText && helperText}
</div>
);
}

View File

@@ -0,0 +1,19 @@
import { ReactNode } from 'react';
// form
import { FormProvider as Form, UseFormReturn } from 'react-hook-form';
// ----------------------------------------------------------------------
type Props = {
children: ReactNode;
methods: UseFormReturn<any>;
onSubmit?: VoidFunction;
};
export default function FormProvider({ children, onSubmit, methods }: Props) {
return (
<Form {...methods}>
<form onSubmit={onSubmit}>{children}</form>
</Form>
);
}

View File

@@ -0,0 +1,69 @@
// form
import { useFormContext, Controller } from 'react-hook-form';
// @mui
import { Checkbox, FormControlLabel, FormGroup, FormControlLabelProps } from '@mui/material';
// ----------------------------------------------------------------------
interface RHFCheckboxProps extends Omit<FormControlLabelProps, 'control'> {
name: string;
}
export function RHFCheckbox({ name, ...other }: RHFCheckboxProps) {
const { control } = useFormContext();
return (
<FormControlLabel
control={
<Controller
name={name}
control={control}
render={({ field }) => <Checkbox {...field} checked={field.value} />}
/>
}
{...other}
/>
);
}
// ----------------------------------------------------------------------
interface RHFMultiCheckboxProps extends Omit<FormControlLabelProps, 'control' | 'label'> {
name: string;
options: string[];
}
export function RHFMultiCheckbox({ name, options, ...other }: RHFMultiCheckboxProps) {
const { control } = useFormContext();
return (
<Controller
name={name}
control={control}
render={({ field }) => {
const onSelected = (option: string) =>
field.value.includes(option)
? field.value.filter((value: string) => value !== option)
: [...field.value, option];
return (
<FormGroup>
{options.map((option) => (
<FormControlLabel
key={option}
control={
<Checkbox
checked={field.value.includes(option)}
onChange={() => field.onChange(onSelected(option))}
/>
}
label={option}
{...other}
/>
))}
</FormGroup>
);
}}
/>
);
}

View File

@@ -0,0 +1,37 @@
// form
import { useFormContext, Controller } from 'react-hook-form';
// @mui
import { FormHelperText } from '@mui/material';
//
import Editor, { Props as EditorProps } from '../editor';
// ----------------------------------------------------------------------
interface Props extends EditorProps {
name: string;
}
export default function RHFEditor({ name, ...other }: Props) {
const { control } = useFormContext();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState: { error } }) => (
<Editor
id={name}
value={field.value}
onChange={field.onChange}
error={!!error}
helperText={
<FormHelperText error sx={{ px: 2, textTransform: 'capitalize' }}>
{error?.message}
</FormHelperText>
}
{...other}
/>
)}
/>
);
}

View File

@@ -0,0 +1,54 @@
// form
import { useFormContext, Controller } from 'react-hook-form';
// @mui
import {
Radio,
RadioGroup,
FormHelperText,
RadioGroupProps,
FormControlLabel,
} from '@mui/material';
// ----------------------------------------------------------------------
interface IProps {
name: string;
options: string[];
getOptionLabel?: string[];
}
export default function RHFRadioGroup({
name,
options,
getOptionLabel,
...other
}: IProps & RadioGroupProps) {
const { control } = useFormContext();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState: { error } }) => (
<div>
<RadioGroup {...field} row {...other}>
{options.map((option, index) => (
<FormControlLabel
key={option}
value={option}
control={<Radio />}
label={getOptionLabel?.length ? getOptionLabel[index] : option}
/>
))}
</RadioGroup>
{!!error && (
<FormHelperText error sx={{ px: 2 }}>
{error.message}
</FormHelperText>
)}
</div>
)}
/>
);
}

View File

@@ -0,0 +1,35 @@
// form
import { useFormContext, Controller } from 'react-hook-form';
// @mui
import { TextField, TextFieldProps } from '@mui/material';
// ----------------------------------------------------------------------
interface IProps {
name: string;
children: any;
}
export default function RHFSelect({ name, children, ...other }: IProps & TextFieldProps) {
const { control } = useFormContext();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
select
fullWidth
SelectProps={{ native: true }}
error={!!error}
helperText={error?.message}
{...other}
>
{children}
</TextField>
)}
/>
);
}

View File

@@ -0,0 +1,29 @@
// form
import { useFormContext, Controller } from 'react-hook-form';
// @mui
import { Switch, FormControlLabel, FormControlLabelProps } from '@mui/material';
// ----------------------------------------------------------------------
type IProps = Omit<FormControlLabelProps, 'control'>;
interface Props extends IProps {
name: string;
}
export default function RHFSwitch({ name, ...other }: Props) {
const { control } = useFormContext();
return (
<FormControlLabel
control={
<Controller
name={name}
control={control}
render={({ field }) => <Switch {...field} checked={field.value} />}
/>
}
{...other}
/>
);
}

View File

@@ -0,0 +1,24 @@
// form
import { useFormContext, Controller } from 'react-hook-form';
// @mui
import { TextField, TextFieldProps } from '@mui/material';
// ----------------------------------------------------------------------
interface IProps {
name: string;
}
export default function RHFTextField({ name, ...other }: IProps & TextFieldProps) {
const { control } = useFormContext();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState: { error } }) => (
<TextField {...field} fullWidth error={!!error} helperText={error?.message} {...other} />
)}
/>
);
}

View File

@@ -0,0 +1,111 @@
// form
import { useFormContext, Controller } from 'react-hook-form';
// @mui
import { FormHelperText } from '@mui/material';
// type
import {
UploadAvatar,
UploadMultiFile,
UploadSingleFile,
UploadProps,
UploadMultiFileProps,
} from '../upload';
// ----------------------------------------------------------------------
interface Props extends Omit<UploadProps, 'file'> {
name: string;
}
export function RHFUploadAvatar({ name, ...other }: Props) {
const { control } = useFormContext();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState: { error } }) => {
const checkError = !!error && !field.value;
return (
<div>
<UploadAvatar error={checkError} {...other} file={field.value} />
{checkError && (
<FormHelperText error sx={{ px: 2, textAlign: 'center' }}>
{error.message}
</FormHelperText>
)}
</div>
);
}}
/>
);
}
// ----------------------------------------------------------------------
export function RHFUploadSingleFile({ name, ...other }: Props) {
const { control } = useFormContext();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState: { error } }) => {
const checkError = !!error && !field.value;
return (
<UploadSingleFile
accept="image/*"
file={field.value}
error={checkError}
helperText={
checkError && (
<FormHelperText error sx={{ px: 2 }}>
{error.message}
</FormHelperText>
)
}
{...other}
/>
);
}}
/>
);
}
// ----------------------------------------------------------------------
interface RHFUploadMultiFileProps extends Omit<UploadMultiFileProps, 'files'> {
name: string;
}
export function RHFUploadMultiFile({ name, ...other }: RHFUploadMultiFileProps) {
const { control } = useFormContext();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState: { error } }) => {
const checkError = !!error && field.value?.length === 0;
return (
<UploadMultiFile
accept="image/*"
files={field.value}
error={checkError}
helperText={
checkError && (
<FormHelperText error sx={{ px: 2 }}>
{error?.message}
</FormHelperText>
)
}
{...other}
/>
);
}}
/>
);
}

View File

@@ -0,0 +1,10 @@
export * from './RHFCheckbox';
export * from './RHFUpload';
export { default as FormProvider } from './FormProvider';
export { default as RHFSwitch } from './RHFSwitch';
export { default as RHFSelect } from './RHFSelect';
export { default as RHFEditor } from './RHFEditor';
export { default as RHFTextField } from './RHFTextField';
export { default as RHFRadioGroup } from './RHFRadioGroup';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,69 @@
// @mui
import { alpha, styled } from '@mui/material/styles';
import { Box, Grid, RadioGroup, CardActionArea } from '@mui/material';
// hooks
import useSettings from '../../hooks/useSettings';
//
import { BoxMask } from '.';
// ----------------------------------------------------------------------
const BoxStyle = styled(CardActionArea)(({ theme }) => ({
height: 48,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: theme.palette.text.disabled,
border: `solid 1px ${theme.palette.grey[500_12]}`,
borderRadius: Number(theme.shape.borderRadius) * 1.25,
}));
// ----------------------------------------------------------------------
export default function SettingColorPresets() {
const { themeColorPresets, onChangeColor, colorOption } = useSettings();
return (
<RadioGroup name="themeColorPresets" value={themeColorPresets} onChange={onChangeColor}>
<Grid dir="ltr" container spacing={1.5}>
{colorOption.map((color) => {
const colorName = color.name;
const colorValue = color.value;
const isSelected = themeColorPresets === colorName;
return (
<Grid key={colorName} item xs={4}>
<BoxStyle
sx={{
...(isSelected && {
bgcolor: alpha(colorValue, 0.08),
border: `solid 2px ${colorValue}`,
boxShadow: `inset 0 4px 8px 0 ${alpha(colorValue, 0.24)}`,
}),
}}
>
<Box
sx={{
width: 24,
height: 14,
borderRadius: '50%',
bgcolor: colorValue,
transform: 'rotate(-45deg)',
transition: (theme) =>
theme.transitions.create('all', {
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.shorter,
}),
...(isSelected && { transform: 'none' }),
}}
/>
<BoxMask value={colorName} />
</BoxStyle>
</Grid>
);
})}
</Grid>
</RadioGroup>
);
}

View File

@@ -0,0 +1,56 @@
// @mui
import { styled } from '@mui/material/styles';
import { Grid, RadioGroup, CardActionArea } from '@mui/material';
// hooks
import useSettings from '../../hooks/useSettings';
//
import Iconify from '../Iconify';
import { BoxMask } from '.';
// ----------------------------------------------------------------------
const BoxStyle = styled(CardActionArea)(({ theme }) => ({
height: 72,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: theme.palette.text.disabled,
border: `solid 1px ${theme.palette.grey[500_12]}`,
borderRadius: Number(theme.shape.borderRadius) * 1.25,
}));
// ----------------------------------------------------------------------
export default function SettingDirection() {
const { themeDirection, onChangeDirection } = useSettings();
return (
<RadioGroup name="themeDirection" value={themeDirection} onChange={onChangeDirection}>
<Grid dir="ltr" container spacing={2.5}>
{['ltr', 'rtl'].map((direction, index) => {
const isSelected = themeDirection === direction;
return (
<Grid key={direction} item xs={6}>
<BoxStyle
sx={{
...(isSelected && {
color: 'primary.main',
boxShadow: (theme) => theme.customShadows.z20,
}),
}}
>
<Iconify
icon={index === 0 ? 'ph:align-left-duotone' : 'ph:align-right-duotone'}
width={28}
height={28}
/>
<BoxMask value={direction} />
</BoxStyle>
</Grid>
);
})}
</Grid>
</RadioGroup>
);
}

View File

@@ -0,0 +1,42 @@
import { useState } from 'react';
// @mui
import { alpha } from '@mui/material/styles';
import { Button } from '@mui/material';
// components
import Iconify from '../Iconify';
// ----------------------------------------------------------------------
export default function SettingFullscreen() {
const [fullscreen, setFullscreen] = useState(false);
const toggleFullScreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
setFullscreen(true);
} else if (document.exitFullscreen) {
document.exitFullscreen();
setFullscreen(false);
}
};
return (
<Button
fullWidth
size="large"
variant="outlined"
color={fullscreen ? 'primary' : 'inherit'}
startIcon={<Iconify icon={fullscreen ? 'ic:round-fullscreen-exit' : 'ic:round-fullscreen'} />}
onClick={toggleFullScreen}
sx={{
fontSize: 14,
...(fullscreen && {
bgcolor: (theme) =>
alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity),
}),
}}
>
{fullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
</Button>
);
}

View File

@@ -0,0 +1,138 @@
// @mui
import { styled, alpha } from '@mui/material/styles';
import { Grid, RadioGroup, CardActionArea, Box, Stack } from '@mui/material';
// hooks
import useSettings from '../../hooks/useSettings';
//
import { BoxMask } from '.';
// ----------------------------------------------------------------------
const BoxStyle = styled(CardActionArea)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(1.5),
color: theme.palette.text.disabled,
border: `solid 1px ${theme.palette.grey[500_12]}`,
borderRadius: Number(theme.shape.borderRadius) * 1.25,
}));
// ----------------------------------------------------------------------
export default function SettingLayout() {
const { themeLayout, onChangeLayout } = useSettings();
return (
<RadioGroup name="themeLayout" value={themeLayout} onChange={onChangeLayout}>
<Grid dir="ltr" container spacing={2.5}>
{['horizontal', 'vertical'].map((layout) => {
const isSelected = themeLayout === layout;
const isVertical = layout === 'vertical';
return (
<Grid key={layout} item xs={6}>
<BoxStyle
sx={{
...(isSelected && {
color: 'primary.main',
boxShadow: (theme) => theme.customShadows.z20,
}),
}}
>
{isVertical ? (
<VerticalBox isSelected={isSelected} />
) : (
<HorizontalBox isSelected={isSelected} />
)}
<BoxMask value={layout} />
</BoxStyle>
</Grid>
);
})}
</Grid>
</RadioGroup>
);
}
// ----------------------------------------------------------------------
type LayoutBoxProps = {
isSelected: boolean;
};
const style = {
width: 1,
height: 32,
borderRadius: 0.5,
};
function VerticalBox({ isSelected }: LayoutBoxProps) {
return (
<>
<Box
sx={{
...style,
mb: 0.75,
height: 12,
bgcolor: (theme) => alpha(theme.palette.text.disabled, 0.72),
...(isSelected && {
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.72),
}),
}}
/>
<Box
sx={{
...style,
border: (theme) => `dashed 1px ${theme.palette.divider}`,
bgcolor: (theme) => alpha(theme.palette.text.disabled, 0.08),
...(isSelected && {
border: (theme) => `dashed 1px ${theme.palette.primary.main}`,
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.16),
}),
}}
/>
</>
);
}
function HorizontalBox({ isSelected }: LayoutBoxProps) {
return (
<>
<Box
sx={{
...style,
mb: 0.75,
height: 12,
bgcolor: (theme) => alpha(theme.palette.text.disabled, 0.72),
...(isSelected && {
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.72),
}),
}}
/>
<Stack width={1} direction="row" justifyContent="space-between">
<Box
sx={{
...style,
width: 20,
bgcolor: (theme) => alpha(theme.palette.text.disabled, 0.32),
...(isSelected && {
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.32),
}),
}}
/>
<Box
sx={{
...style,
width: `calc(100% - 26px)`,
border: (theme) => `dashed 1px ${theme.palette.divider}`,
bgcolor: (theme) => alpha(theme.palette.text.disabled, 0.08),
...(isSelected && {
border: (theme) => `dashed 1px ${theme.palette.primary.main}`,
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.16),
}),
}}
/>
</Stack>
</>
);
}

View File

@@ -0,0 +1,57 @@
// @mui
import { styled } from '@mui/material/styles';
import { Grid, RadioGroup, CardActionArea } from '@mui/material';
// hooks
import useSettings from '../../hooks/useSettings';
//
import Iconify from '../Iconify';
import { BoxMask } from '.';
// ----------------------------------------------------------------------
const BoxStyle = styled(CardActionArea)(({ theme }) => ({
height: 72,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: theme.palette.text.disabled,
border: `solid 1px ${theme.palette.grey[500_12]}`,
borderRadius: Number(theme.shape.borderRadius) * 1.25,
}));
// ----------------------------------------------------------------------
export default function SettingMode() {
const { themeMode, onChangeMode } = useSettings();
return (
<RadioGroup name="themeMode" value={themeMode} onChange={onChangeMode}>
<Grid dir="ltr" container spacing={2.5}>
{['light', 'dark'].map((mode, index) => {
const isSelected = themeMode === mode;
return (
<Grid key={mode} item xs={6}>
<BoxStyle
sx={{
bgcolor: mode === 'light' ? 'common.white' : 'grey.800',
...(isSelected && {
color: 'primary.main',
boxShadow: (theme) => theme.customShadows.z20,
}),
}}
>
<Iconify
icon={index === 0 ? 'ph:sun-duotone' : 'ph:moon-duotone'}
width={28}
height={28}
/>
<BoxMask value={mode} />
</BoxStyle>
</Grid>
);
})}
</Grid>
</RadioGroup>
);
}

View File

@@ -0,0 +1,69 @@
// @mui
import { styled } from '@mui/material/styles';
import { CardActionArea, Stack } from '@mui/material';
// hooks
import useSettings from '../../hooks/useSettings';
//
import Iconify from '../Iconify';
// ----------------------------------------------------------------------
const BoxStyle = styled(CardActionArea)(({ theme }) => ({
padding: theme.spacing(2),
color: theme.palette.text.disabled,
border: `solid 1px ${theme.palette.grey[500_12]}`,
backgroundColor: theme.palette.background.neutral,
borderRadius: Number(theme.shape.borderRadius) * 1.25,
}));
// ----------------------------------------------------------------------
export default function SettingStretch() {
const { themeStretch, onToggleStretch } = useSettings();
const ICON_SIZE = {
width: themeStretch ? 24 : 18,
height: themeStretch ? 24 : 18,
};
return (
<BoxStyle
onClick={onToggleStretch}
sx={{
...(themeStretch && {
color: (theme) => theme.palette.primary.main,
}),
}}
>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{
px: 1,
mx: 'auto',
width: 0.5,
height: 40,
borderRadius: 1,
color: 'action.active',
bgcolor: 'background.default',
boxShadow: (theme) => theme.customShadows.z12,
transition: (theme) => theme.transitions.create('width'),
...(themeStretch && {
width: 1,
color: 'primary.main',
}),
}}
>
<Iconify
icon={themeStretch ? 'eva:arrow-ios-back-fill' : 'eva:arrow-ios-forward-fill'}
{...ICON_SIZE}
/>
<Iconify
icon={themeStretch ? 'eva:arrow-ios-forward-fill' : 'eva:arrow-ios-back-fill'}
{...ICON_SIZE}
/>
</Stack>
</BoxStyle>
);
}

View File

@@ -0,0 +1,69 @@
// @mui
import { alpha, styled } from '@mui/material/styles';
import { Tooltip } from '@mui/material';
// utils
import cssStyles from '../../utils/cssStyles';
//
import Iconify from '../Iconify';
import { IconButtonAnimate } from '../animate';
// ----------------------------------------------------------------------
const RootStyle = styled('span')(({ theme }) => ({
...cssStyles(theme).bgBlur({ opacity: 0.64 }),
right: 0,
top: '50%',
position: 'fixed',
marginTop: theme.spacing(-3),
padding: theme.spacing(0.5),
zIndex: theme.zIndex.drawer + 2,
borderRadius: '24px 0 20px 24px',
boxShadow: `-12px 12px 32px -4px ${alpha(
theme.palette.mode === 'light' ? theme.palette.grey[600] : theme.palette.common.black,
0.36
)}`,
}));
const DotStyle = styled('span')(({ theme }) => ({
top: 8,
width: 8,
height: 8,
right: 10,
borderRadius: '50%',
position: 'absolute',
backgroundColor: theme.palette.error.main,
}));
// ----------------------------------------------------------------------
type Props = {
open: boolean;
notDefault: boolean;
onToggle: VoidFunction;
};
export default function ToggleButton({ notDefault, open, onToggle }: Props) {
return (
<RootStyle>
{notDefault && !open && <DotStyle />}
<Tooltip title="Settings" placement="left">
<IconButtonAnimate
color="inherit"
onClick={onToggle}
sx={{
p: 1.25,
transition: (theme) => theme.transitions.create('all'),
'&:hover': {
color: 'primary.main',
bgcolor: (theme) =>
alpha(theme.palette.primary.main, theme.palette.action.hoverOpacity),
},
}}
>
<Iconify icon="eva:options-2-fill" width={20} height={20} />
</IconButtonAnimate>
</Tooltip>
</RootStyle>
);
}

View File

@@ -0,0 +1,189 @@
import { AnimatePresence, m } from 'framer-motion';
import { useState, useEffect } from 'react';
// @mui
import { alpha, styled } from '@mui/material/styles';
import { Backdrop, Divider, Typography, Stack, FormControlLabel, Radio } from '@mui/material';
// hooks
import useSettings from '../../hooks/useSettings';
// utils
import cssStyles from '../../utils/cssStyles';
// config
import { NAVBAR, defaultSettings } from '../../config';
//
import Iconify from '../Iconify';
import Scrollbar from '../Scrollbar';
import { IconButtonAnimate, varFade } from '../animate';
//
import ToggleButton from './ToggleButton';
import SettingMode from './SettingMode';
import SettingLayout from './SettingLayout';
import SettingStretch from './SettingStretch';
import SettingDirection from './SettingDirection';
import SettingFullscreen from './SettingFullscreen';
import SettingColorPresets from './SettingColorPresets';
// ----------------------------------------------------------------------
const RootStyle = styled(m.div)(({ theme }) => ({
...cssStyles(theme).bgBlur({ color: theme.palette.background.paper, opacity: 0.92 }),
top: 0,
right: 0,
bottom: 0,
display: 'flex',
position: 'fixed',
overflow: 'hidden',
width: NAVBAR.BASE_WIDTH,
flexDirection: 'column',
margin: theme.spacing(2),
paddingBottom: theme.spacing(3),
zIndex: theme.zIndex.drawer + 3,
borderRadius: Number(theme.shape.borderRadius) * 1.5,
boxShadow: `-24px 12px 32px -4px ${alpha(
theme.palette.mode === 'light' ? theme.palette.grey[500] : theme.palette.common.black,
0.16
)}`,
}));
// ----------------------------------------------------------------------
export default function Settings() {
const {
themeMode,
themeDirection,
themeColorPresets,
themeStretch,
themeLayout,
onResetSetting,
} = useSettings();
const [open, setOpen] = useState(false);
const notDefault =
themeMode !== defaultSettings.themeMode ||
themeDirection !== defaultSettings.themeDirection ||
themeColorPresets !== defaultSettings.themeColorPresets ||
themeLayout !== defaultSettings.themeLayout ||
themeStretch !== defaultSettings.themeStretch;
const varSidebar =
themeDirection !== 'rtl'
? varFade({
distance: NAVBAR.BASE_WIDTH,
durationIn: 0.32,
durationOut: 0.32,
}).inRight
: varFade({
distance: NAVBAR.BASE_WIDTH,
durationIn: 0.32,
durationOut: 0.32,
}).inLeft;
useEffect(() => {
if (open) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
}, [open]);
const handleToggle = () => {
setOpen((prev) => !prev);
};
const handleClose = () => {
setOpen(false);
};
return (
<>
<Backdrop
open={open}
onClick={handleClose}
sx={{ background: 'transparent', zIndex: (theme) => theme.zIndex.drawer + 1 }}
/>
{!open && <ToggleButton open={open} notDefault={notDefault} onToggle={handleToggle} />}
<AnimatePresence>
{open && (
<>
<RootStyle {...varSidebar}>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{ py: 2, pr: 1, pl: 2.5 }}
>
<Typography variant="subtitle1">Settings</Typography>
<div>
<IconButtonAnimate onClick={onResetSetting}>
<Iconify icon={'ic:round-refresh'} width={20} height={20} />
</IconButtonAnimate>
<IconButtonAnimate onClick={handleClose}>
<Iconify icon={'eva:close-fill'} width={20} height={20} />
</IconButtonAnimate>
</div>
</Stack>
<Divider sx={{ borderStyle: 'dashed' }} />
<Scrollbar sx={{ flexGrow: 1 }}>
<Stack spacing={3} sx={{ p: 3 }}>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Mode</Typography>
<SettingMode />
</Stack>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Direction</Typography>
<SettingDirection />
</Stack>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Layout</Typography>
<SettingLayout />
</Stack>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Presets</Typography>
<SettingColorPresets />
</Stack>
<Stack spacing={1.5}>
<Typography variant="subtitle2">Stretch</Typography>
<SettingStretch />
</Stack>
<SettingFullscreen />
</Stack>
</Scrollbar>
</RootStyle>
</>
)}
</AnimatePresence>
</>
);
}
// ----------------------------------------------------------------------
type Props = {
value: string;
};
export function BoxMask({ value }: Props) {
return (
<FormControlLabel
label=""
value={value}
control={<Radio sx={{ display: 'none' }} />}
sx={{
m: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
position: 'absolute',
}}
/>
);
}

View File

@@ -0,0 +1,45 @@
// ----------------------------------------------------------------------
export type ThemeMode = 'light' | 'dark';
export type ThemeDirection = 'rtl' | 'ltr';
export type ThemeColorPresets = 'default' | 'purple' | 'cyan' | 'blue' | 'orange' | 'red';
export type ThemeLayout = 'vertical' | 'horizontal';
export type ThemeStretch = boolean;
type ColorVariants = {
name: string;
lighter: string;
light: string;
main: string;
dark: string;
darker: string;
contrastText: string;
};
export type SettingsValueProps = {
themeMode: ThemeMode;
themeDirection: ThemeDirection;
themeColorPresets: ThemeColorPresets;
themeStretch: ThemeStretch;
themeLayout: ThemeLayout;
};
export type SettingsContextProps = {
themeMode: ThemeMode;
themeDirection: ThemeDirection;
themeColorPresets: ThemeColorPresets;
themeLayout: ThemeLayout;
themeStretch: boolean;
setColor: ColorVariants;
colorOption: {
name: string;
value: string;
}[];
onToggleMode: VoidFunction;
onToggleStretch: VoidFunction;
onResetSetting: VoidFunction;
onChangeMode: (event: React.ChangeEvent<HTMLInputElement>) => void;
onChangeDirection: (event: React.ChangeEvent<HTMLInputElement>) => void;
onChangeColor: (event: React.ChangeEvent<HTMLInputElement>) => void;
onChangeLayout: (event: React.ChangeEvent<HTMLInputElement>) => void;
};

View File

@@ -0,0 +1,38 @@
// @mui
import { Box, Typography, Stack } from '@mui/material';
// assets
import { UploadIllustration } from '../../assets';
// ----------------------------------------------------------------------
export default function BlockContent() {
return (
<Stack
spacing={2}
alignItems="center"
justifyContent="center"
direction={{ xs: 'column', md: 'row' }}
sx={{ width: 1, textAlign: { xs: 'center', md: 'left' } }}
>
<UploadIllustration sx={{ width: 220 }} />
<Box sx={{ p: 3 }}>
<Typography gutterBottom variant="h5">
Drop or Select file
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Drop files here or click&nbsp;
<Typography
variant="body2"
component="span"
sx={{ color: 'primary.main', textDecoration: 'underline' }}
>
browse
</Typography>
&nbsp;thorough your machine
</Typography>
</Box>
</Stack>
);
}

View File

@@ -0,0 +1,134 @@
import isString from 'lodash/isString';
import { m, AnimatePresence } from 'framer-motion';
// @mui
import { alpha } from '@mui/material/styles';
import { List, Stack, Button, IconButton, ListItemText, ListItem } from '@mui/material';
// utils
import { fData } from '../../utils/formatNumber';
// type
import { UploadMultiFileProps, CustomFile } from './type';
//
import Image from '../Image';
import Iconify from '../Iconify';
import { varFade } from '../animate';
// ----------------------------------------------------------------------
const getFileData = (file: CustomFile | string) => {
if (typeof file === 'string') {
return {
key: file,
};
}
return {
key: file.name,
name: file.name,
size: file.size,
preview: file.preview,
};
};
// ----------------------------------------------------------------------
export default function MultiFilePreview({
showPreview = false,
files,
onRemove,
onRemoveAll,
}: UploadMultiFileProps) {
const hasFile = files.length > 0;
return (
<>
<List disablePadding sx={{ ...(hasFile && { my: 3 }) }}>
<AnimatePresence>
{files.map((file) => {
const { key, name, size, preview } = getFileData(file as CustomFile);
if (showPreview) {
return (
<ListItem
key={key}
component={m.div}
{...varFade().inRight}
sx={{
p: 0,
m: 0.5,
width: 80,
height: 80,
borderRadius: 1.25,
overflow: 'hidden',
position: 'relative',
display: 'inline-flex',
border: (theme) => `solid 1px ${theme.palette.divider}`,
}}
>
<Image alt="preview" src={isString(file) ? file : preview} ratio="1/1" />
<IconButton
size="small"
onClick={() => onRemove(file)}
sx={{
top: 6,
p: '2px',
right: 6,
position: 'absolute',
color: 'common.white',
bgcolor: (theme) => alpha(theme.palette.grey[900], 0.72),
'&:hover': {
bgcolor: (theme) => alpha(theme.palette.grey[900], 0.48),
},
}}
>
<Iconify icon={'eva:close-fill'} />
</IconButton>
</ListItem>
);
}
return (
<ListItem
key={key}
component={m.div}
{...varFade().inRight}
sx={{
my: 1,
px: 2,
py: 0.75,
borderRadius: 0.75,
border: (theme) => `solid 1px ${theme.palette.divider}`,
}}
>
<Iconify
icon={'eva:file-fill'}
sx={{ width: 28, height: 28, color: 'text.secondary', mr: 2 }}
/>
<ListItemText
primary={isString(file) ? file : name}
secondary={isString(file) ? '' : fData(size || 0)}
primaryTypographyProps={{ variant: 'subtitle2' }}
secondaryTypographyProps={{ variant: 'caption' }}
/>
<IconButton edge="end" size="small" onClick={() => onRemove(file)}>
<Iconify icon={'eva:close-fill'} />
</IconButton>
</ListItem>
);
})}
</AnimatePresence>
</List>
{hasFile && (
<Stack direction="row" justifyContent="flex-end" spacing={1.5}>
<Button color="inherit" size="small" onClick={onRemoveAll}>
Remove all
</Button>
<Button size="small" variant="contained">
Upload files
</Button>
</Stack>
)}
</>
);
}

View File

@@ -0,0 +1,47 @@
import { FileRejection } from 'react-dropzone';
// @mui
import { alpha } from '@mui/material/styles';
import { Box, Paper, Typography } from '@mui/material';
// type
import { CustomFile } from './type';
// utils
import { fData } from '../../utils/formatNumber';
// ----------------------------------------------------------------------
type Props = {
fileRejections: FileRejection[];
};
export default function RejectionFiles({ fileRejections }: Props) {
return (
<Paper
variant="outlined"
sx={{
py: 1,
px: 2,
mt: 3,
borderColor: 'error.light',
bgcolor: (theme) => alpha(theme.palette.error.main, 0.08),
}}
>
{fileRejections.map(({ file, errors }) => {
const { path, size }: CustomFile = file;
return (
<Box key={path} sx={{ my: 1 }}>
<Typography variant="subtitle2" noWrap>
{path} - {fData(size)}
</Typography>
{errors.map((error) => (
<Typography key={error.code} variant="caption" component="p">
- {error.message}
</Typography>
))}
</Box>
);
})}
</Paper>
);
}

View File

@@ -0,0 +1,114 @@
import isString from 'lodash/isString';
import { useDropzone } from 'react-dropzone';
// @mui
import { Typography } from '@mui/material';
import { styled } from '@mui/material/styles';
// type
import { UploadProps } from './type';
//
import Image from '../Image';
import Iconify from '../Iconify';
import RejectionFiles from './RejectionFiles';
// ----------------------------------------------------------------------
const RootStyle = styled('div')(({ theme }) => ({
width: 144,
height: 144,
margin: 'auto',
borderRadius: '50%',
padding: theme.spacing(1),
border: `1px dashed ${theme.palette.grey[500_32]}`,
}));
const DropZoneStyle = styled('div')({
zIndex: 0,
width: '100%',
height: '100%',
outline: 'none',
display: 'flex',
overflow: 'hidden',
borderRadius: '50%',
position: 'relative',
alignItems: 'center',
justifyContent: 'center',
'& > *': { width: '100%', height: '100%' },
'&:hover': {
cursor: 'pointer',
'& .placeholder': {
zIndex: 9,
},
},
});
const PlaceholderStyle = styled('div')(({ theme }) => ({
display: 'flex',
position: 'absolute',
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'center',
color: theme.palette.text.secondary,
backgroundColor: theme.palette.background.neutral,
transition: theme.transitions.create('opacity', {
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.shorter,
}),
'&:hover': { opacity: 0.72 },
}));
// ----------------------------------------------------------------------
export default function UploadAvatar({ error, file, helperText, sx, ...other }: UploadProps) {
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
multiple: false,
...other,
});
return (
<>
<RootStyle
sx={{
...((isDragReject || error) && {
borderColor: 'error.light',
}),
...sx,
}}
>
<DropZoneStyle
{...getRootProps()}
sx={{
...(isDragActive && { opacity: 0.72 }),
}}
>
<input {...getInputProps()} />
{file && (
<Image alt="avatar" src={isString(file) ? file : file.preview} sx={{ zIndex: 8 }} />
)}
<PlaceholderStyle
className="placeholder"
sx={{
...(file && {
opacity: 0,
color: 'common.white',
bgcolor: 'grey.900',
'&:hover': { opacity: 0.72 },
}),
...((isDragReject || error) && {
bgcolor: 'error.lighter',
}),
}}
>
<Iconify icon={'ic:round-add-a-photo'} sx={{ width: 24, height: 24, mb: 1 }} />
<Typography variant="caption">{file ? 'Update photo' : 'Upload photo'}</Typography>
</PlaceholderStyle>
</DropZoneStyle>
</RootStyle>
{helperText && helperText}
{fileRejections.length > 0 && <RejectionFiles fileRejections={fileRejections} />}
</>
);
}

View File

@@ -0,0 +1,69 @@
import { useDropzone } from 'react-dropzone';
// @mui
import { styled } from '@mui/material/styles';
import { Box } from '@mui/material';
// type
import { UploadMultiFileProps } from './type';
//
import BlockContent from './BlockContent';
import RejectionFiles from './RejectionFiles';
import MultiFilePreview from './MultiFilePreview';
// ----------------------------------------------------------------------
const DropZoneStyle = styled('div')(({ theme }) => ({
outline: 'none',
padding: theme.spacing(5, 1),
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.neutral,
border: `1px dashed ${theme.palette.grey[500_32]}`,
'&:hover': { opacity: 0.72, cursor: 'pointer' },
}));
// ----------------------------------------------------------------------
export default function UploadMultiFile({
error,
showPreview = false,
files,
onRemove,
onRemoveAll,
helperText,
sx,
...other
}: UploadMultiFileProps) {
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
...other,
});
return (
<Box sx={{ width: '100%', ...sx }}>
<DropZoneStyle
{...getRootProps()}
sx={{
...(isDragActive && { opacity: 0.72 }),
...((isDragReject || error) && {
color: 'error.main',
borderColor: 'error.light',
bgcolor: 'error.lighter',
}),
}}
>
<input {...getInputProps()} />
<BlockContent />
</DropZoneStyle>
{fileRejections.length > 0 && <RejectionFiles fileRejections={fileRejections} />}
<MultiFilePreview
files={files}
showPreview={showPreview}
onRemove={onRemove}
onRemoveAll={onRemoveAll}
/>
{helperText && helperText}
</Box>
);
}

View File

@@ -0,0 +1,82 @@
import isString from 'lodash/isString';
import { useDropzone } from 'react-dropzone';
// @mui
import { styled } from '@mui/material/styles';
import { Box } from '@mui/material';
// type
import { UploadProps } from './type';
//
import Image from '../Image';
import RejectionFiles from './RejectionFiles';
import BlockContent from './BlockContent';
// ----------------------------------------------------------------------
const DropZoneStyle = styled('div')(({ theme }) => ({
outline: 'none',
overflow: 'hidden',
position: 'relative',
padding: theme.spacing(5, 1),
borderRadius: theme.shape.borderRadius,
transition: theme.transitions.create('padding'),
backgroundColor: theme.palette.background.neutral,
border: `1px dashed ${theme.palette.grey[500_32]}`,
'&:hover': { opacity: 0.72, cursor: 'pointer' },
}));
// ----------------------------------------------------------------------
export default function UploadSingleFile({
error = false,
file,
helperText,
sx,
...other
}: UploadProps) {
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
multiple: false,
...other,
});
return (
<Box sx={{ width: '100%', ...sx }}>
<DropZoneStyle
{...getRootProps()}
sx={{
...(isDragActive && { opacity: 0.72 }),
...((isDragReject || error) && {
color: 'error.main',
borderColor: 'error.light',
bgcolor: 'error.lighter',
}),
...(file && {
padding: '12% 0',
}),
}}
>
<input {...getInputProps()} />
<BlockContent />
{file && (
<Image
alt="file preview"
src={isString(file) ? file : file.preview}
sx={{
top: 8,
left: 8,
borderRadius: 1,
position: 'absolute',
width: 'calc(100% - 16px)',
height: 'calc(100% - 16px)',
}}
/>
)}
</DropZoneStyle>
{fileRejections.length > 0 && <RejectionFiles fileRejections={fileRejections} />}
{helperText && helperText}
</Box>
);
}

View File

@@ -0,0 +1,5 @@
export * from './type';
export { default as UploadAvatar } from './UploadAvatar';
export { default as UploadMultiFile } from './UploadMultiFile';
export { default as UploadSingleFile } from './UploadSingleFile';

View File

@@ -0,0 +1,29 @@
import { ReactNode } from 'react';
import { DropzoneOptions } from 'react-dropzone';
// @mui
import { SxProps } from '@mui/material';
import { Theme } from '@mui/material/styles';
// ----------------------------------------------------------------------
export interface CustomFile extends File {
path?: string;
preview?: string;
}
export interface UploadProps extends DropzoneOptions {
error?: boolean;
file: CustomFile | string | null;
helperText?: ReactNode;
sx?: SxProps<Theme>;
}
export interface UploadMultiFileProps extends DropzoneOptions {
error?: boolean;
files: (File | string)[];
showPreview: boolean;
onRemove: (file: File | string) => void;
onRemoveAll: VoidFunction;
sx?: SxProps<Theme>;
helperText?: ReactNode;
}