корректный выбор и отправка файла для Images

This commit is contained in:
Nastya 2025-06-20 19:24:07 +03:00
parent 4096eb845c
commit 9f32bd9c9d
4 changed files with 142 additions and 67 deletions

@ -9,6 +9,7 @@ import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { useQuizStore } from "@/stores/useQuizStore";
import { useTranslation } from "react-i18next";
import { OwnImage } from "./OwnImage";
import { useSnackbar } from "notistack";
type ImagesProps = {
questionId: string;
@ -99,6 +100,7 @@ export const ImageVariant = ({
const { t } = useTranslation();
const isMobile = useRootContainerSize() < 450;
const isTablet = useRootContainerSize() < 850;
const { enqueueSnackbar } = useSnackbar();
const canvasRef = useRef<HTMLCanvasElement | null>(null);
@ -171,7 +173,16 @@ export const ImageVariant = ({
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Box sx={{ width: "100%", height: "300px" }}>
{own ? (
<OwnImage imageUrl={choiceImgUrl} />
<OwnImage
imageUrl={choiceImgUrl}
questionId={questionId}
variantId={variant.id}
onValidationError={(errorType) => {
enqueueSnackbar(errorType === "size" ? t("file is too big") : t("file type is not supported"), {
variant: "warning",
});
}}
/>
) : (
variant.extendedText && (
<canvas

@ -1,58 +1,89 @@
import { Box, ButtonBase, IconButton, Typography, useTheme } from "@mui/material";
import { useState, useRef, useEffect } from "react";
import { useState, useRef } from "react";
import CloseIcon from "@mui/icons-material/Close";
import { useTranslation } from "react-i18next";
import { useQuizStore } from "@/stores/useQuizStore";
import { useQuizViewStore } from "@/stores/quizView";
import { useSnackbar } from "notistack";
import { Skeleton } from "@mui/material";
import UploadIcon from "@/assets/icons/UploadIcon";
import { sendFile } from "@/api/quizRelase";
import { ACCEPT_SEND_FILE_TYPES_MAP, MAX_FILE_SIZE } from "../../tools/fileUpload";
type OwnImageProps = {
imageUrl?: string;
questionId: string;
variantId: string;
onValidationError: (error: "size" | "type") => void;
};
export const OwnImage = ({ imageUrl }: OwnImageProps) => {
export const OwnImage = ({ imageUrl, questionId, variantId, onValidationError }: OwnImageProps) => {
const theme = useTheme();
const { t } = useTranslation();
const { quizId, preview } = useQuizStore();
const { answers, updateAnswer, ownVariants, updateOwnVariant } = useQuizViewStore((state) => state);
const { enqueueSnackbar } = useSnackbar();
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isDropzoneHighlighted, setIsDropzoneHighlighted] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// Sync state if external imageUrl changes
useEffect(() => {
if (imageUrl) {
setSelectedFile(null); // Clear local file selection when external URL is provided
const ownVariantData = ownVariants.find((v) => v.id === variantId);
const uploadImage = async (file: File) => {
if (isUploading) return;
if (!file) return;
if (file.size > MAX_FILE_SIZE) {
onValidationError("size");
return;
}
}, [imageUrl]);
const isFileTypeAccepted = ACCEPT_SEND_FILE_TYPES_MAP.picture.some((fileType) =>
file.name.toLowerCase().endsWith(fileType)
);
if (!isFileTypeAccepted) {
onValidationError("type");
return;
}
setIsUploading(true);
try {
const data = await sendFile({
questionId,
body: {
file: file,
name: file.name,
preview,
},
qid: quizId,
});
const fileId = data!.data.fileIDMap[questionId];
// Сохраняем fileId в originalImageUrl
updateOwnVariant(variantId, "", "", fileId);
// Для UI — локальный preview
const localImageUrl = URL.createObjectURL(file);
updateAnswer(questionId, `${file.name}|${localImageUrl}`, 0);
setSelectedFile(file);
} catch (error) {
console.error("Error uploading image:", error);
enqueueSnackbar(t("The answer was not counted"));
} finally {
setIsUploading(false);
}
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file && file.type.startsWith("image/")) {
setSelectedFile(file);
if (file) {
uploadImage(file);
}
};
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
setIsDropzoneHighlighted(false);
const file = event.dataTransfer.files?.[0];
if (file && file.type.startsWith("image/")) {
setSelectedFile(file);
}
};
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
};
const handleDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
setIsDropzoneHighlighted(true);
};
const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
setIsDropzoneHighlighted(false);
};
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (fileInputRef.current) {
@ -64,18 +95,28 @@ export const OwnImage = ({ imageUrl }: OwnImageProps) => {
const handleRemoveImage = (e: React.MouseEvent) => {
e.stopPropagation();
setSelectedFile(null);
updateAnswer(questionId, "", 0);
updateOwnVariant(variantId, "", "", "");
};
const imageToDisplay = selectedFile ? URL.createObjectURL(selectedFile) : imageUrl;
const imageToDisplay = selectedFile
? URL.createObjectURL(selectedFile)
: ownVariantData?.variant.originalImageUrl || imageUrl;
if (isUploading) {
return (
<Skeleton
variant="rounded"
sx={{ width: "100%", height: "100%", borderRadius: "12px" }}
/>
);
}
return (
<ButtonBase
component="div"
onClick={handleClick}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
disabled={isUploading}
sx={{
width: "100%",
height: "100%",
@ -86,18 +127,17 @@ export const OwnImage = ({ imageUrl }: OwnImageProps) => {
transition: "border-color 0.3s, background-color 0.3s",
overflow: "hidden",
position: "relative",
"&:hover .overlay": {
opacity: 1,
},
opacity: isUploading ? 0.7 : 1,
}}
>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept="image/*"
accept={ACCEPT_SEND_FILE_TYPES_MAP.picture.join(",")}
hidden
/>
{imageToDisplay ? (
<>
<Box sx={{ width: "100%", height: "100%", position: "relative" }}>
@ -107,21 +147,7 @@ export const OwnImage = ({ imageUrl }: OwnImageProps) => {
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</Box>
<Box
className="overlay"
sx={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundColor: "rgba(0, 0, 0, 0.4)",
opacity: 0,
transition: "opacity 0.3s",
pointerEvents: "none",
}}
/>
{selectedFile && (
{(selectedFile || ownVariantData?.variant.originalImageUrl) && (
<IconButton
onClick={handleRemoveImage}
sx={{
@ -131,6 +157,8 @@ export const OwnImage = ({ imageUrl }: OwnImageProps) => {
zIndex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
color: "white",
height: "25px",
width: "25px",
"&:hover": {
backgroundColor: "rgba(0, 0, 0, 0.7)",
},
@ -150,6 +178,7 @@ export const OwnImage = ({ imageUrl }: OwnImageProps) => {
opacity: 0.5,
}}
>
<UploadIcon />
<Typography
variant="body2"
color="text.secondary"

@ -30,7 +30,7 @@ interface QuizViewStore {
interface QuizViewActions {
updateAnswer: (questionId: string, answer: string | string[] | Moment, points: number) => void;
deleteAnswer: (questionId: string) => void;
updateOwnVariant: (id: string, answer: string, extendedText?: string) => void;
updateOwnVariant: (id: string, answer: string, extendedText?: string, originalImageUrl?: string) => void;
deleteOwnVariant: (id: string) => void;
setCurrentQuizStep: (step: QuizStep) => void;
}
@ -90,7 +90,7 @@ export const createQuizViewStore = () =>
}
);
},
updateOwnVariant(id, answer, extendedText) {
updateOwnVariant(id, answer, extendedText, originalImageUrl) {
set(
(state) => {
const index = state.ownVariants.findIndex((variant) => variant.id === id);
@ -103,7 +103,7 @@ export const createQuizViewStore = () =>
answer,
extendedText: extendedText || "",
hints: "",
originalImageUrl: "",
originalImageUrl: originalImageUrl || "",
},
});
} else {
@ -111,6 +111,9 @@ export const createQuizViewStore = () =>
if (extendedText) {
state.ownVariants[index].variant.extendedText = extendedText;
}
if (originalImageUrl) {
state.ownVariants[index].variant.originalImageUrl = originalImageUrl;
}
}
},
false,

@ -116,8 +116,25 @@ export async function sendQuestionAnswer(
let answerString = ``;
selectedVariants.forEach((e) => {
if (!e.isOwn || (e.isOwn && question.content.own)) {
let imageValue = e.extendedText;
if (e.isOwn) {
// Берем fileId из ownVariants для own вариантов
const ownVariantData = ownVariants.find((v) => v.id === e.id)?.variant;
if (ownVariantData?.originalImageUrl) {
// Конструируем полный URL для own вариантов
const baseUrl =
"https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/55c25eb9-4533-4d51-9da5-54e63e8aeace/";
// Убираем расширение файла из fileId
const fileIdWithoutExtension = ownVariantData.originalImageUrl.replace(
/\.(jpg|jpeg|png|gif|webp)$/i,
""
);
imageValue = baseUrl + fileIdWithoutExtension;
}
}
const body = {
Image: e.extendedText,
Image: imageValue,
Description: e.isOwn ? ownAnswer : e.answer,
};
answerString += `\`${JSON.stringify(body)}\`,`;
@ -135,8 +152,23 @@ export async function sendQuestionAnswer(
const variant = question.content.variants.find((v) => v.id === questionAnswer.answer);
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
let imageValue = variant.extendedText;
if (variant.isOwn) {
// Берем fileId из ownVariants для own вариантов
const ownVariantData = ownVariants.find((v) => v.id === variant.id)?.variant;
if (ownVariantData?.originalImageUrl) {
// Конструируем полный URL для own вариантов
const baseUrl =
"https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/55c25eb9-4533-4d51-9da5-54e63e8aeace/";
// Убираем расширение файла из fileId
const fileIdWithoutExtension = ownVariantData.originalImageUrl.replace(/\.(jpg|jpeg|png|gif|webp)$/i, "");
imageValue = baseUrl + fileIdWithoutExtension;
}
}
const body = {
Image: variant.extendedText,
Image: imageValue,
Description: variant.answer,
};
if (!body) throw new Error(`Body of answer in question ${question.id} is undefined`);