hospital portal

This commit is contained in:
pajri
2023-02-01 16:22:03 +07:00
parent 8d5f4bb0bd
commit a7e688a52c
340 changed files with 46280 additions and 1 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,17 @@
import { Pagination } from "@mui/material";
import { Box } from "@mui/system";
import { LaravelPaginatedData } from "../@types/paginated-data";
export interface Props {
paginationData?: LaravelPaginatedData;
onPageChange: any;
}
export default function BasePagination({ paginationData, onPageChange }: Props) {
return (
<Box sx={{ m: 2 }} display="flex" justifyContent="flex-end">
<Pagination count={paginationData?.last_page} page={paginationData?.current_page} variant="outlined" shape="rounded" onChange={onPageChange}/>
</Box>
)
}

View File

@@ -0,0 +1,92 @@
import { ReactElement } from 'react';
import { Link as RouterLink } from 'react-router-dom';
// @mui
import {
Box,
Link,
Typography,
BreadcrumbsProps,
Breadcrumbs as MUIBreadcrumbs,
} from '@mui/material';
// ----------------------------------------------------------------------
type TLink = {
href?: string;
name: string;
icon?: ReactElement;
};
export interface Props extends BreadcrumbsProps {
links: TLink[];
activeLast?: boolean;
}
export default function Breadcrumbs({ links, activeLast = false, ...other }: Props) {
const currentLink = links[links.length - 1].name;
const listDefault = links.map((link) => <LinkItem key={link.name} link={link} />);
const listActiveLast = links.map((link) => (
<div key={link.name}>
{link.name !== currentLink ? (
<LinkItem link={link} />
) : (
<Typography
variant="body2"
sx={{
maxWidth: 260,
overflow: 'hidden',
whiteSpace: 'nowrap',
color: 'text.disabled',
textOverflow: 'ellipsis',
}}
>
{currentLink}
</Typography>
)}
</div>
));
return (
<MUIBreadcrumbs
separator={
<Box
component="span"
sx={{ width: 4, height: 4, borderRadius: '50%', bgcolor: 'text.disabled' }}
/>
}
{...other}
>
{activeLast ? listDefault : listActiveLast}
</MUIBreadcrumbs>
);
}
// ----------------------------------------------------------------------
type LinkItemProps = {
link: TLink;
};
function LinkItem({ link }: LinkItemProps) {
const { href, name, icon } = link;
return (
<Link
key={name}
variant="body2"
component={RouterLink}
to={href || '#'}
sx={{
lineHeight: 2,
display: 'flex',
alignItems: 'center',
color: 'text.primary',
'& > div': { display: 'inherit' },
}}
>
{icon && <Box sx={{ mr: 1, '& svg': { width: 20, height: 20 } }}>{icon}</Box>}
{name}
</Link>
);
}

View File

