implement image upload
This commit is contained in:
parent
960ee026bd
commit
52bf638baf
@ -69,7 +69,7 @@ function addQuizImages(quizId: number, image: Blob) {
|
||||
formData.append("quiz", quizId.toString());
|
||||
formData.append("image", image);
|
||||
|
||||
return makeRequest<FormData, never>({
|
||||
return makeRequest<FormData, { [key: string]: string; }>({
|
||||
url: `${imagesUrl}/quiz/putImages`,
|
||||
body: formData,
|
||||
method: "PUT",
|
||||
|
@ -29,7 +29,7 @@ export type QuizResultsType = true | null;
|
||||
|
||||
export interface QuizConfig {
|
||||
type: QuizType;
|
||||
logo: string;
|
||||
logo: string | null;
|
||||
noStartPage: boolean;
|
||||
startpageType: QuizStartpageType;
|
||||
results: QuizResultsType;
|
||||
@ -39,9 +39,9 @@ export interface QuizConfig {
|
||||
position: QuizStartpageAlignType;
|
||||
background: {
|
||||
type: null | "image" | "video";
|
||||
desktop: string;
|
||||
mobile: string;
|
||||
video: string;
|
||||
desktop: string | null;
|
||||
mobile: string | null;
|
||||
video: string | null;
|
||||
cycle: boolean;
|
||||
};
|
||||
};
|
||||
@ -57,7 +57,7 @@ export interface QuizConfig {
|
||||
|
||||
export const defaultQuizConfig: QuizConfig = {
|
||||
type: null,
|
||||
logo: "",
|
||||
logo: null,
|
||||
noStartPage: false,
|
||||
startpageType: null,
|
||||
results: null,
|
||||
@ -67,9 +67,9 @@ export const defaultQuizConfig: QuizConfig = {
|
||||
position: "left",
|
||||
background: {
|
||||
type: null,
|
||||
desktop: "https://happypik.ru/wp-content/uploads/2019/09/njashnye-kotiki8.jpg",
|
||||
mobile: "https://krot.info/uploads/posts/2022-03/1646156155_3-krot-info-p-smeshnie-tolstie-koti-smeshnie-foto-3.png",
|
||||
video: "https://youtu.be/dbaPkCiLPKQ",
|
||||
desktop: null,
|
||||
mobile: null,
|
||||
video: null,
|
||||
cycle: false,
|
||||
},
|
||||
},
|
||||
|
@ -72,7 +72,7 @@ export default function FormTypeQuestions({ question }: Props) {
|
||||
margin: "20px",
|
||||
}}
|
||||
>
|
||||
{(true /* TODO какое-то непонятное условие */
|
||||
{(true /* TODO только первый вопрос */
|
||||
? BUTTON_TYPE_QUESTIONS
|
||||
: BUTTON_TYPE_SHORT_QUESTIONS
|
||||
).map(({ icon, title, value: questionType }) => (
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import { openCropModal } from "@root/cropModal";
|
||||
import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal";
|
||||
import { addQuestionVariant, setVariantImageUrl, setVariantOriginalImageUrl } from "@root/questions/actions";
|
||||
import { addQuestionVariant, uploadQuestionImage } from "@root/questions/actions";
|
||||
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||
import { useState } from "react";
|
||||
@ -26,6 +26,7 @@ import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||
import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict";
|
||||
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
||||
import SwitchOptionsAndPict from "./switchOptionsAndPict";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
|
||||
|
||||
interface Props {
|
||||
@ -38,27 +39,41 @@ export default function OptionsAndPicture({ question }: Props) {
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||
const quizQid = useCurrentQuiz()?.qid;
|
||||
|
||||
const SSHC = (data: string) => {
|
||||
setSwitchState(data);
|
||||
};
|
||||
|
||||
const handleImageUpload = (files: FileList | null) => {
|
||||
const handleImageUpload = async (files: FileList | null) => {
|
||||
if (!files?.length || !selectedVariantId) return;
|
||||
|
||||
const [file] = Array.from(files);
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
setVariantImageUrl(question.id, selectedVariantId, url);
|
||||
setVariantOriginalImageUrl(question.id, selectedVariantId, url);
|
||||
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
|
||||
if (!("variants" in question.content)) return;
|
||||
|
||||
const variant = question.content.variants.find(variant => variant.id === selectedVariantId);
|
||||
if (!variant) return;
|
||||
|
||||
variant.extendedText = url;
|
||||
variant.originalImageUrl = url;
|
||||
});
|
||||
closeImageUploadModal();
|
||||
openCropModal(url, url);
|
||||
openCropModal(file, url);
|
||||
};
|
||||
|
||||
function handleCropModalSaveClick(url: string) {
|
||||
function handleCropModalSaveClick(imageBlob: Blob) {
|
||||
if (!selectedVariantId) return;
|
||||
|
||||
setVariantImageUrl(question.id, selectedVariantId, url);
|
||||
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
|
||||
if (!("variants" in question.content)) return;
|
||||
|
||||
const variant = question.content.variants.find(variant => variant.id === selectedVariantId);
|
||||
if (!variant) return;
|
||||
|
||||
variant.extendedText = url;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
@ -312,7 +327,7 @@ export default function OptionsAndPicture({ question }: Props) {
|
||||
height: "19px",
|
||||
}}
|
||||
onClick={() => {
|
||||
addQuestionVariant(question.id)
|
||||
addQuestionVariant(question.id);
|
||||
}}
|
||||
>
|
||||
Добавьте ответ
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import { openCropModal } from "@root/cropModal";
|
||||
import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal";
|
||||
import { addQuestionVariant, setVariantImageUrl, setVariantOriginalImageUrl } from "@root/questions/actions";
|
||||
import { addQuestionVariant, uploadQuestionImage } from "@root/questions/actions";
|
||||
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||
import { useState } from "react";
|
||||
@ -17,6 +17,7 @@ import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||
import ButtonsOptions from "../ButtonsOptions";
|
||||
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
||||
import SwitchAnswerOptionsPict from "./switchOptionsPict";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
|
||||
|
||||
interface Props {
|
||||
@ -25,6 +26,7 @@ interface Props {
|
||||
|
||||
export default function OptionsPicture({ question }: Props) {
|
||||
const theme = useTheme();
|
||||
const quizQid = useCurrentQuiz()?.qid;
|
||||
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
||||
const [switchState, setSwitchState] = useState("setting");
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||
@ -33,22 +35,35 @@ export default function OptionsPicture({ question }: Props) {
|
||||
setSwitchState(data);
|
||||
};
|
||||
|
||||
const handleImageUpload = (files: FileList | null) => {
|
||||
const handleImageUpload = async (files: FileList | null) => {
|
||||
if (!files?.length || !selectedVariantId) return;
|
||||
|
||||
const [file] = Array.from(files);
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
setVariantImageUrl(question.id, selectedVariantId, url);
|
||||
setVariantOriginalImageUrl(question.id, selectedVariantId, url);
|
||||
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
|
||||
if (!("variants" in question.content)) return;
|
||||
|
||||
const variant = question.content.variants.find(variant => variant.id === selectedVariantId);
|
||||
if (!variant) return;
|
||||
|
||||
variant.extendedText = url;
|
||||
variant.originalImageUrl = url;
|
||||
});
|
||||
closeImageUploadModal();
|
||||
openCropModal(url, url);
|
||||
openCropModal(file, url);
|
||||
};
|
||||
|
||||
function handleCropModalSaveClick(url: string) {
|
||||
function handleCropModalSaveClick(imageBlob: Blob) {
|
||||
if (!selectedVariantId) return;
|
||||
|
||||
setVariantImageUrl(question.id, selectedVariantId, url);
|
||||
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
|
||||
if (!("variants" in question.content)) return;
|
||||
|
||||
const variant = question.content.variants.find(variant => variant.id === selectedVariantId);
|
||||
if (!variant) return;
|
||||
|
||||
variant.extendedText = url;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -2,7 +2,8 @@ import { VideofileIcon } from "@icons/questionsPage/VideofileIcon";
|
||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { openCropModal } from "@root/cropModal";
|
||||
import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal";
|
||||
import { setPageQuestionOriginalPicture, setPageQuestionPicture, updateQuestion } from "@root/questions/actions";
|
||||
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
||||
import CustomTextField from "@ui_kit/CustomTextField";
|
||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||
@ -27,6 +28,7 @@ export default function PageOptions({ disableInput, question }: Props) {
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(980));
|
||||
const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(780));
|
||||
const quizQid = useCurrentQuiz()?.qid;
|
||||
|
||||
const setText = useDebouncedCallback((value) => {
|
||||
updateQuestion(question.id, question => {
|
||||
@ -40,19 +42,25 @@ export default function PageOptions({ disableInput, question }: Props) {
|
||||
setSwitchState(data);
|
||||
};
|
||||
|
||||
function handleImageUpload(fileList: FileList | null) {
|
||||
async function handleImageUpload(fileList: FileList | null) {
|
||||
if (!fileList?.length) return;
|
||||
|
||||
const url = URL.createObjectURL(fileList[0]);
|
||||
const url = await uploadQuestionImage(question.id, quizQid, fileList[0], (question, url) => {
|
||||
if (question.type !== "page") return;
|
||||
|
||||
setPageQuestionPicture(question.id, url);
|
||||
setPageQuestionOriginalPicture(question.id, url);
|
||||
question.content.picture = url;
|
||||
question.content.originalPicture = url;
|
||||
});
|
||||
closeImageUploadModal();
|
||||
openCropModal(url, url);
|
||||
openCropModal(fileList[0], url);
|
||||
}
|
||||
|
||||
function handleCropModalSaveClick(url: string) {
|
||||
setPageQuestionPicture(question.id, url);
|
||||
function handleCropModalSaveClick(imageBlob: Blob) {
|
||||
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
|
||||
if (question.type !== "page") return;
|
||||
|
||||
question.content.picture = url;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
||||
import { Box, ButtonBase, Typography, useTheme } from "@mui/material";
|
||||
import { openCropModal } from "@root/cropModal";
|
||||
import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal";
|
||||
import { setQuestionBackgroundImage, setQuestionOriginalBackgroundImage } from "@root/questions/actions";
|
||||
import { uploadQuestionImage } from "@root/questions/actions";
|
||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||
import UploadBox from "@ui_kit/UploadBox";
|
||||
import { type DragEvent } from "react";
|
||||
import UploadIcon from "../../../assets/icons/UploadIcon";
|
||||
import { UploadImageModal } from "./UploadImageModal";
|
||||
import { openCropModal } from "@root/cropModal";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
|
||||
|
||||
type UploadImageProps = {
|
||||
@ -16,18 +17,17 @@ type UploadImageProps = {
|
||||
|
||||
export default function UploadImage({ question }: UploadImageProps) {
|
||||
const theme = useTheme();
|
||||
const quizQid = useCurrentQuiz()?.qid;
|
||||
|
||||
const handleImageUpload = (files: FileList | null) => {
|
||||
const handleImageUpload = async (files: FileList | null) => {
|
||||
if (!files?.length) return;
|
||||
|
||||
const [file] = Array.from(files);
|
||||
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
setQuestionBackgroundImage(question.id, url);
|
||||
setQuestionOriginalBackgroundImage(question.id, url);
|
||||
const url = await uploadQuestionImage(question.id, quizQid, files[0], (question, url) => {
|
||||
question.content.back = url;
|
||||
question.content.originalBack = url;
|
||||
});
|
||||
closeImageUploadModal();
|
||||
openCropModal(url, url);
|
||||
openCropModal(files[0], url);
|
||||
};
|
||||
|
||||
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||
@ -37,8 +37,10 @@ export default function UploadImage({ question }: UploadImageProps) {
|
||||
handleImageUpload(event.dataTransfer.files);
|
||||
};
|
||||
|
||||
function handleCropModalSaveClick(url: string) {
|
||||
setQuestionBackgroundImage(question.id, url);
|
||||
function handleCropModalSaveClick(imageBlob: Blob) {
|
||||
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
|
||||
question.content.back = url;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -11,7 +11,7 @@ export default function ImageAndVideoButtons() {
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "12px", mt: "20px", mb: "20px" }}>
|
||||
<AddImage onClick={() => openCropModal("", "")} />
|
||||
<AddImage onClick={undefined/* TODO () => openCropModal("", "") */} />
|
||||
<CropModal />
|
||||
<Typography
|
||||
sx={{
|
||||
|
@ -82,7 +82,7 @@ export default function StartPageSettings() {
|
||||
const MobileVersionHC = (bool: boolean) => {
|
||||
setMobileVersion(bool);
|
||||
};
|
||||
|
||||
|
||||
if (!quiz) return null; // TODO throw and catch with error boundary
|
||||
|
||||
return (
|
||||
@ -306,6 +306,10 @@ export default function StartPageSettings() {
|
||||
text={"5 MB максимум"}
|
||||
heightImg={"110px"}
|
||||
sx={{ maxWidth: "300px" }}
|
||||
imageUrl={quiz.config.startpage.background.desktop}
|
||||
onImageUpload={(quiz, url) => {
|
||||
quiz.config.startpage.background.desktop = url;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@ -350,7 +354,14 @@ export default function StartPageSettings() {
|
||||
>
|
||||
Изображение для мобильной версии
|
||||
</Typography>
|
||||
<DropZone text={"5 MB максимум"} heightImg={"110px"} />
|
||||
<DropZone
|
||||
text={"5 MB максимум"}
|
||||
heightImg={"110px"}
|
||||
imageUrl={quiz.config.startpage.background.mobile}
|
||||
onImageUpload={(quiz, url) => {
|
||||
quiz.config.startpage.background.mobile = url;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<></>
|
||||
@ -432,7 +443,14 @@ export default function StartPageSettings() {
|
||||
>
|
||||
Изображение для мобильной версии
|
||||
</Typography>
|
||||
<DropZone text={"5 MB максимум"} heightImg={"110px"} />
|
||||
<DropZone
|
||||
text={"5 MB максимум"}
|
||||
heightImg={"110px"}
|
||||
imageUrl={quiz.config.startpage.background.mobile}
|
||||
onImageUpload={(quiz, url) => {
|
||||
quiz.config.startpage.background.mobile = url;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
@ -496,6 +514,10 @@ export default function StartPageSettings() {
|
||||
text={"5 MB максимум"}
|
||||
heightImg={"110px"}
|
||||
sx={{ maxWidth: "300px" }}
|
||||
imageUrl={quiz.config.logo}
|
||||
onImageUpload={(quiz, url) => {
|
||||
quiz.config.logo = url;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@ -566,6 +588,10 @@ export default function StartPageSettings() {
|
||||
text={"5 MB максимум"}
|
||||
heightImg={"110px"}
|
||||
sx={{ maxWidth: "300px" }}
|
||||
imageUrl={quiz.config.logo}
|
||||
onImageUpload={(quiz, url) => {
|
||||
quiz.config.logo = url;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import UploadIcon from "@icons/UploadIcon";
|
||||
import { Quiz } from "@model/quiz/quiz";
|
||||
import {
|
||||
Box,
|
||||
ButtonBase,
|
||||
@ -7,7 +8,7 @@ import {
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { updateQuiz } from "@root/quizes/actions";
|
||||
import { uploadQuizImage } from "@root/quizes/actions";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useState } from "react";
|
||||
@ -18,27 +19,27 @@ interface Props {
|
||||
sx?: SxProps<Theme>;
|
||||
heightImg: string;
|
||||
widthImg?: string;
|
||||
onImageUpload: (quiz: Quiz, url: string) => void;
|
||||
imageUrl: string | null;
|
||||
}
|
||||
|
||||
//Научи функцию принимать данные для валидации
|
||||
export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => {
|
||||
export const DropZone = ({ text, sx, heightImg, widthImg, onImageUpload, imageUrl }: Props) => {
|
||||
const theme = useTheme();
|
||||
const quiz = useCurrentQuiz();
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
if (!quiz) return null; // TODO throw and catch with error boundary
|
||||
|
||||
const imgHC = (imgInp: HTMLInputElement) => {
|
||||
const imgHC = async (imgInp: HTMLInputElement) => {
|
||||
if (!quiz) return;
|
||||
|
||||
const file = imgInp.files?.[0];
|
||||
if (file) {
|
||||
if (file.size < 5242880) {
|
||||
updateQuiz(quiz.id, quiz => {
|
||||
quiz.config.startpage.background.desktop = URL.createObjectURL(file);
|
||||
});
|
||||
} else {
|
||||
enqueueSnackbar("Размер картинки слишком велик");
|
||||
}
|
||||
}
|
||||
|
||||
if (!file) return;
|
||||
if (file.size > 5 * 2 ** 20) return enqueueSnackbar("Размер картинки слишком велик");
|
||||
|
||||
uploadQuizImage(quiz.id, file, onImageUpload);
|
||||
};
|
||||
|
||||
const dragenterHC = () => {
|
||||
@ -54,13 +55,9 @@ export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => {
|
||||
setReady(false);
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file.size < 5242880) {
|
||||
updateQuiz(quiz.id, quiz => {
|
||||
quiz.config.startpage.background.desktop = URL.createObjectURL(file);
|
||||
});
|
||||
} else {
|
||||
enqueueSnackbar("Размер картинки слишком велик");
|
||||
}
|
||||
if (file.size < 5 * 2 ** 20) return enqueueSnackbar("Размер картинки слишком велик");
|
||||
|
||||
uploadQuizImage(quiz.id, file, onImageUpload);
|
||||
};
|
||||
|
||||
const dragOverHC = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
@ -91,7 +88,7 @@ export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
border: `1px solid ${ready ? "red" : theme.palette.grey2.main}`,
|
||||
borderRadius: "8px",
|
||||
opacity: quiz.config.startpage.background.desktop ? "0.5" : 1,
|
||||
opacity: imageUrl ? "0.5" : 1,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
@ -109,11 +106,11 @@ export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => {
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
{quiz.config.startpage.background.desktop && (
|
||||
{imageUrl && (
|
||||
<img
|
||||
height={heightImg}
|
||||
width={widthImg}
|
||||
src={quiz.config.startpage.background.desktop}
|
||||
src={imageUrl}
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: "-1",
|
||||
|
@ -4,13 +4,13 @@ import { devtools } from "zustand/middleware";
|
||||
|
||||
type CropModalStore = {
|
||||
isCropModalOpen: boolean;
|
||||
imageUrl: string | null;
|
||||
imageBlob: Blob | null;
|
||||
originalImageUrl: string | null;
|
||||
};
|
||||
|
||||
const initialState: CropModalStore = {
|
||||
isCropModalOpen: false,
|
||||
imageUrl: null,
|
||||
imageBlob: null,
|
||||
originalImageUrl: null,
|
||||
};
|
||||
|
||||
@ -25,19 +25,24 @@ export const useCropModalStore = create<CropModalStore>()(
|
||||
),
|
||||
);
|
||||
|
||||
export const openCropModal = (imageUrl: string, originalImageUrl: string) => useCropModalStore.setState(
|
||||
{
|
||||
isCropModalOpen: true,
|
||||
imageUrl,
|
||||
originalImageUrl,
|
||||
},
|
||||
false,
|
||||
{
|
||||
type: "openCropModal",
|
||||
imageUrl,
|
||||
originalImageUrl,
|
||||
export const openCropModal = async (image: Blob | string, originalImageUrl: string | null | undefined) => {
|
||||
if (typeof image === "string") {
|
||||
const response = await fetch(image);
|
||||
const blob = await response.blob();
|
||||
|
||||
useCropModalStore.setState({
|
||||
isCropModalOpen: true,
|
||||
imageBlob: blob,
|
||||
originalImageUrl,
|
||||
}, false, "openCropModal");
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
useCropModalStore.setState({
|
||||
isCropModalOpen: true,
|
||||
imageBlob: image,
|
||||
}, false, "openCropModal");
|
||||
};
|
||||
|
||||
export const closeCropModal = () => useCropModalStore.setState(
|
||||
initialState,
|
||||
@ -45,22 +50,14 @@ export const closeCropModal = () => useCropModalStore.setState(
|
||||
"closeCropModal"
|
||||
);
|
||||
|
||||
export const setCropModalImageUrl = (imageUrl: string | null) => useCropModalStore.setState(
|
||||
{ imageUrl },
|
||||
export const setCropModalImageBlob = (imageBlob: Blob | null) => useCropModalStore.setState(
|
||||
{ imageBlob },
|
||||
false,
|
||||
{
|
||||
type: "setCropModalImageUrl",
|
||||
imageUrl,
|
||||
}
|
||||
"setCropModalImageUrl"
|
||||
);
|
||||
|
||||
export const resetToOriginalImage = (): boolean => {
|
||||
if (!useCropModalStore.getState().originalImageUrl) return false;
|
||||
|
||||
useCropModalStore.setState(
|
||||
state => ({ imageUrl: state.originalImageUrl }),
|
||||
false,
|
||||
"resetToOriginalImage"
|
||||
);
|
||||
return true;
|
||||
};
|
||||
export const setCropModalOriginalImageUrl = (originalImageUrl: string | null) => useCropModalStore.setState(
|
||||
{ originalImageUrl },
|
||||
false,
|
||||
"setCropModalOriginalImageUrl"
|
||||
);
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { questionApi } from "@api/question";
|
||||
import { quizApi } from "@api/quiz";
|
||||
import { devlog } from "@frontend/kitui";
|
||||
import { questionToEditQuestionRequest } from "@model/question/edit";
|
||||
import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question";
|
||||
import { AnyTypedQuizQuestion, QuestionVariant, UntypedQuizQuestion, createQuestionVariant } from "@model/questionTypes/shared";
|
||||
import { defaultQuestionByType } from "../../constants/default";
|
||||
import { produce } from "immer";
|
||||
import { nanoid } from "nanoid";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { defaultQuestionByType } from "../../constants/default";
|
||||
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
|
||||
import { RequestQueue } from "../../utils/requestQueue";
|
||||
import { QuestionsStore, useQuestionsStore } from "./store";
|
||||
@ -136,11 +137,11 @@ export const updateQuestion = (
|
||||
clearTimeout(requestTimeoutId);
|
||||
requestTimeoutId = setTimeout(() => {
|
||||
requestQueue.enqueue(async () => {
|
||||
const q = useQuestionsStore.getState().questions.find(q => q.id === questionId);
|
||||
if (!q) return;
|
||||
if (q.type === null) throw new Error("Cannot send update request for untyped question");
|
||||
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
|
||||
if (!question) return;
|
||||
if (question.type === null) throw new Error("Cannot send update request for untyped question");
|
||||
|
||||
const response = await questionApi.edit(questionToEditQuestionRequest(q));
|
||||
const response = await questionApi.edit(questionToEditQuestionRequest(question));
|
||||
|
||||
setQuestionBackendId(questionId, response.updated);
|
||||
}).catch(error => {
|
||||
@ -211,98 +212,37 @@ export const reorderQuestionVariants = (
|
||||
});
|
||||
};
|
||||
|
||||
export const setQuestionBackgroundImage = (
|
||||
export const uploadQuestionImage = async (
|
||||
questionId: string,
|
||||
url: string,
|
||||
quizQid: string | undefined,
|
||||
blob: Blob,
|
||||
updateFn: (question: AnyTypedQuizQuestion, imageId: string) => void,
|
||||
) => {
|
||||
updateQuestion(questionId, question => {
|
||||
if (question.content.back === url) return;
|
||||
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
|
||||
if (!question || !quizQid) return;
|
||||
|
||||
if (
|
||||
question.content.back !== question.content.originalBack
|
||||
) URL.revokeObjectURL(question.content.back);
|
||||
question.content.back = url;
|
||||
});
|
||||
};
|
||||
try {
|
||||
const response = await quizApi.addImages(question.quizId, blob);
|
||||
|
||||
export const setQuestionOriginalBackgroundImage = (
|
||||
questionId: string,
|
||||
url: string,
|
||||
) => {
|
||||
updateQuestion(questionId, question => {
|
||||
if (question.content.originalBack === url) return;
|
||||
const values = Object.values(response);
|
||||
if (values.length !== 1) {
|
||||
console.warn("Error uploading image");
|
||||
return;
|
||||
}
|
||||
|
||||
URL.revokeObjectURL(question.content.originalBack);
|
||||
question.content.originalBack = url;
|
||||
});
|
||||
};
|
||||
const imageId = values[0];
|
||||
const imageUrl = `https://squiz.pena.digital/squizimages/${quizQid}/${imageId}`;
|
||||
|
||||
export const setVariantImageUrl = (
|
||||
questionId: string,
|
||||
variantId: string,
|
||||
url: string,
|
||||
) => {
|
||||
updateQuestion(questionId, question => {
|
||||
if (!("variants" in question.content)) return;
|
||||
updateQuestion(questionId, question => {
|
||||
updateFn(question, imageUrl);
|
||||
});
|
||||
|
||||
const variant = question.content.variants.find(variant => variant.id === variantId);
|
||||
if (!variant) return;
|
||||
return imageUrl;
|
||||
} catch (error) {
|
||||
devlog("Error uploading question image", error);
|
||||
|
||||
if (variant.extendedText === url) return;
|
||||
|
||||
if (variant.extendedText !== variant.originalImageUrl) URL.revokeObjectURL(variant.extendedText);
|
||||
variant.extendedText = url;
|
||||
});
|
||||
};
|
||||
|
||||
export const setVariantOriginalImageUrl = (
|
||||
questionId: string,
|
||||
variantId: string,
|
||||
url: string,
|
||||
) => {
|
||||
updateQuestion(questionId, question => {
|
||||
if (!("variants" in question.content)) return;
|
||||
|
||||
const variant = question.content.variants.find(
|
||||
variant => variant.id === variantId
|
||||
) as QuestionVariant | undefined;
|
||||
if (!variant) return;
|
||||
|
||||
if (variant.originalImageUrl === url) return;
|
||||
|
||||
URL.revokeObjectURL(variant.originalImageUrl);
|
||||
variant.originalImageUrl = url;
|
||||
});
|
||||
};
|
||||
|
||||
export const setPageQuestionPicture = (
|
||||
questionId: string,
|
||||
url: string,
|
||||
) => {
|
||||
updateQuestion(questionId, question => {
|
||||
if (question.type !== "page") return;
|
||||
|
||||
if (question.content.picture === url) return;
|
||||
|
||||
if (
|
||||
question.content.picture !== question.content.originalPicture
|
||||
) URL.revokeObjectURL(question.content.picture);
|
||||
question.content.picture = url;
|
||||
});
|
||||
};
|
||||
|
||||
export const setPageQuestionOriginalPicture = (
|
||||
questionId: string,
|
||||
url: string,
|
||||
) => {
|
||||
updateQuestion(questionId, question => {
|
||||
if (question.type !== "page") return;
|
||||
|
||||
if (question.content.originalPicture === url) return;
|
||||
|
||||
URL.revokeObjectURL(question.content.originalPicture);
|
||||
question.content.originalPicture = url;
|
||||
});
|
||||
enqueueSnackbar("Не удалось загрузить изображение");
|
||||
}
|
||||
};
|
||||
|
||||
export const setQuestionInnerName = (
|
||||
|
@ -178,6 +178,35 @@ export const deleteQuiz = async (quizId: string) => requestQueue.enqueue(async (
|
||||
|
||||
// TODO copy quiz
|
||||
|
||||
export const uploadQuizImage = async (
|
||||
quizId: string,
|
||||
blob: Blob,
|
||||
updateFn: (quiz: Quiz, imageId: string) => void,
|
||||
) => {
|
||||
const quiz = useQuizStore.getState().quizes.find(q => q.id === quizId);
|
||||
if (!quiz) return;
|
||||
|
||||
try {
|
||||
const response = await quizApi.addImages(quiz.backendId, blob);
|
||||
|
||||
const values = Object.values(response);
|
||||
if (values.length !== 1) {
|
||||
console.warn("Error uploading image");
|
||||
return;
|
||||
}
|
||||
|
||||
const imageId = values[0];
|
||||
|
||||
updateQuiz(quizId, quiz => {
|
||||
updateFn(quiz, `https://squiz.pena.digital/squizimages/${quiz.qid}/${imageId}`);
|
||||
});
|
||||
} catch (error) {
|
||||
devlog("Error uploading quiz image", error);
|
||||
|
||||
enqueueSnackbar("Не удалось загрузить изображение");
|
||||
}
|
||||
};
|
||||
|
||||
function setProducedState<A extends string | { type: unknown; }>(
|
||||
recipe: (state: QuizStore) => void,
|
||||
action?: A,
|
||||
|
@ -12,12 +12,11 @@ import {
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { closeCropModal, resetToOriginalImage, setCropModalImageUrl, useCropModalStore } from "@root/cropModal";
|
||||
import { FC, useRef, useState } from "react";
|
||||
import { closeCropModal, setCropModalImageBlob, useCropModalStore } from "@root/cropModal";
|
||||
import { FC, useMemo, useRef, useState } from "react";
|
||||
import ReactCrop, { Crop, PixelCrop } from "react-image-crop";
|
||||
import "react-image-crop/dist/ReactCrop.css";
|
||||
import { canvasPreview } from "./utils/canvasPreview";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
|
||||
const styleSlider: SxProps<Theme> = {
|
||||
@ -45,13 +44,14 @@ const styleSlider: SxProps<Theme> = {
|
||||
};
|
||||
|
||||
interface Props {
|
||||
onSaveImageClick?: (imageUrl: string) => void;
|
||||
onSaveImageClick?: (imageBlob: Blob) => void;
|
||||
}
|
||||
|
||||
export const CropModal: FC<Props> = ({ onSaveImageClick }) => {
|
||||
const theme = useTheme();
|
||||
const isCropModalOpen = useCropModalStore(state => state.isCropModalOpen);
|
||||
const imageUrl = useCropModalStore(state => state.imageUrl);
|
||||
const imageBlob = useCropModalStore(state => state.imageBlob);
|
||||
const originalImageUrl = useCropModalStore(state => state.originalImageUrl);
|
||||
const [crop, setCrop] = useState<Crop>();
|
||||
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
|
||||
const [darken, setDarken] = useState(0);
|
||||
@ -60,6 +60,8 @@ export const CropModal: FC<Props> = ({ onSaveImageClick }) => {
|
||||
const cropImageElementRef = useRef<HTMLImageElement>(null);
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(786));
|
||||
|
||||
const imageUrl = useMemo(() => imageBlob && URL.createObjectURL(imageBlob), [imageBlob]);
|
||||
|
||||
const handleCropClick = async () => {
|
||||
if (!completedCrop) throw new Error("No completed crop");
|
||||
if (!cropImageElementRef.current) throw new Error("No image");
|
||||
@ -75,27 +77,31 @@ export const CropModal: FC<Props> = ({ onSaveImageClick }) => {
|
||||
await canvasPreview(cropImageElementRef.current, canvasCopy, completedCrop, rotate);
|
||||
|
||||
canvasCopy.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
throw new Error("Failed to create blob");
|
||||
}
|
||||
const newImageUrl = URL.createObjectURL(blob);
|
||||
if (!blob) throw new Error("Failed to create blob");
|
||||
|
||||
setCropModalImageUrl(newImageUrl);
|
||||
setCropModalImageBlob(blob);
|
||||
setCrop(undefined);
|
||||
setCompletedCrop(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
function handleSaveClick() {
|
||||
if (imageUrl) onSaveImageClick?.(imageUrl);
|
||||
if (imageBlob) onSaveImageClick?.(imageBlob);
|
||||
setCrop(undefined);
|
||||
setCompletedCrop(undefined);
|
||||
closeCropModal();
|
||||
}
|
||||
|
||||
function handleLoadOriginalImage() {
|
||||
const isSuccess = resetToOriginalImage();
|
||||
if (!isSuccess) enqueueSnackbar("Не удалось восстановить оригинал. Приносим глубочайшие извинения");
|
||||
async function handleLoadOriginalImage() {
|
||||
if (!originalImageUrl) return;
|
||||
|
||||
const response = await fetch(originalImageUrl);
|
||||
const blob = await response.blob();
|
||||
|
||||
onSaveImageClick?.(blob);
|
||||
setCropModalImageBlob(blob);
|
||||
setCrop(undefined);
|
||||
setCompletedCrop(undefined);
|
||||
}
|
||||
|
||||
const getImageSize = () => {
|
||||
@ -257,6 +263,7 @@ export const CropModal: FC<Props> = ({ onSaveImageClick }) => {
|
||||
<Button
|
||||
onClick={handleLoadOriginalImage}
|
||||
disableRipple
|
||||
disabled={!originalImageUrl}
|
||||
sx={{
|
||||
width: "215px",
|
||||
height: "48px",
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { Box, Button } from "@mui/material";
|
||||
import { FC } from "react";
|
||||
import { CropModal } from "./CropModal";
|
||||
import { openCropModal } from "@root/cropModal";
|
||||
|
||||
const ImageCrop: FC = () => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Button onClick={() => openCropModal("", "")}>Открыть модалку</Button>
|
||||
<Button onClick={undefined}>Открыть модалку</Button>
|
||||
<CropModal />
|
||||
</Box>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user