fix file dragndrop

This commit is contained in:
nflnkr 2024-02-16 14:19:14 +03:00
parent 2203ad6d23
commit f6c8b3b13b
3 changed files with 284 additions and 364 deletions

@ -13,138 +13,46 @@ import CloseBold from "@icons/CloseBold";
import UploadIcon from "@icons/UploadIcon"; import UploadIcon from "@icons/UploadIcon";
import { sendAnswer, sendFile } from "@api/quizRelase"; import { sendAnswer, sendFile } from "@api/quizRelase";
import { useQuizData } from "@contexts/QuizDataContext";
import Info from "@icons/Info"; import Info from "@icons/Info";
import type { UploadFileType } from "@model/questionTypes/file";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import type { DragEvent } from "react"; import { useState } from "react";
import { useState, type ChangeEvent } from "react";
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext"; import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
import type { QuizQuestionFile } from "../../../model/questionTypes/file"; import type { QuizQuestionFile } from "../../../model/questionTypes/file";
import { useQuizData } from "@contexts/QuizDataContext"; import { ACCEPT_SEND_FILE_TYPES_MAP, MAX_FILE_SIZE, UPLOAD_FILE_DESCRIPTIONS_MAP } from "../tools/fileUpload";
type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null;
type FileProps = { type FileProps = {
currentQuestion: QuizQuestionFile; currentQuestion: QuizQuestionFile;
}; };
const CurrentModal = ({ status }: { status: "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | ""; }) => {
switch (status) {
case 'errorType':
return (<>
<Typography>Выбран некорректный тип файла</Typography>
</>);
case 'errorSize':
return (<>
<Typography>Файл слишком большой. Максимальный размер 50 МБ</Typography>
</>);
default:
return (<>
<Typography>Допустимые расширения файлов:</Typography>
<Typography>{
//@ts-ignore
ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}</Typography>
</>);
}
};
const ACCEPT_SEND_FILE_TYPES_MAP = {
picture: [
".jpeg",
".jpg",
".png",
".ico",
".gif",
".tiff",
".webp",
".eps",
".svg"
],
video: [
".mp4",
".mov",
".wmv",
".avi",
".avchd",
".flv",
".f4v",
".swf",
".mkv",
".webm",
".mpeg-2"
],
audio: [
".aac",
".aiff",
".dsd",
".flac",
".mp3",
".mqa",
".ogg",
".wav",
".wma"
],
document: [
".doc",
".docx",
".dotx",
".rtf",
".odt",
".pdf",
".txt",
".xls",
".ppt",
".xlsx",
".pptx",
".pages",
],
};
const UPLOAD_FILE_DESCRIPTIONS_MAP: Record<
UploadFileType,
{ title: string; description: string; }
> = {
picture: {
title: "Добавить изображение",
description: "Принимает изображения",
},
video: {
title: "Добавить видео",
description: "Принимает .mp4 и .mov формат — максимум 50мб",
},
audio: { title: "Добавить аудиофайл", description: "Принимает аудиофайлы" },
document: { title: "Добавить документ", description: "Принимает документы" },
} as const;
export const File = ({ currentQuestion }: FileProps) => { export const File = ({ currentQuestion }: FileProps) => {
const theme = useTheme(); const theme = useTheme();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const { quizId } = useQuizData(); const { quizId } = useQuizData();
const [statusModal, setStatusModal] = useState<"errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | "">(""); const [modalWarningType, setModalWarningType] = useState<ModalWarningType>(null);
const [readySend, setReadySend] = useState(true) const [isSending, setIsSending] = useState<boolean>(false);
const [isDropzoneHighlighted, setIsDropzoneHighlighted] = useState<boolean>(false);
const isMobile = useRootContainerSize() < 500;
const answer = answers.find( const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id ({ questionId }) => questionId === currentQuestion.id
)?.answer as string; )?.answer as string;
const isMobile = useRootContainerSize() < 500;
const uploadFile = async ({ target }: ChangeEvent<HTMLInputElement>) => {
if (readySend) {
setReadySend(false)
const file = target.files?.[0];
if (file) {
if (file.size <= 52428800) {
//проверяем на соответствие
console.log(file.name.toLowerCase());
if (ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].find((ednding => {
console.log(ednding);
console.log(file.name.toLowerCase().endsWith(ednding));
return file.name.toLowerCase().endsWith(ednding);
}))) {
//Нужный формат const uploadFile = async (file: File | undefined) => {
console.log(file); if (isSending) return;
if (!file) return;
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
const isFileTypeAccepted = ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].some(
fileType => file.name.toLowerCase().endsWith(fileType)
);
if (!isFileTypeAccepted) return setModalWarningType("errorType");
setIsSending(true);
try { try {
const data = await sendFile({ const data = await sendFile({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: { body: {
@ -161,36 +69,31 @@ export const File = ({ currentQuestion }: FileProps) => {
qid: quizId, qid: quizId,
}); });
updateAnswer( updateAnswer(currentQuestion.id, `${file.name}|${URL.createObjectURL(file)}`, 0);
currentQuestion.id,
`${file.name}|${URL.createObjectURL(file)}`,
0
);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
enqueueSnackbar("ответ не был засчитан"); enqueueSnackbar("ответ не был засчитан");
} }
} else { setIsSending(false);
};
//неподходящий формат const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
setStatusModal("errorType"); event.preventDefault();
} setIsDropzoneHighlighted(false);
} else {
setStatusModal("errorSize"); const file = event.dataTransfer.files[0];
}
} uploadFile(file);
setReadySend(true)
}
}; };
return ( return (
<>
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}>{currentQuestion.title}</Typography> <Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>{currentQuestion.title}</Typography>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -200,7 +103,7 @@ export const File = ({ currentQuestion }: FileProps) => {
maxWidth: answer?.split("|")[0] ? "640px" : "600px", maxWidth: answer?.split("|")[0] ? "640px" : "600px",
}} }}
> >
{answer?.split("|")[0] && ( {answer?.split("|")[0] ? (
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}> <Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
<Typography color={theme.palette.text.primary}>Вы загрузили:</Typography> <Typography color={theme.palette.text.primary}>Вы загрузили:</Typography>
<Box <Box
@ -227,44 +130,55 @@ export const File = ({ currentQuestion }: FileProps) => {
sx={{ p: 0 }} sx={{ p: 0 }}
onClick={async () => { onClick={async () => {
if (answer.length > 0) { if (answer.length > 0) {
setReadySend(false) setIsSending(true);
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: "", body: "",
qid: quizId, qid: quizId,
}) });
} }
console.log(answer) console.log(answer);
updateAnswer(currentQuestion.id, "", 0); updateAnswer(currentQuestion.id, "", 0);
setReadySend(true) setIsSending(false);
}} }}
> >
<CloseBold /> <CloseBold />
</IconButton> </IconButton>
</Box> </Box>
</Box> </Box>
)} ) : (
<Box
{!answer?.split("|")[0] && ( sx={{
<Box sx={{
display: "flex", display: "flex",
alignItems: "center" alignItems: "center"
}}> }}
{ >
{isSending ?
readySend ? <> <Skeleton
<ButtonBase component="label" sx={{ justifyContent: "flex-start", width: "100%" }}> variant="rounded"
sx={{
width: "100%",
height: "120px",
maxWidth: "560px",
}}
/>
:
<ButtonBase
component="label"
sx={{ justifyContent: "flex-start", width: "100%" }}
>
<input <input
onChange={uploadFile} onChange={e => uploadFile(e.target.files?.[0])}
hidden hidden
accept={ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].join(",")} accept={ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].join(",")}
multiple multiple
type="file" type="file"
/> />
<Box <Box
onDragOver={(event: DragEvent<HTMLDivElement>) => onDragEnter={() => !answer?.split("|")[0] && setIsDropzoneHighlighted(true)}
event.preventDefault() onDragLeave={() => setIsDropzoneHighlighted(false)}
} onDragOver={(e) => e.preventDefault()}
onDrop={onDrop}
sx={{ sx={{
width: "100%", width: "100%",
height: isMobile ? undefined : "120px", height: isMobile ? undefined : "120px",
@ -274,8 +188,7 @@ export const File = ({ currentQuestion }: FileProps) => {
alignItems: "center", alignItems: "center",
padding: "33px 44px 33px 55px", padding: "33px 44px 33px 55px",
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
border: `1px solid #9A9AAF`, border: `1px solid ${isDropzoneHighlighted ? "red" : "#9A9AAF"}`,
// border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px", borderRadius: "8px",
}} }}
> >
@ -288,10 +201,7 @@ export const File = ({ currentQuestion }: FileProps) => {
fontWeight: 500, fontWeight: 500,
}} }}
> >
{ {UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type].title}
UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type]
.title
}
</Typography> </Typography>
<Typography <Typography
sx={{ sx={{
@ -301,27 +211,17 @@ export const File = ({ currentQuestion }: FileProps) => {
lineHeight: "19px", lineHeight: "19px",
}} }}
> >
{ {UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type].description}
UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type]
.description
}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
</ButtonBase> </ButtonBase>
</> :
<Skeleton
variant="rounded"
sx={{
width: "100%",
height: "120px",
maxWidth: "560px",
}}
/>
} }
<Info sx={{ width: "40px", height: "40px" }} color={theme.palette.primary.main} onClick={() => setStatusModal(currentQuestion.content.type)} /> <Info
sx={{ width: "40px", height: "40px" }}
color={theme.palette.primary.main}
onClick={() => setModalWarningType(currentQuestion.content.type)}
/>
</Box> </Box>
)} )}
{answer && currentQuestion.content.type === "picture" && ( {answer && currentQuestion.content.type === "picture" && (
@ -347,10 +247,9 @@ export const File = ({ currentQuestion }: FileProps) => {
/> />
)} )}
</Box> </Box>
</Box>
<Modal <Modal
open={Boolean(statusModal)} open={modalWarningType !== null}
onClose={() => setStatusModal("")} onClose={() => setModalWarningType(null)}
> >
<Box sx={{ <Box sx={{
position: 'absolute', position: 'absolute',
@ -363,10 +262,25 @@ export const File = ({ currentQuestion }: FileProps) => {
boxShadow: 24, boxShadow: 24,
p: 4, p: 4,
}}> }}>
<CurrentModal status={statusModal} /> <CurrentModal status={modalWarningType} />
</Box> </Box>
</Modal> </Modal>
</Box>
);
};
const CurrentModal = ({ status }: { status: ModalWarningType; }) => {
switch (status) {
case null: return null;
case 'errorType': return <Typography>Выбран некорректный тип файла</Typography>;
case 'errorSize': return <Typography>Файл слишком большой. Максимальный размер 50 МБ</Typography>;
default: return (
<>
<Typography>Допустимые расширения файлов:</Typography>
<Typography>{
ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}</Typography>
</> </>
); );
}
}; };