@@ -0,0 +1,60 @@
import { ReactNode } from 'react';
import { isString } from 'lodash';
// @mui
import { Box, Typography, Link } from '@mui/material';
//
import Breadcrumbs, { Props as BreadcrumbsProps } from './Breadcrumbs';
// ----------------------------------------------------------------------
interface Props extends BreadcrumbsProps {
action?: ReactNode;
heading: string;
moreLink?: string | string[];
}
export default function HeaderBreadcrumbs({
links,
action,
heading,
moreLink = '' || [],
sx,
...other
}: Props) {
return (
<Box sx={{ mb: 5, ...sx }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h4" gutterBottom>
{heading}
</Typography>
<Breadcrumbs links={links} {...other} />
</Box>
{action && <Box sx={{ flexShrink: 0 }}>{action}</Box>}
</Box>
<Box sx={{ mt: 2 }}>
{isString(moreLink) ? (
<Link href={moreLink} target="_blank" rel="noopener" variant="body2">
{moreLink}
</Link>
) : (
moreLink.map((href) => (
<Link
noWrap
key={href}
href={href}
variant="body2"
target="_blank"
rel="noopener"
sx={{ display: 'table' }}
>
{href}
</Link>
))
)}
</Box>
</Box>
);
}

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,28 @@
import { Card, Paper, TableContainer } from "@mui/material";
import { LaravelPaginatedData } from "../@types/paginated-data";
import BasePagination from "./BasePagination";
type LaravelTableProps = {
isLoading: boolean;
lastRequest: number;
data: LaravelPaginatedData;
handlePageChange: void;
TableContent: any;
};
function LaravelTable(props: LaravelTableProps) {
return (
<Card>
<TableContainer component={Paper}>
{ props.TableContent }
</TableContainer>
{ !props.isLoading ?
(<BasePagination paginationData={props.data} onPageChange={props.handlePageChange}/>) :
(<div></div>)
}
</Card>
)
}
export default LaravelTable

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,29 @@
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 }}>
<img src="/logo/logo-linksehat.png" alt="LinkSehat" />
</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,56 @@
import { Dialog, DialogTitle, DialogContent, Stack, Typography, IconButton } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { ReactElement } from 'react';
import Iconify from './Iconify';
// ----------------------------------------------------------------------
type MuiDialogProps = {
title?: {
name?: string;
icon?: string;
};
openDialog: boolean;
setOpenDialog: Function;
content?: ReactElement;
maxWidth?: string;
};
// ----------------------------------------------------------------------
const MuiDialog = ({ title, openDialog, setOpenDialog, content, maxWidth }: MuiDialogProps) => {
const handleClose = () => {
setOpenDialog(false);
};
let maxWidthDialog = 'md';
if (maxWidth) {
maxWidthDialog = maxWidth;
}
return (
<Dialog open={openDialog} onClose={handleClose} fullWidth={true} maxWidth={maxWidthDialog}>
<DialogTitle sx={{ backgroundColor: '#19BBBB', color: '#FFF', padding: 2 }}>
<Stack direction="row" alignItems="center" justifyContent="space-between">
{title?.icon ? (
<Stack direction="row">
<Iconify icon={title?.icon} width={25} height={25} sx={{ marginRight: '10px' }} />
<Typography variant="h6">{title?.name}</Typography>
</Stack>
) : (
<Typography variant="h6">{title?.name ? title?.name : ''}</Typography>
)}
<IconButton sx={{ color: '#FFF' }} onClick={handleClose}>
<CloseIcon />
</IconButton>
</Stack>
</DialogTitle>
<DialogContent sx={{ backgroundColor: '#F9FAFB' }}>
{content ? content : 'Testing Content Dialog'}
</DialogContent>
</Dialog>
);
};
export default MuiDialog;

View File

