Files
HKSingleParty/03_source/frontend/src/components/upload/upload-avatar.tsx
louiscklaw 834f58bde1 update,
2025-05-30 01:14:10 +08:00

135 lines
3.8 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useDropzone } from 'react-dropzone';
import { varAlpha, mergeClasses } from 'minimal-shared/utils';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { Image } from '../image';
import { Iconify } from '../iconify';
import { uploadClasses } from './classes';
import { RejectionFiles } from './components/rejection-files';
import type { UploadProps } from './types';
import { useTranslation } from 'react-i18next';
// ----------------------------------------------------------------------
export function UploadAvatar({ sx, error, value, disabled, helperText, className, ...other }: UploadProps) {
const { t } = useTranslation();
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
multiple: false,
disabled,
accept: { 'image/*': [] },
...other,
});
const hasFile = !!value;
const hasError = isDragReject || !!error;
const [preview, setPreview] = useState('');
useEffect(() => {
if (typeof value === 'string') {
setPreview(value);
} else if (value instanceof File) {
setPreview(URL.createObjectURL(value));
}
}, [value]);
const renderPreview = () => hasFile && <Image alt="Avatar" src={preview} sx={{ width: 1, height: 1, borderRadius: '50%' }} />;
const renderPlaceholder = () => (
<Box
className="upload-placeholder"
sx={(theme) => ({
top: 0,
gap: 1,
left: 0,
width: 1,
height: 1,
zIndex: 9,
display: 'flex',
borderRadius: '50%',
position: 'absolute',
alignItems: 'center',
color: 'text.disabled',
flexDirection: 'column',
justifyContent: 'center',
bgcolor: varAlpha(theme.vars.palette.grey['500Channel'], 0.08),
transition: theme.transitions.create(['opacity'], {
duration: theme.transitions.duration.shorter,
}),
'&:hover': { opacity: 0.72 },
...(hasError && {
color: 'error.main',
bgcolor: varAlpha(theme.vars.palette.error.mainChannel, 0.08),
}),
...(hasFile && {
zIndex: 9,
opacity: 0,
color: 'common.white',
bgcolor: varAlpha(theme.vars.palette.grey['900Channel'], 0.64),
}),
})}
>
<Iconify icon="solar:camera-add-bold" width={32} />
<Typography variant="caption">{hasFile ? t('Update photo') : t('Update photo')}</Typography>
</Box>
);
const renderContent = () => (
<Box
sx={{
width: 1,
height: 1,
overflow: 'hidden',
borderRadius: '50%',
position: 'relative',
}}
>
{renderPreview()}
{renderPlaceholder()}
</Box>
);
return (
<>
<Box
{...getRootProps()}
className={mergeClasses([uploadClasses.uploadBox, className])}
sx={[
(theme) => ({
p: 1,
m: 'auto',
width: 144,
height: 144,
cursor: 'pointer',
overflow: 'hidden',
borderRadius: '50%',
border: `1px dashed ${varAlpha(theme.vars.palette.grey['500Channel'], 0.2)}`,
...(isDragActive && { opacity: 0.72 }),
...(disabled && { opacity: 0.48, pointerEvents: 'none' }),
...(hasError && { borderColor: 'error.main' }),
...(hasFile && {
...(hasError && { bgcolor: varAlpha(theme.vars.palette.error.mainChannel, 0.08) }),
'&:hover .upload-placeholder': { opacity: 1 },
}),
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
>
<input {...getInputProps()} />
{renderContent()}
</Box>
{helperText && helperText}
{!!fileRejections.length && <RejectionFiles files={fileRejections} />}
</>
);
}