@ -1,63 +0,0 @@
import { ChangeEvent, useEffect, useRef, useState } from "react";
import { Box, Button, Typography } from "@mui/material";
import type {
QuizQuestionFile,
UploadFileType,
} from "@model/questionTypes/file";
const UPLOAD_FILE_TYPES_MAP: Record<UploadFileType, string> = {
picture: "image/*",
video: "video/*",
audio: "audio/*",
document:
".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,.pdf",
} as const;
interface Props {
question: QuizQuestionFile;
}
export default function File({ question }: Props) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | null>(null);
const [acceptedType, setAcceptedType] = useState<any>(
UPLOAD_FILE_TYPES_MAP.picture
);
useEffect(() => {
setAcceptedType(UPLOAD_FILE_TYPES_MAP[question.content.type]);
}, [question.content.type]);
function handleFileChange(event: ChangeEvent<HTMLInputElement>) {
if (!event.target.files?.[0]) return setFile(null);
setFile(event.target.files[0]);
}
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: 1,
}}
>
<Typography variant="h6" data-cy="question-title">{question.title}</Typography>
<Button variant="contained" onClick={() => fileInputRef.current?.click()}>
Загрузить файл
<input
ref={fileInputRef}
onChange={handleFileChange}
type="file"
accept={acceptedType}
data-cy="file-upload-input"
style={{
display: "none",
}}
/>
</Button>
{file && <Typography data-cy="chosen-file-name">Выбран файл: {file.name}</Typography>}
</Box>
);
}

