250 lines
6.6 KiB
React
250 lines
6.6 KiB
React
// Components
|
|
import { Box, Stack, Typography } from "@mui/material";
|
|
import Icon from "../../Icon";
|
|
import Image from "../../Image/index.jsx";
|
|
import TextField from "@mui/material/TextField";
|
|
import IconButton from "@mui/material/IconButton";
|
|
import ProgressUpload from "../../ProgressBars/index.jsx";
|
|
|
|
// Utils
|
|
import PropTypes from "prop-types";
|
|
import { useCallback, useState, useRef, useEffect } from "react";
|
|
import { useTheme } from "@emotion/react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
/**
|
|
* ImageUpload component allows users to upload images with drag-and-drop functionality.
|
|
* It supports file size and format validation.
|
|
*
|
|
* @component
|
|
* @param {Object} props - Component props
|
|
* @param {boolean} [props.previewIsRound=false] - Determines if the image preview should be round
|
|
* @param {string} [props.src] - Source URL of the image to display
|
|
* @param {function} props.onChange - Callback function to handle file change, takes a file as an argument
|
|
* @param {number} [props.maxSize=3145728] - Maximum file size allowed in bytes (default is 3MB)
|
|
* @param {Array<string>} [props.accept=['jpg', 'jpeg', 'png']] - Array of accepted file formats
|
|
* @param {Object} [props.errors] - Object containing error messages
|
|
* @returns {JSX.Element} The rendered component
|
|
*/
|
|
const ImageUpload = ({
|
|
previewIsRound = false,
|
|
src,
|
|
onChange,
|
|
maxSize = 3 * 1024 * 1024,
|
|
accept = ["jpg", "jpeg", "png"],
|
|
error,
|
|
}) => {
|
|
const theme = useTheme();
|
|
const { t } = useTranslation();
|
|
const [uploadComplete, setUploadComplete] = useState(false);
|
|
const [completedFile, setCompletedFile] = useState(null);
|
|
const [file, setFile] = useState(null);
|
|
const [progress, setProgress] = useState({ value: 0, isLoading: false });
|
|
const intervalRef = useRef(null);
|
|
const [localError, setLocalError] = useState(null);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
|
const roundStyle = previewIsRound ? { borderRadius: "50%" } : {};
|
|
|
|
const handleImageChange = useCallback(
|
|
(file) => {
|
|
if (!file) return;
|
|
|
|
const isValidType = accept.some((type) => file.type.includes(type));
|
|
const isValidSize = file.size <= maxSize;
|
|
|
|
if (!isValidType) {
|
|
setLocalError(t("invalidFileFormat"));
|
|
return;
|
|
}
|
|
if (!isValidSize) {
|
|
setLocalError(t("invalidFileSize"));
|
|
return;
|
|
}
|
|
|
|
setLocalError(null);
|
|
|
|
const previewFile = {
|
|
src: URL.createObjectURL(file),
|
|
name: file.name,
|
|
file,
|
|
};
|
|
|
|
setFile(previewFile);
|
|
setProgress({ value: 0, isLoading: true });
|
|
|
|
intervalRef.current = setInterval(() => {
|
|
setProgress((prev) => {
|
|
const buffer = 12;
|
|
if (prev.value + buffer >= 100) {
|
|
clearInterval(intervalRef.current);
|
|
setUploadComplete(true);
|
|
setCompletedFile(previewFile);
|
|
return { value: 100, isLoading: false };
|
|
}
|
|
return { value: prev.value + buffer, isLoading: true };
|
|
});
|
|
}, 120);
|
|
},
|
|
[maxSize, accept]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (uploadComplete && completedFile) {
|
|
onChange?.(completedFile);
|
|
setUploadComplete(false);
|
|
setCompletedFile(null);
|
|
}
|
|
}, [uploadComplete, completedFile, onChange]);
|
|
|
|
return (
|
|
<>
|
|
{src ? (
|
|
<Stack
|
|
direction="row"
|
|
justifyContent="center"
|
|
>
|
|
<Image
|
|
alt="Uploaded preview"
|
|
src={src}
|
|
width="250px"
|
|
height="250px"
|
|
sx={{ ...roundStyle }}
|
|
/>
|
|
</Stack>
|
|
) : (
|
|
<>
|
|
<Box
|
|
className="image-field-wrapper"
|
|
mt={theme.spacing(8)}
|
|
onDragEnter={() => setIsDragging(true)}
|
|
onDragLeave={() => setIsDragging(false)}
|
|
onDrop={() => setIsDragging(false)}
|
|
sx={{
|
|
position: "relative",
|
|
height: "fit-content",
|
|
border: "dashed",
|
|
borderRadius: theme.shape.borderRadius,
|
|
borderColor: isDragging
|
|
? theme.palette.primary.main
|
|
: theme.palette.primary.lowContrast,
|
|
backgroundColor: isDragging ? "hsl(215, 87%, 51%, 0.05)" : "transparent",
|
|
borderWidth: "2px",
|
|
transition: "0.2s",
|
|
"&:hover": {
|
|
borderColor: theme.palette.primary.main,
|
|
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
|
|
},
|
|
}}
|
|
>
|
|
<TextField
|
|
type="file"
|
|
onChange={(e) => handleImageChange(e?.target?.files?.[0])}
|
|
sx={{
|
|
width: "100%",
|
|
"& .MuiInputBase-input[type='file']": {
|
|
opacity: 0,
|
|
cursor: "pointer",
|
|
maxWidth: "500px",
|
|
minHeight: "175px",
|
|
zIndex: 1,
|
|
},
|
|
"& fieldset": {
|
|
padding: 0,
|
|
border: "none",
|
|
},
|
|
}}
|
|
/>
|
|
<Stack
|
|
alignItems="center"
|
|
gap="4px"
|
|
sx={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: "50%",
|
|
transform: "translate(-50%, -50%)",
|
|
zIndex: 0,
|
|
width: "100%",
|
|
}}
|
|
>
|
|
<IconButton
|
|
sx={{
|
|
pointerEvents: "none",
|
|
borderRadius: theme.shape.borderRadius,
|
|
border: `solid ${theme.shape.borderThick}px ${theme.palette.primary.lowContrast}`,
|
|
boxShadow: theme.shape.boxShadow,
|
|
}}
|
|
>
|
|
<Icon
|
|
name="Upload"
|
|
size={24}
|
|
/>
|
|
</IconButton>
|
|
<Typography
|
|
component="h2"
|
|
color={theme.palette.primary.contrastTextTertiary}
|
|
>
|
|
<Typography
|
|
component="span"
|
|
fontSize="inherit"
|
|
color="info"
|
|
fontWeight={500}
|
|
>
|
|
{t("ClickUpload")}
|
|
</Typography>{" "}
|
|
or {t("DragandDrop")}
|
|
</Typography>
|
|
<Typography
|
|
component="p"
|
|
color={theme.palette.primary.contrastTextTertiary}
|
|
sx={{ opacity: 0.6 }}
|
|
>
|
|
({t("MaxSize")}: {Math.round(maxSize / 1024 / 1024)}MB)
|
|
</Typography>
|
|
</Stack>
|
|
</Box>
|
|
{(localError || progress.isLoading || progress.value !== 0) && (
|
|
<ProgressUpload
|
|
icon={
|
|
<Icon
|
|
name="Image"
|
|
size={20}
|
|
/>
|
|
}
|
|
label={file?.name || "Upload failed"}
|
|
size={file?.size}
|
|
progress={progress.value}
|
|
onClick={() => {
|
|
clearInterval(intervalRef.current);
|
|
setFile(null);
|
|
setProgress({ value: 0, isLoading: false });
|
|
setLocalError(null);
|
|
onChange(undefined);
|
|
}}
|
|
error={localError || error}
|
|
/>
|
|
)}
|
|
<Typography
|
|
component="p"
|
|
color={theme.palette.primary.contrastTextTertiary}
|
|
sx={{ opacity: 0.6 }}
|
|
>
|
|
{t("SupportedFormats")}: {accept.join(", ").toUpperCase()}
|
|
</Typography>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
ImageUpload.propTypes = {
|
|
previewIsRound: PropTypes.bool,
|
|
src: PropTypes.string,
|
|
onChange: PropTypes.func,
|
|
maxSize: PropTypes.number,
|
|
accept: PropTypes.array,
|
|
error: PropTypes.string,
|
|
};
|
|
|
|
export default ImageUpload;
|