@@ -0,0 +1,93 @@
import React, { Dispatch, FunctionComponent, useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { Box, Stack, Typography } from '@mui/material';
import BlockContent from './upload/BlockContent';
import { styled } from '@mui/material/styles';
import { UploadIllustration } from '../assets';
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,
backgroundColor: '#ffffff',
border: `2px dashed ${theme.palette.grey[500_32]}`,
'&:hover': { cursor: 'pointer', backgroundColor: '#f7f7f7' },
}));
const MyDropzone: FunctionComponent<{
setFile: Dispatch<any>;
currentImage: string;
}> = ({
setFile,
currentImage,
// onChanges,
}) => {
const onDrop = useCallback((acceptedFiles) => {
// Do something with the files
console.log(acceptedFiles);
setFile(acceptedFiles[0]);
setImage(acceptedFiles[0]);
// onChanges(acceptedFiles[0]);
}, []);
const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({
onDrop,
multiple: false,
});
const [image, setImage] = useState<File | null>(null);
return (
<DropZoneStyle
{...getRootProps()}
sx={{
...(isDragActive && { opacity: 2.72 }),
}}
>
<input {...getInputProps()} />
<Stack
spacing={2}
alignItems="center"
justifyContent="center"
direction={{ xs: 'column', md: 'row' }}
sx={{ width: 1, textAlign: { xs: 'center', md: 'left' } }}
>
{image ? (
<img src={URL.createObjectURL(image)} alt="preview" width={220} />
) : currentImage ? (
<img src={currentImage} alt="preview" width={220} />
) : (
<UploadIllustration sx={{ width: 220 }} />
)}
<Box sx={{ p: 3 }}>
<Typography gutterBottom variant="h5">
Pilih Foto
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Letakkan foto disini atau klik jelajahi &nbsp;
<Typography
variant="body2"
component="span"
sx={{ color: 'primary.main', textDecoration: 'underline' }}
>
jelajahi
</Typography>
&nbsp;foto di perangkat Anda
</Typography>
</Box>
</Stack>
{/* <BlockContent file={image} /> */}
{isDragReject && <p>Unsupported file type...</p>}
</DropZoneStyle>
);
};
export default MyDropzone;

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} | LinkSehat`}</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,128 @@
import React, { Dispatch, FunctionComponent, useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { Box, Stack, Typography } from '@mui/material';
import BlockContent from './upload/BlockContent';
import { styled } from '@mui/material/styles';
import { UploadIllustration } from '../assets';
import Iconify from './Iconify';
const RootStyle = styled('div')(({ theme }) => ({
width: 144,
height: 144,
margin: 'auto',
borderRadius: '50%',
padding: theme.spacing(1),
border: `2px 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 },
}));
const UploadImage: FunctionComponent<{
setFile: Dispatch<any>;
currentImage: string;
}> = ({ setFile, currentImage, setSave, error, file, helperText, sx, ...other }) => {
const onDrop = useCallback(
(acceptedFiles) => {
// Do something with the files
console.log(acceptedFiles);
setFile(acceptedFiles[0]);
setImage(acceptedFiles[0]);
},
[setFile, setSave]
);
const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({
onDrop,
multiple: false,
});
const [image, setImage] = useState<File | null>(null);
return (
<RootStyle
sx={{
...((isDragReject || error) && {
borderColor: 'error.light',
}),
...sx,
}}
>
<DropZoneStyle
{...getRootProps()}
sx={{
...(isDragActive && { opacity: 2.72 }),
}}
>
<input {...getInputProps()} />
{/* <Stack
spacing={2}
alignItems="center"
justifyContent="center"
direction={{ xs: 'column', md: 'row' }}
sx={{ width: 1, textAlign: { xs: 'center', md: 'left' } }}
> */}
{image ? (
<img src={URL.createObjectURL(image)} alt="preview" width={220} />
) : currentImage ? (
<img src={currentImage} alt="preview" width={220} />
) : (
<PlaceholderStyle
className="placeholder"
sx={{
...((isDragReject || error) && {
bgcolor: 'error.lighter',
}),
}}
>
<Iconify icon={'ic:round-add-a-photo'} sx={{ width: 24, height: 24, mb: 1 }} />
<Typography variant="caption">{image ? 'Update photo' : 'Upload photo'}</Typography>
</PlaceholderStyle>
)}
<PlaceholderStyle className="placeholder">
<Iconify icon={'ic:round-add-a-photo'} sx={{ width: 24, height: 24, mb: 1 }} />
<Typography variant="caption">{image ? 'Update photo' : 'Upload photo'}</Typography>
</PlaceholderStyle>
{/* </Stack> */}
{/* <BlockContent file={image} /> */}
{isDragReject && <p>Unsupported file type...</p>}
</DropZoneStyle>
</RootStyle>
);
};
export default UploadImage;

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,191 @@
import { ApexOptions } from 'apexcharts';
// @mui
import { useTheme } from '@mui/material/styles';
// ----------------------------------------------------------------------
export default function BaseOptionChart(): ApexOptions {
const theme = useTheme();
const LABEL_TOTAL = {
show: true,
label: 'Total',
color: theme.palette.text.secondary,
fontSize: theme.typography.subtitle2.fontSize as string,
fontWeight: theme.typography.subtitle2.fontWeight,
lineHeight: theme.typography.subtitle2.lineHeight,
};
const LABEL_VALUE = {
offsetY: 8,
color: theme.palette.text.primary,
fontSize: theme.typography.h3.fontSize as string,
fontWeight: theme.typography.h3.fontWeight,
lineHeight: theme.typography.h3.lineHeight,
};
return {
// Colors
colors: [
theme.palette.primary.main,
theme.palette.chart.yellow[0],
theme.palette.chart.blue[0],
theme.palette.chart.violet[0],
theme.palette.chart.green[0],
theme.palette.chart.red[0],
],
// Chart
chart: {
toolbar: { show: false },
zoom: { enabled: false },
// animations: { enabled: false },
foreColor: theme.palette.text.disabled,
fontFamily: theme.typography.fontFamily,
},
// States
states: {
hover: {
filter: {
type: 'lighten',
value: 0.04,
},
},
active: {
filter: {
type: 'darken',
value: 0.88,
},
},
},
// Fill
fill: {
opacity: 1,
gradient: {
type: 'vertical',
shadeIntensity: 0,
opacityFrom: 0.4,
opacityTo: 0,
stops: [0, 100],
},
},
// Datalabels
dataLabels: { enabled: false },
// Stroke
stroke: {
width: 3,
curve: 'smooth',
lineCap: 'round',
},
// Grid
grid: {
strokeDashArray: 3,
borderColor: theme.palette.divider,
},
// Xaxis
xaxis: {
axisBorder: { show: false },
axisTicks: { show: false },
},
// Markers
markers: {
size: 0,
strokeColors: theme.palette.background.paper,
},
// Tooltip
tooltip: {
x: {
show: false,
},
},
// Legend
legend: {
show: true,
fontSize: String(13),
position: 'top',
horizontalAlign: 'right',
markers: {
radius: 12,
},
fontWeight: 500,
itemMargin: { horizontal: 12 },
labels: {
colors: theme.palette.text.primary,
},
},
// plotOptions
plotOptions: {
// Bar
bar: {
columnWidth: '28%',
borderRadius: 4,
},
// Pie + Donut
pie: {
donut: {
labels: {
show: true,
value: LABEL_VALUE,
total: LABEL_TOTAL,
},
},
},
// Radialbar
radialBar: {
track: {
strokeWidth: '100%',
background: theme.palette.grey[500_16],
},
dataLabels: {
value: LABEL_VALUE,
total: LABEL_TOTAL,
},
},
// Radar
radar: {
polygons: {
fill: { colors: ['transparent'] },
strokeColors: theme.palette.divider,
connectorColors: theme.palette.divider,
},
},
// polarArea
polarArea: {
rings: {
strokeColor: theme.palette.divider,
},
spokes: {
connectorColors: theme.palette.divider,
},
},
},
// Responsive
responsive: [
{
// sm
breakpoint: theme.breakpoints.values.sm,
options: {
plotOptions: { bar: { columnWidth: '40%' } },
},
},
{
// md
breakpoint: theme.breakpoints.values.md,
options: {
plotOptions: { bar: { columnWidth: '32%' } },
},
},
],
};
}