@ -0,0 +1,69 @@
import { UploadFileType } from "@model/questionTypes/file";
export const MAX_FILE_SIZE = 5 * 2 ** 20;
export const UPLOAD_FILE_DESCRIPTIONS_MAP = {
picture: {
title: "Добавить изображение",
description: "Принимает изображения",
},
video: {
title: "Добавить видео",
description: "Принимает .mp4 и .mov формат — максимум 50мб",
},
audio: { title: "Добавить аудиофайл", description: "Принимает аудиофайлы" },
document: { title: "Добавить документ", description: "Принимает документы" },
} as const satisfies Record<UploadFileType, { title: string; description: string; }>;
export const ACCEPT_SEND_FILE_TYPES_MAP = {
picture: [
".jpeg",
".jpg",
".png",
".ico",
".gif",
".tiff",
".webp",
".eps",
".svg"
],
video: [
".mp4",
".mov",
".wmv",
".avi",
".avchd",
".flv",
".f4v",
".swf",
".mkv",
".webm",
".mpeg-2"
],
audio: [
".aac",
".aiff",
".dsd",
".flac",
".mp3",
".mqa",
".ogg",
".wav",
".wma"
],
document: [
".doc",
".docx",
".dotx",
".rtf",
".odt",
".pdf",
".txt",
".xls",
".ppt",
".xlsx",
".pptx",
".pages",
],
} as const;