корректный выбор и отправка файла для Images
This commit is contained in:
parent
4096eb845c
commit
9f32bd9c9d
@ -9,6 +9,7 @@ import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
|||||||
import { useQuizStore } from "@/stores/useQuizStore";
|
import { useQuizStore } from "@/stores/useQuizStore";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { OwnImage } from "./OwnImage";
|
import { OwnImage } from "./OwnImage";
|
||||||
|
import { useSnackbar } from "notistack";
|
||||||
|
|
||||||
type ImagesProps = {
|
type ImagesProps = {
|
||||||
questionId: string;
|
questionId: string;
|
||||||
@ -99,6 +100,7 @@ export const ImageVariant = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isMobile = useRootContainerSize() < 450;
|
const isMobile = useRootContainerSize() < 450;
|
||||||
const isTablet = useRootContainerSize() < 850;
|
const isTablet = useRootContainerSize() < 850;
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
|
||||||
@ -171,7 +173,16 @@ export const ImageVariant = ({
|
|||||||
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||||
<Box sx={{ width: "100%", height: "300px" }}>
|
<Box sx={{ width: "100%", height: "300px" }}>
|
||||||
{own ? (
|
{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 && (
|
variant.extendedText && (
|
||||||
<canvas
|
<canvas
|
||||||
|
@ -1,58 +1,89 @@
|
|||||||
import { Box, ButtonBase, IconButton, Typography, useTheme } from "@mui/material";
|
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 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 = {
|
type OwnImageProps = {
|
||||||
imageUrl?: string;
|
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 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 [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
const [isDropzoneHighlighted, setIsDropzoneHighlighted] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Sync state if external imageUrl changes
|
const ownVariantData = ownVariants.find((v) => v.id === variantId);
|
||||||
useEffect(() => {
|
|
||||||
if (imageUrl) {
|
const uploadImage = async (file: File) => {
|
||||||
setSelectedFile(null); // Clear local file selection when external URL is provided
|
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 handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file && file.type.startsWith("image/")) {
|
if (file) {
|
||||||
setSelectedFile(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) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
@ -64,18 +95,28 @@ export const OwnImage = ({ imageUrl }: OwnImageProps) => {
|
|||||||
const handleRemoveImage = (e: React.MouseEvent) => {
|
const handleRemoveImage = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setSelectedFile(null);
|
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 (
|
return (
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
component="div"
|
component="div"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDrop={handleDrop}
|
disabled={isUploading}
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragEnter={handleDragEnter}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
@ -86,18 +127,17 @@ export const OwnImage = ({ imageUrl }: OwnImageProps) => {
|
|||||||
transition: "border-color 0.3s, background-color 0.3s",
|
transition: "border-color 0.3s, background-color 0.3s",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
"&:hover .overlay": {
|
opacity: isUploading ? 0.7 : 1,
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
accept="image/*"
|
accept={ACCEPT_SEND_FILE_TYPES_MAP.picture.join(",")}
|
||||||
hidden
|
hidden
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{imageToDisplay ? (
|
{imageToDisplay ? (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ width: "100%", height: "100%", position: "relative" }}>
|
<Box sx={{ width: "100%", height: "100%", position: "relative" }}>
|
||||||
@ -107,21 +147,7 @@ export const OwnImage = ({ imageUrl }: OwnImageProps) => {
|
|||||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
{(selectedFile || ownVariantData?.variant.originalImageUrl) && (
|
||||||
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 && (
|
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleRemoveImage}
|
onClick={handleRemoveImage}
|
||||||
sx={{
|
sx={{
|
||||||
@ -131,6 +157,8 @@ export const OwnImage = ({ imageUrl }: OwnImageProps) => {
|
|||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
color: "white",
|
color: "white",
|
||||||
|
height: "25px",
|
||||||
|
width: "25px",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||||
},
|
},
|
||||||
@ -150,6 +178,7 @@ export const OwnImage = ({ imageUrl }: OwnImageProps) => {
|
|||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<UploadIcon />
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
|
@ -30,7 +30,7 @@ interface QuizViewStore {
|
|||||||
interface QuizViewActions {
|
interface QuizViewActions {
|
||||||
updateAnswer: (questionId: string, answer: string | string[] | Moment, points: number) => void;
|
updateAnswer: (questionId: string, answer: string | string[] | Moment, points: number) => void;
|
||||||
deleteAnswer: (questionId: string) => 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;
|
deleteOwnVariant: (id: string) => void;
|
||||||
setCurrentQuizStep: (step: QuizStep) => void;
|
setCurrentQuizStep: (step: QuizStep) => void;
|
||||||
}
|
}
|
||||||
@ -90,7 +90,7 @@ export const createQuizViewStore = () =>
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
updateOwnVariant(id, answer, extendedText) {
|
updateOwnVariant(id, answer, extendedText, originalImageUrl) {
|
||||||
set(
|
set(
|
||||||
(state) => {
|
(state) => {
|
||||||
const index = state.ownVariants.findIndex((variant) => variant.id === id);
|
const index = state.ownVariants.findIndex((variant) => variant.id === id);
|
||||||
@ -103,7 +103,7 @@ export const createQuizViewStore = () =>
|
|||||||
answer,
|
answer,
|
||||||
extendedText: extendedText || "",
|
extendedText: extendedText || "",
|
||||||
hints: "",
|
hints: "",
|
||||||
originalImageUrl: "",
|
originalImageUrl: originalImageUrl || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -111,6 +111,9 @@ export const createQuizViewStore = () =>
|
|||||||
if (extendedText) {
|
if (extendedText) {
|
||||||
state.ownVariants[index].variant.extendedText = extendedText;
|
state.ownVariants[index].variant.extendedText = extendedText;
|
||||||
}
|
}
|
||||||
|
if (originalImageUrl) {
|
||||||
|
state.ownVariants[index].variant.originalImageUrl = originalImageUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
|
@ -116,8 +116,25 @@ export async function sendQuestionAnswer(
|
|||||||
let answerString = ``;
|
let answerString = ``;
|
||||||
selectedVariants.forEach((e) => {
|
selectedVariants.forEach((e) => {
|
||||||
if (!e.isOwn || (e.isOwn && question.content.own)) {
|
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 = {
|
const body = {
|
||||||
Image: e.extendedText,
|
Image: imageValue,
|
||||||
Description: e.isOwn ? ownAnswer : e.answer,
|
Description: e.isOwn ? ownAnswer : e.answer,
|
||||||
};
|
};
|
||||||
answerString += `\`${JSON.stringify(body)}\`,`;
|
answerString += `\`${JSON.stringify(body)}\`,`;
|
||||||
@ -135,8 +152,23 @@ export async function sendQuestionAnswer(
|
|||||||
const variant = question.content.variants.find((v) => v.id === questionAnswer.answer);
|
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}`);
|
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 = {
|
const body = {
|
||||||
Image: variant.extendedText,
|
Image: imageValue,
|
||||||
Description: variant.answer,
|
Description: variant.answer,
|
||||||
};
|
};
|
||||||
if (!body) throw new Error(`Body of answer in question ${question.id} is undefined`);
|
if (!body) throw new Error(`Body of answer in question ${question.id} is undefined`);
|
||||||
|
Loading…
Reference in New Issue
Block a user