View File

@@ -0,0 +1,58 @@
// @mui
import { alpha, useTheme } from '@mui/material/styles';
import { GlobalStyles } from '@mui/material';
// utils
import cssStyles from '../../utils/cssStyles';
// ----------------------------------------------------------------------
export default function ChartStyle() {
const theme = useTheme();
return (
<GlobalStyles
styles={{
'&.apexcharts-canvas': {
// Tooltip
'.apexcharts-xaxistooltip': {
...cssStyles(theme).bgBlur(),
border: 0,
color: theme.palette.text.primary,
boxShadow: theme.customShadows.dropdown,
borderRadius: Number(theme.shape.borderRadius) * 1.5,
'&:before': { borderBottomColor: 'transparent' },
'&:after': { borderBottomColor: alpha(theme.palette.background.default, 0.8) },
},
'.apexcharts-tooltip.apexcharts-theme-light': {
...cssStyles(theme).bgBlur(),
border: 0,
boxShadow: theme.customShadows.dropdown,
borderRadius: Number(theme.shape.borderRadius) * 1.5,
'& .apexcharts-tooltip-title': {
border: 0,
textAlign: 'center',
fontWeight: theme.typography.fontWeightBold,
backgroundColor: theme.palette.grey[500_16],
color: theme.palette.text[theme.palette.mode === 'light' ? 'secondary' : 'primary'],
},
},
// Legend
'.apexcharts-legend': {
padding: 0,
},
'.apexcharts-legend-series': {
display: 'flex !important',
alignItems: 'center',
},
'.apexcharts-legend-marker': {
marginRight: 8,
},
'.apexcharts-legend-text': {
lineHeight: '18px',
textTransform: 'capitalize',
},
},
}}
/>
);
}

View File

@@ -0,0 +1,2 @@
export { default as ChartStyle } from './ChartStyle';
export { default as BaseOptionChart } from './BaseOptionChart';

View File

