implement image upload

This commit is contained in:
nflnkr 2023-12-01 21:05:59 +03:00
parent 960ee026bd
commit 52bf638baf
15 changed files with 243 additions and 208 deletions

@ -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>
);