возможность выбора своей картинки

This commit is contained in:
Nastya 2025-06-20 22:14:39 +03:00
parent 311cdedce6
commit b8b30a352b
5 changed files with 76 additions and 229 deletions

@ -1,197 +1,33 @@
import { Box, ButtonBase, IconButton, Typography, useTheme } from "@mui/material";
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";
import React, { forwardRef } from "react";
import { useQuizViewStore } from "@stores/quizView";
// Пропсы компонента
export type OwnVarimgImageProps = {
imageUrl?: string;
interface OwnVarimgImageProps {
questionId: string;
variantId: string;
onValidationError: (error: "size" | "type") => void;
};
}
export const OwnVarimgImage = ({ imageUrl, questionId, variantId, onValidationError }: OwnVarimgImageProps) => {
const theme = useTheme();
const { t } = useTranslation();
const { quizId, preview } = useQuizStore();
const { ownVariants, updateOwnVariant, updateAnswer } = useQuizViewStore((state) => state);
const { enqueueSnackbar } = useSnackbar();
export const OwnVarimgImage = forwardRef<HTMLInputElement, OwnVarimgImageProps>(({ questionId, variantId }, ref) => {
const { updateAnswer, updateOwnVariantWithFile } = useQuizViewStore((state) => state);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// Получаем ownVariant для этого варианта
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;
}
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, name: file.name, preview },
qid: quizId,
});
const fileId = data?.data.fileIDMap[questionId];
const localImageUrl = URL.createObjectURL(file);
// @ts-ignore
updateOwnVariant(variantId, "", "", fileId, localImageUrl, file);
updateAnswer(questionId, variantId, 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) {
uploadImage(file);
}
};
// Открытие диалога выбора файла
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (fileInputRef.current) fileInputRef.current.value = "";
fileInputRef.current?.click();
};
// Удаление изображения
const handleRemoveImage = (e: React.MouseEvent) => {
e.stopPropagation();
setSelectedFile(null);
updateOwnVariant(variantId, "", "", "", "");
};
// Определяем, что показывать
let imageToDisplay: string | null = null;
if (selectedFile) {
imageToDisplay = URL.createObjectURL(selectedFile);
} else if (ownVariantData?.variant.localImageUrl) {
// @ts-ignore
if (ownVariantData.variant.file) {
// @ts-ignore
imageToDisplay = URL.createObjectURL(ownVariantData.variant.file);
} else {
imageToDisplay = ownVariantData.variant.localImageUrl;
updateOwnVariantWithFile(variantId, file);
updateAnswer(questionId, variantId, 0);
event.target.value = "";
}
} else if (imageUrl) {
imageToDisplay = imageUrl;
}
if (isUploading) {
return (
<Skeleton
variant="rounded"
sx={{ width: "100%", height: "100%", borderRadius: "12px" }}
/>
);
}
};
return (
<ButtonBase
component="div"
onClick={handleClick}
disabled={isUploading}
sx={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "12px",
transition: "border-color 0.3s, background-color 0.3s",
overflow: "hidden",
position: "relative",
opacity: isUploading ? 0.7 : 1,
"&:hover": {
backgroundColor: "rgba(0, 0, 0, 0.1)",
},
}}
>
<input
type="file"
ref={fileInputRef}
id={`own-image-input-${variantId}`}
onChange={handleFileChange}
accept={ACCEPT_SEND_FILE_TYPES_MAP.picture.join(",")}
hidden
/>
{imageToDisplay ? (
<>
<Box sx={{ width: "100%", height: "100%", position: "relative" }}>
<img
src={imageToDisplay}
alt="Preview"
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</Box>
<IconButton
onClick={handleRemoveImage}
sx={{
position: "absolute",
top: 8,
right: 8,
zIndex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
color: "white",
height: "25px",
width: "25px",
"&:hover": {
backgroundColor: "rgba(0, 0, 0, 0.7)",
},
}}
>
<CloseIcon />
</IconButton>
</>
) : (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
opacity: 0.5,
}}
>
<UploadIcon />
<Typography
variant="body2"
color="text.secondary"
sx={{ p: 2, textAlign: "center" }}
>
добавьте свою картинку
</Typography>
</Box>
)}
</ButtonBase>
<input
type="file"
ref={ref}
style={{ display: "none" }}
accept="image/*"
onChange={handleFileChange}
/>
);
};
});
OwnVarimgImage.displayName = "OwnVarimgImage";