@@ -0,0 +1,275 @@
// @mui
import { styled } from '@mui/material/styles';
import {
Typography,
LinearProgress,
linearProgressClasses,
Stack,
FormControlLabel,
TableRow,
TableCell,
Table,
TableBody,
TextField,
Button,
} from '@mui/material';
import { LoadingButton } from '@mui/lab';
import Checkbox from '@mui/material/Checkbox';
// components
import { FormProvider, RHFTextField } from '../../components/hook-form';
// React
import React, { ReactElement, useEffect, useState } from 'react';
import { fCurrency } from '../../utils/formatNumber';
// yup
import * as Yup from 'yup';
// form
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import MuiDialog from '../MuiDialog';
import { Member } from '../../@types/member';
import { LaravelPaginatedDataDefault } from '../../@types/paginated-data';
import DataTable from '../LaravelTable';
import axios from '../../utils/axios';
// ----------------------------------------------------------------------
type DataContent = {
info: string;
date: string;
time: string;
};
type MuiDialogProps = {
title?: {
name?: string;
icon?: string;
};
openDialog: boolean;
setOpenDialog: Function;
content?: ReactElement;
onSelect?: Function;
};
type FormValuesProps = {
topup: string;
};
// ----------------------------------------------------------------------
const testData = {
companyName: 'PT. Aman Mineral',
policyNumber: 12345678,
totalMembers: 50,
totalCases: 100,
totalPersen: 75,
myLimit: '375.000.000',
totalLimit: 500000000,
};
const BorderLinearProgress = styled(LinearProgress)(({ theme }) => ({
height: 10,
borderRadius: 6,
[`&.${linearProgressClasses.colorPrimary}`]: {
backgroundColor: theme.palette.grey[theme.palette.mode === 'light' ? 300 : 800],
},
[`& .${linearProgressClasses.bar}`]: {
borderRadius: 6,
background: 'linear-gradient(270deg, #19BBBB 38.42%, #FF9565 76.21%, #FE7253 104.02%)',
},
}));
// ----------------------------------------------------------------------
export default function MemberSelectDialog({
openDialog,
setOpenDialog,
onSelect
}: MuiDialogProps) {
const [dataTableLoading, setDataTableLoading] = useState(false)
const [dataTableData, setDataTableData] = useState(LaravelPaginatedDataDefault)
const [searchFilter, setSearchFilter] = useState({
search: ''
})
const handleSearchChange = (event: any) => {
setSearchFilter({...searchFilter, ...{search: event.target.value}})
}
const loadDataTableData = async (appliedFilter : any | null = null) => {
setDataTableLoading(true);
const filter = appliedFilter ? appliedFilter : {};
const response = await axios.get('/members', { params: filter });
// console.log(response.data);
setDataTableLoading(false);
setDataTableData(response.data);
}
const applyFilter = async (searchFilter: {search: string}) => {
await loadDataTableData(searchFilter);
}
const handleFilterSubmit = (event: any) => {
event.preventDefault();
applyFilter(searchFilter)
}
useEffect(() => {
console.log('effect openDialog');
if (openDialog === false) {
}
}, [openDialog]);
const handlePageChange = () => {
console.log('handle change')
}
// Called on every row to map the data to the columns
function createData(member: Member): Member {
return {
...member,
};
}
const Row = (props: { row: ReturnType<typeof createData>, onSelect }) => (
<React.Fragment>
<TableRow sx={{ '& > *': { borderBottom: 'unset' } }}>
{/* <TableCell>
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowDownIcon /> : <KeyboardArrowRightIcon />}
</IconButton>
</TableCell> */}
{/* { columns.map((column, index) =>
<TableCell key={ index } align={ column.align } minwidth={ column.minWidth }>{ row[column.id] ?? '-' }</TableCell>
) } */}
{/* TODO FIX DISPLAY DATA FROM RELATION */}
{/* { id: 'member_id', label: 'MemberID', minWidth: 100, align: "left" },
{ id: 'principal_id', label: 'Mapping ID', minWidth: 100, align: "left" },
{ id: 'nik', label: 'NIK', minWidth: 100, align: "left" },
{ id: 'current_policy.policy_number', label: 'Policy Number', minWidth: 100, align: "left" },
{ id: 'effective_date', label: 'Effective Date', minWidth: 100, align: "left" },
{ id: 'name', label: 'Name', minWidth: 100, align: "left" },
{ id: 'nric', label: 'NRIC', minWidth: 100, align: "left" },
{ id: 'email', label: 'E-mail', minWidth: 100, align: "left" },
{ id: 'plan_id', label: 'PlanID', minWidth: 100, align: "left" },
{ id: 'termination_date', label: 'Termination Date', minWidth: 100, align: 'right' },
{ id: 'activation_date', label: 'Activation Date', minWidth: 100, align: "right" },
*/}
<TableCell align="left">{props.row.member_id}</TableCell>
<TableCell align="left">{props.row.principal_id}</TableCell>
<TableCell align="left">{props.row.employeds?.[0]?.nik}</TableCell>
<TableCell align="left">{props.row.current_policy?.policy_id}</TableCell>
<TableCell align="left">{props.row.current_policy?.start}</TableCell>
<TableCell align="left">{props.row.name}</TableCell>
<TableCell align="left">{props.row.nric}</TableCell>
<TableCell align="left">{props.row.email}</TableCell>
<TableCell align="left">{props.row.current_plan?.code}</TableCell>
<TableCell align="left">{props.row.current_policy?.start}</TableCell>
<TableCell align="left">{props.row.current_policy?.end}</TableCell>
<TableCell align="right" style={{ position: 'sticky', right: 0}}><Button variant="outlined" color="success" style={{backgroundColor: "#ffffff"}} size="small" onClick={() => {onSelect(props.row); setOpenDialog(false); }}>Select</Button></TableCell>
</TableRow>
{/* COLLAPSIBLE ROW */}
{/* <TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={10}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ borderBottom: 1 }}>
<Typography variant="body2" gutterBottom component="div">
No Extra Data
</Typography>
</Box>
{false && <Box sx={{ margin: 1 }} />}
</Collapse>
</TableCell>
</TableRow> */}
</React.Fragment>
);
const headStyle = {
fontWeight: 'bold',
};
const TableContent = () => (
<Table aria-label="collapsible table">
{/* ------------------ TABLE HEADER ------------------ */}
<TableBody>
<TableRow>
<TableCell style={headStyle} align="left" />
<TableCell style={headStyle} align="left">
Type
</TableCell>
<TableCell style={headStyle} align="left">
Code
</TableCell>
<TableCell style={headStyle} align="left">
Name
</TableCell>
<TableCell style={headStyle} align="left">
Version
</TableCell>
<TableCell style={headStyle} align="right">
Status
</TableCell>
<TableCell style={headStyle} align="right">
Action
</TableCell>
</TableRow>
</TableBody>
{/* ------------------ END TABLE HEADER ------------------ */}
{/* ------------------ TABLE ROW ------------------ */}
{dataTableLoading ? (
<TableBody>
<TableRow>
<TableCell colSpan={8} align="center">
Loading
</TableCell>
</TableRow>
</TableBody>
) : dataTableData.data.length === 0 ? (
<TableBody>
<TableRow>
<TableCell colSpan={8} align="center">
No Data
</TableCell>
</TableRow>
</TableBody>
) : (
<TableBody>
{dataTableData.data.map((row) => (
<Row key={row.id} row={row} onSelect/>
))}
</TableBody>
)}
{/* ------------------ END TABLE ROW ------------------ */}
</Table>
);
const getContent = () => (
<Stack spacing={1} marginTop={2}>
<form onSubmit={handleFilterSubmit}>
<TextField label="Search" variant="outlined" fullWidth onChange={handleSearchChange} value={searchFilter.search} autoFocus/>
</form>
<DataTable
isLoading={dataTableLoading}
lastRequest={0}
data={dataTableData}
handlePageChange={handlePageChange}
TableContent={<TableContent />}
/>
</Stack>
);
return (
<MuiDialog
title={{name: "Select Member"}}
openDialog={openDialog}
setOpenDialog={setOpenDialog}
content={getContent()}
maxWidth="xl"
/>
);
}

