Separate Client Portal & Dashboard
This commit is contained in:
38
frontend/dashboard/src/components/upload/BlockContent.tsx
Normal file
38
frontend/dashboard/src/components/upload/BlockContent.tsx
Normal 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
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="span"
|
||||
sx={{ color: 'primary.main', textDecoration: 'underline' }}
|
||||
>
|
||||
browse
|
||||
</Typography>
|
||||
thorough your machine
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
134
frontend/dashboard/src/components/upload/MultiFilePreview.tsx
Normal file
134
frontend/dashboard/src/components/upload/MultiFilePreview.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
47
frontend/dashboard/src/components/upload/RejectionFiles.tsx
Normal file
47
frontend/dashboard/src/components/upload/RejectionFiles.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
114
frontend/dashboard/src/components/upload/UploadAvatar.tsx
Normal file
114
frontend/dashboard/src/components/upload/UploadAvatar.tsx
Normal 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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
69
frontend/dashboard/src/components/upload/UploadMultiFile.tsx
Normal file
69
frontend/dashboard/src/components/upload/UploadMultiFile.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
5
frontend/dashboard/src/components/upload/index.ts
Normal file
5
frontend/dashboard/src/components/upload/index.ts
Normal 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';
|
||||
29
frontend/dashboard/src/components/upload/type.ts
Normal file
29
frontend/dashboard/src/components/upload/type.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user