Separate Client Portal & Dashboard

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

View File

@@ -0,0 +1,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;
}