View File

@@ -0,0 +1,179 @@
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>
{!isSimple && (
<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,100 @@
import { ReactNode } from 'react';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
// @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 {
id?: string;
error?: boolean;
value?: string;
onChange?: (value: string) => void;
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: false, // need highlightjs
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,41 @@
import { Autocomplete, TextField, TextFieldProps } from '@mui/material';
import { useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
interface IProps {
options: any[];
name: string;
label?: string;
}
export default function RHFAutocomplete({ name, options, label, ...other }: IProps & TextFieldProps) {
const { control } = useFormContext();
console.log(control)
const [value, setValue] = useState<string | null>(options[0]);
const [inputValue, setInputValue] = useState('');
return (
<Controller
name={name}
control={control}
render={({ field, fieldState: { error } }) => (
<Autocomplete
{...field}
options={options}
value={value}
onChange={(event: any, newValue: string | null) => {
// console.log('fuck', newValue)
setValue(newValue.id);
}}
inputValue={inputValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
renderInput={(params) => <TextField {...params} label={label ?? name} />}
{...other}
/>
)}
/>
);
}

View File

@@ -0,0 +1,115 @@
// 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>
);
}}
/>
);
}
// ----------------------------------------------------------------------
interface optionsCustomInterface {
value: string;
label: string;
}
interface RHFCustomMultiCheckboxProps {
name: string;
options: optionsCustomInterface[];
}
export function RHFCustomMultiCheckbox({ name, options, ...other }: RHFCustomMultiCheckboxProps) {
const { control } = useFormContext();
return (
<Controller
name={name}
control={control}
render={({ field }) => {
const onSelected = (option: optionsCustomInterface) =>
field.value.includes(option.value)
? field.value.filter((value: string) => value !== option.value)
: [...field.value, option.value];
return (
<FormGroup>
{options.map((option, index) => (
<FormControlLabel
key={index}
control={
<Checkbox
checked={field.value.includes(option.value)}
onChange={() => field.onChange(onSelected(option))}
/>
}
label={option.label}
{...other}
/>
))}
</FormGroup>
);
}}
/>
);
}