@ -1,23 +1,12 @@
import type { QuestionVariant, QuestionVariantWithEditedImages } from "@/model/questionTypes/shared";
import { useQuizStore } from "@/stores/useQuizStore";
import {
FormControlLabel,
TextareaAutosize,
Radio,
useTheme,
Box,
Input,
FormControl,
InputLabel,
Typography,
} from "@mui/material";
import { FormControlLabel, TextareaAutosize, Radio, useTheme, Box, Input, Typography } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { type MouseEvent } from "react";
import { useTranslation } from "react-i18next";
import { useDebouncedCallback } from "use-debounce";
type VarimgVariantProps = {
questionId: string;
@ -175,16 +164,12 @@ export const VarimgVariant = ({
value={index}
onClick={sendVariant}
label={
variant?.isOwn ? (
<OwnInput
questionId={questionId}
variant={variant}
largeCheck={questionLargeCheck}
ownPlaceholder={ownPlaceholder || "|"}
/>
) : (
variant.answer
)
<OwnInput
questionId={questionId}
variant={variant}
largeCheck={questionLargeCheck}
ownPlaceholder={ownPlaceholder || "|"}
/>
}
control={
<Radio
@ -239,18 +224,7 @@ export const VarimgVariant = ({
labelPlacement="start"
value={index}
onClick={sendVariant}
label={
variant?.isOwn ? (
<OwnInput
questionId={questionId}
variant={variant}
largeCheck={questionLargeCheck}
ownPlaceholder={ownPlaceholder || "|"}
/>
) : (
variant.answer
)
}
label={variant.answer}
control={
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}

@ -1,7 +1,8 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Box, ButtonBase, RadioGroup, Typography, useTheme } from "@mui/material";
import { VarimgVariant } from "./VarimgVariant";
import { OwnVarimgImage } from "./OwnVarimgImage";
import { useQuizViewStore } from "@stores/quizView";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
@ -35,9 +36,13 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
[currentQuestion.content.variants]
);
const ownVariantData = ownVariants.find((v) => v.id === answer);
const ownImageUrl = ownVariantData?.variant.file
? URL.createObjectURL(ownVariantData.variant.file)
: ownVariantData?.variant.localImageUrl;
const ownImageUrl = useMemo(() => {
return ownVariantData?.variant.file
? URL.createObjectURL(ownVariantData.variant.file)
: ownVariantData?.variant.localImageUrl;
}, [ownVariantData]);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!ownVariant) {
@ -67,15 +72,9 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
}
}, [variant]);
const handlePlaceholderClick = () => {
if (ownVariantInQuestion) {
document.getElementById(`own-image-input-${ownVariantInQuestion.id}`)?.click();
}
};
const handlePreviewAreaClick = () => {
if (ownVariantInQuestion) {
document.getElementById(`own-image-input-${ownVariantInQuestion.id}`)?.click();
inputRef.current?.click();
}
};
@ -140,6 +139,13 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
answer={answer}
/>
))}
{ownVariantInQuestion && (
<OwnVarimgImage
ref={inputRef}
questionId={currentQuestion.id}
variantId={ownVariantInQuestion.id}
/>
)}
</Box>
</RadioGroup>
<ButtonBase

@ -54,6 +54,7 @@ export type QuestionVariant = {
/** Локальный URL для предпросмотра */
localImageUrl?: string;
points?: number;
file?: File;
};
export interface QuestionVariantWithEditedImages extends QuestionVariant {
editedUrlImagesList?: EditedUrlImagesList | null;

@ -37,6 +37,7 @@ interface QuizViewActions {
originalImageUrl?: string,
localImageUrl?: string
) => void;
updateOwnVariantWithFile: (variantId: string, file: File) => void;
deleteOwnVariant: (id: string) => void;
setCurrentQuizStep: (step: QuizStep) => void;
}
@ -134,6 +135,35 @@ export const createQuizViewStore = () =>
}
);
},
updateOwnVariantWithFile(variantId, file) {
set(
(state) => {
const index = state.ownVariants.findIndex((v) => v.id === variantId);
if (index < 0) {
state.ownVariants.push({
id: variantId,
variant: {
id: variantId,
answer: "",
extendedText: "",
hints: "",
originalImageUrl: "",
file: file,
},
});
} else {
state.ownVariants[index].variant.file = file;
state.ownVariants[index].variant.localImageUrl = undefined;
}
},
false,
{
type: "updateOwnVariantWithFile",
variantId,
}
);
},
deleteOwnVariant(id) {
set(
(state) => {