View File

@@ -0,0 +1,65 @@
// form
import { useFormContext, Controller } from 'react-hook-form';
import * as React from 'react';
// @mui
import { IconButton, TextField, TextFieldProps } from '@mui/material';
import { LocalizationProvider, MobileDatePicker } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { DesktopDatePicker } from '@mui/x-date-pickers/DesktopDatePicker';
import InputAdornment from '@mui/material/InputAdornment';
import EventIcon from '@mui/icons-material/Event';
import { fPostFormat } from '../../utils/formatTime';
// ----------------------------------------------------------------------
interface IProps {
name: string;
}
export default function RHFDatepicker({ name, ...other }: IProps & TextFieldProps) {
const { control } = useFormContext();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState: { error } }) => (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<MobileDatePicker
inputFormat="yyyy-MM-dd"
value={field.value}
onChange={(value) => {
field.onChange(fPostFormat(value));
}}
renderInput={(field) => (
<TextField
{...field}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<EventIcon />
</InputAdornment>
),
}}
error={!!error}
helperText={error?.message}
{...other}
/>
)}
/>
{/* <DesktopDatePicker
value={field.value}
onChange={(value) => {
field.onChange(fPostFormat(value));
}}
renderInput={(params) => <TextField {...params} fullWidth />}
/> */}
</LocalizationProvider>
)}
/>
);
}

View File

@@ -0,0 +1,38 @@
// 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}
simple={true}
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,56 @@
// form
import { useFormContext, Controller } from 'react-hook-form';
// @mui
import { Switch, FormControlLabel, FormControlLabelProps } from '@mui/material';
import { useEffect, useState } from 'react';
// ----------------------------------------------------------------------
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}
/>
);
}
// interface Props extends IProps {
// valueTrue: any,
// valueFalse: any,
// }
// export function RHFSwitchEnum({ name, valueTrue, valueFalse, ...other}: Props) {
// const { control } = useFormContext();
// const [isChecked, setIsChecked] = useState(valueTrue === field.value);
// useEffect(() => {
// // setIsChecked()
// }, [])
// 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,110 @@
// 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,11 @@
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';
export { default as RHFDatepicker } from './RHFDatepicker';

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, list.openWhen);
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,17 @@
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, openWhen?: string[]) {
const listPathWhenActive = [ ...openWhen ?? [], path ];
return listPathWhenActive.includes(pathname);
// return path ? !!matchPath({ path: path, end: false }, pathname) : false;
}

View File

@@ -0,0 +1,35 @@
import { ReactElement } from 'react';
import { BoxProps } from '@mui/material';
// ----------------------------------------------------------------------
export type NavListProps = {
title: string;
path: string;
icon?: ReactElement;
info?: ReactElement;
openWhen? : string[];
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, list.openWhen);
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,53 @@
// @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, index) => (
<List key={index} disablePadding sx={{ px: 2 }}>
<ListSubheaderStyle
key={index}
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;
}