fix dropzone & image modals

This commit is contained in:
nflnkr 2023-12-04 14:57:54 +03:00
parent 7824fafc51
commit 0e1f9aab23
9 changed files with 425 additions and 456 deletions

@ -14,11 +14,10 @@ import {
useMediaQuery, useMediaQuery,
useTheme useTheme
} from "@mui/material"; } from "@mui/material";
import { setCropModal } from "@root/cropModal";
import { addQuestionVariant, uploadQuestionImage } from "@root/questions/actions"; import { addQuestionVariant, uploadQuestionImage } from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton"; import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import { CropModal } from "@ui_kit/Modal/CropModal"; import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { useState } from "react"; import { useState } from "react";
import { useDisclosure } from "../../../utils/useDisclosure"; import { useDisclosure } from "../../../utils/useDisclosure";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
@ -40,17 +39,22 @@ export default function OptionsAndPicture({ question }: Props) {
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const quizQid = useCurrentQuiz()?.qid; const quizQid = useCurrentQuiz()?.qid;
const [isCropModalOpen, openCropModal, closeCropModal] = useDisclosure();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure(); const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
originalImageUrl,
setCropModalImageBlob,
} = useCropModalState();
const SSHC = (data: string) => { const SSHC = (data: string) => {
setSwitchState(data); setSwitchState(data);
}; };
const handleImageUpload = async (files: FileList | null) => { const handleImageUpload = async (file: File) => {
if (!files?.length || !selectedVariantId) return; if (!selectedVariantId) return;
const [file] = Array.from(files);
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => { const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
if (!("variants" in question.content)) return; if (!("variants" in question.content)) return;
@ -62,8 +66,7 @@ export default function OptionsAndPicture({ question }: Props) {
variant.originalImageUrl = url; variant.originalImageUrl = url;
}); });
closeImageUploadModal(); closeImageUploadModal();
setCropModal(file, url); openCropModal(file, url);
openCropModal();
}; };
function handleCropModalSaveClick(imageBlob: Blob) { function handleCropModalSaveClick(imageBlob: Blob) {
@ -92,13 +95,10 @@ export default function OptionsAndPicture({ question }: Props) {
onImageClick={() => { onImageClick={() => {
setSelectedVariantId(variant.id); setSelectedVariantId(variant.id);
if (variant.extendedText) { if (variant.extendedText) {
openCropModal(); return openCropModal(
setCropModal(
variant.extendedText, variant.extendedText,
variant.originalImageUrl variant.originalImageUrl
); );
return;
} }
openImageUploadModal(); openImageUploadModal();
@ -120,13 +120,10 @@ export default function OptionsAndPicture({ question }: Props) {
onImageClick={() => { onImageClick={() => {
setSelectedVariantId(variant.id); setSelectedVariantId(variant.id);
if (variant.extendedText) { if (variant.extendedText) {
openCropModal(); return openCropModal(
setCropModal(
variant.extendedText, variant.extendedText,
variant.originalImageUrl variant.originalImageUrl
); );
return;
} }
openImageUploadModal(); openImageUploadModal();
@ -141,8 +138,19 @@ export default function OptionsAndPicture({ question }: Props) {
</> </>
)} )}
/> />
<UploadImageModal isOpen={isImageUploadOpen} onClose={closeImageUploadModal} imgHC={handleImageUpload} /> <UploadImageModal
<CropModal isOpen={isCropModalOpen} onClose={closeCropModal} onSaveImageClick={handleCropModalSaveClick} /> isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
/>
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",

@ -5,10 +5,9 @@ import {
useMediaQuery, useMediaQuery,
useTheme useTheme
} from "@mui/material"; } from "@mui/material";
import { setCropModal } from "@root/cropModal";
import { addQuestionVariant, uploadQuestionImage } from "@root/questions/actions"; import { addQuestionVariant, uploadQuestionImage } from "@root/questions/actions";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton"; import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import { CropModal } from "@ui_kit/Modal/CropModal"; import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { useState } from "react"; import { useState } from "react";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import type { QuizQuestionImages } from "../../../model/questionTypes/images"; import type { QuizQuestionImages } from "../../../model/questionTypes/images";
@ -30,17 +29,22 @@ export default function OptionsPicture({ question }: Props) {
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null); const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
const [switchState, setSwitchState] = useState("setting"); const [switchState, setSwitchState] = useState("setting");
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const [isCropModalOpen, openCropModal, closeCropModal] = useDisclosure();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure(); const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
originalImageUrl,
setCropModalImageBlob,
} = useCropModalState();
const SSHC = (data: string) => { const SSHC = (data: string) => {
setSwitchState(data); setSwitchState(data);
}; };
const handleImageUpload = async (files: FileList | null) => { const handleImageUpload = async (file: File) => {
if (!files?.length || !selectedVariantId) return; if (!selectedVariantId) return;
const [file] = Array.from(files);
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => { const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
if (!("variants" in question.content)) return; if (!("variants" in question.content)) return;
@ -52,8 +56,7 @@ export default function OptionsPicture({ question }: Props) {
variant.originalImageUrl = url; variant.originalImageUrl = url;
}); });
closeImageUploadModal(); closeImageUploadModal();
setCropModal(file, url); openCropModal(file, url);
openCropModal();
}; };
function handleCropModalSaveClick(imageBlob: Blob) { function handleCropModalSaveClick(imageBlob: Blob) {
@ -82,13 +85,10 @@ export default function OptionsPicture({ question }: Props) {
onImageClick={() => { onImageClick={() => {
setSelectedVariantId(variant.id); setSelectedVariantId(variant.id);
if (variant.extendedText) { if (variant.extendedText) {
openCropModal(); return openCropModal(
setCropModal(
variant.extendedText, variant.extendedText,
variant.originalImageUrl || "" variant.originalImageUrl
); );
return;
} }
openImageUploadModal(); openImageUploadModal();
@ -110,13 +110,10 @@ export default function OptionsPicture({ question }: Props) {
onImageClick={() => { onImageClick={() => {
setSelectedVariantId(variant.id); setSelectedVariantId(variant.id);
if (variant.extendedText) { if (variant.extendedText) {
openCropModal(); return openCropModal(
setCropModal(
variant.extendedText, variant.extendedText,
variant.originalImageUrl || "" variant.originalImageUrl
); );
return;
} }
openImageUploadModal(); openImageUploadModal();
@ -131,8 +128,19 @@ export default function OptionsPicture({ question }: Props) {
</> </>
)} )}
/> />
<UploadImageModal isOpen={isImageUploadOpen} onClose={closeImageUploadModal} imgHC={handleImageUpload} /> <UploadImageModal
<CropModal isOpen={isCropModalOpen} onClose={closeCropModal} onSaveImageClick={handleCropModalSaveClick} /> isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
/>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}> <Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Link <Link
component="button" component="button"

@ -1,11 +1,10 @@
import { VideofileIcon } from "@icons/questionsPage/VideofileIcon"; import { VideofileIcon } from "@icons/questionsPage/VideofileIcon";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { setCropModal } from "@root/cropModal";
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions"; import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton"; import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { CropModal } from "@ui_kit/Modal/CropModal"; import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { useState } from "react"; import { useState } from "react";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import type { QuizQuestionPage } from "../../../model/questionTypes/page"; import type { QuizQuestionPage } from "../../../model/questionTypes/page";
@ -29,7 +28,14 @@ export default function PageOptions({ disableInput, question }: Props) {
const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990)); const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(780)); const isMobile = useMediaQuery(theme.breakpoints.down(780));
const quizQid = useCurrentQuiz()?.qid; const quizQid = useCurrentQuiz()?.qid;
const [isCropModalOpen, openCropModal, closeCropModal] = useDisclosure(); const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
originalImageUrl,
setCropModalImageBlob,
} = useCropModalState();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure(); const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const setText = useDebouncedCallback((value) => { const setText = useDebouncedCallback((value) => {
@ -44,18 +50,15 @@ export default function PageOptions({ disableInput, question }: Props) {
setSwitchState(data); setSwitchState(data);
}; };
async function handleImageUpload(fileList: FileList | null) { async function handleImageUpload(file: File) {
if (!fileList?.length) return; const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
const url = await uploadQuestionImage(question.id, quizQid, fileList[0], (question, url) => {
if (question.type !== "page") return; if (question.type !== "page") return;
question.content.picture = url; question.content.picture = url;
question.content.originalPicture = url; question.content.originalPicture = url;
}); });
closeImageUploadModal(); closeImageUploadModal();
setCropModal(fileList[0], url); openCropModal(file, url);
openCropModal();
} }
function handleCropModalSaveClick(imageBlob: Blob) { function handleCropModalSaveClick(imageBlob: Blob) {
@ -108,7 +111,7 @@ export default function PageOptions({ disableInput, question }: Props) {
imageSrc={question.content.picture} imageSrc={question.content.picture}
onImageClick={() => { onImageClick={() => {
if (question.content.picture) { if (question.content.picture) {
return setCropModal( return openCropModal(
question.content.picture, question.content.picture,
question.content.originalPicture question.content.originalPicture
); );
@ -133,8 +136,19 @@ export default function PageOptions({ disableInput, question }: Props) {
Изображение Изображение
</Typography> </Typography>
</Box> </Box>
<UploadImageModal isOpen={isImageUploadOpen} onClose={closeImageUploadModal} imgHC={handleImageUpload} /> <UploadImageModal
<CropModal isOpen={isCropModalOpen} onClose={closeCropModal} onSaveImageClick={handleCropModalSaveClick} /> isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
/>
<Typography> или</Typography> <Typography> или</Typography>
<Box <Box
sx={{ sx={{

@ -9,29 +9,26 @@ import {
} from "@mui/material"; } from "@mui/material";
import UploadIcon from "../../../assets/icons/UploadIcon"; import UploadIcon from "../../../assets/icons/UploadIcon";
import SearchIcon from "../../../assets/icons/SearchIcon"; import SearchIcon from "../../../assets/icons/SearchIcon";
import * as React from "react";
import UnsplashIcon from "../../../assets/icons/Unsplash.svg"; import UnsplashIcon from "../../../assets/icons/Unsplash.svg";
import { useRef, useState, type DragEvent } from "react";
import type { DragEvent } from "react";
interface ModalkaProps { interface ModalkaProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
imgHC: (imgInp: FileList | null) => void; handleImageChange: (file: File) => void;
} }
export const UploadImageModal: React.FC<ModalkaProps> = ({ export const UploadImageModal: React.FC<ModalkaProps> = ({
imgHC, handleImageChange,
isOpen, isOpen,
onClose, onClose,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const dropZone = useRef<HTMLDivElement>(null);
const [ready, setReady] = useState(false);
const dropZone = React.useRef<HTMLDivElement>(null); const handleDragEnter = (event: DragEvent<HTMLDivElement>) => {
const [ready, setReady] = React.useState(false);
const handleDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
setReady(true); setReady(true);
}; };
@ -40,7 +37,10 @@ export const UploadImageModal: React.FC<ModalkaProps> = ({
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
imgHC(event.dataTransfer.files); const file = event.dataTransfer.files[0];
if (!file) return;
handleImageChange(file);
}; };
return ( return (
@ -79,7 +79,7 @@ export const UploadImageModal: React.FC<ModalkaProps> = ({
</Typography> </Typography>
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}> <ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
<input <input
onChange={(event) => imgHC(event.target.files)} onChange={(event) => event.target.files?.[0] && handleImageChange(event.target.files[0])}
hidden hidden
accept="image/*" accept="image/*"
multiple multiple

@ -1,14 +1,11 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { Box, ButtonBase, Typography, useTheme } from "@mui/material"; import { Box, ButtonBase, Typography, useTheme } from "@mui/material";
import { uploadQuestionImage } from "@root/questions/actions"; import { updateQuestion, 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 { setCropModal } from "@root/cropModal";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { DropZone } from "../../../pages/startPage/dropZone";
import { useDisclosure } from "../../../utils/useDisclosure"; import { useDisclosure } from "../../../utils/useDisclosure";
import { UploadImageModal } from "./UploadImageModal";
type UploadImageProps = { type UploadImageProps = {
@ -17,34 +14,9 @@ type UploadImageProps = {
export default function UploadImage({ question }: UploadImageProps) { export default function UploadImage({ question }: UploadImageProps) {
const theme = useTheme(); const theme = useTheme();
const quizQid = useCurrentQuiz()?.qid; const quiz = useCurrentQuiz();
const [isCropModalOpen, openCropModal, closeCropModal] = useDisclosure();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const handleImageUpload = async (files: FileList | null) => { if (!quiz) return null;
if (!files?.length) return;
const url = await uploadQuestionImage(question.id, quizQid, files[0], (question, url) => {
question.content.back = url;
question.content.originalBack = url;
});
closeImageUploadModal();
setCropModal(files[0], url);
openCropModal();
};
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
handleImageUpload(event.dataTransfer.files);
};
function handleCropModalSaveClick(imageBlob: Blob) {
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
question.content.back = url;
});
}
return ( return (
<Box sx={{ padding: "20px" }}> <Box sx={{ padding: "20px" }}>
@ -58,36 +30,29 @@ export default function UploadImage({ question }: UploadImageProps) {
> >
Загрузить изображение Загрузить изображение
</Typography> </Typography>
<ButtonBase <DropZone
onClick={openImageUploadModal} text={"5 MB максимум"}
sx={{ heightImg={"110px"}
width: "100%", sx={{ maxWidth: "300px", width: "100%" }}
maxWidth: "260px", imageUrl={question.content.back}
height: "120px", originalImageUrl={question.content.originalBack}
onImageUploadClick={file => {
uploadQuestionImage(question.id, quiz.qid, file, (question, url) => {
question.content.back = url;
question.content.originalBack = url;
});
}} }}
> onDeleteClick={() => {
{question.content.back ? updateQuestion(question.id, question => {
<img question.content.back = null;
src={question.content.back} });
alt="question background" }}
style={{ onImageSaveClick={file => {
width: "100%", uploadQuestionImage(question.id, quiz.qid, file, (question, url) => {
height: "100%", question.content.back = url;
objectFit: "scale-down", });
display: "block",
}} }}
/> />
:
<UploadBox
handleDrop={handleDrop}
sx={{ maxWidth: "260px" }}
icon={<UploadIcon />}
text="5 MB максимум"
/>
}
</ButtonBase>
<UploadImageModal isOpen={isImageUploadOpen} onClose={closeImageUploadModal} imgHC={handleImageUpload} />
<CropModal isOpen={isCropModalOpen} onClose={closeCropModal} onSaveImageClick={handleCropModalSaveClick} />
</Box> </Box>
); );
} }

@ -307,7 +307,14 @@ export default function StartPageSettings() {
heightImg={"110px"} heightImg={"110px"}
sx={{ maxWidth: "300px" }} sx={{ maxWidth: "300px" }}
imageUrl={quiz.config.startpage.background.desktop} imageUrl={quiz.config.startpage.background.desktop}
onFileChange={file => { originalImageUrl={quiz.config.startpage.background.originalDesktop}
onImageUploadClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.desktop = url;
quiz.config.startpage.background.originalDesktop = url;
});
}}
onImageSaveClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => { uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.desktop = url; quiz.config.startpage.background.desktop = url;
}); });
@ -368,7 +375,14 @@ export default function StartPageSettings() {
text={"5 MB максимум"} text={"5 MB максимум"}
heightImg={"110px"} heightImg={"110px"}
imageUrl={quiz.config.startpage.background.mobile} imageUrl={quiz.config.startpage.background.mobile}
onFileChange={file => { originalImageUrl={quiz.config.startpage.background.originalMobile}
onImageUploadClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.mobile = url;
quiz.config.startpage.background.originalMobile = url;
});
}}
onImageSaveClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => { uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.mobile = url; quiz.config.startpage.background.mobile = url;
}); });
@ -464,7 +478,14 @@ export default function StartPageSettings() {
text={"5 MB максимум"} text={"5 MB максимум"}
heightImg={"110px"} heightImg={"110px"}
imageUrl={quiz.config.startpage.background.mobile} imageUrl={quiz.config.startpage.background.mobile}
onFileChange={file => { originalImageUrl={quiz.config.startpage.background.originalMobile}
onImageUploadClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.mobile = url;
quiz.config.startpage.background.originalMobile = url;
});
}}
onImageSaveClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => { uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.mobile = url; quiz.config.startpage.background.mobile = url;
}); });
@ -539,7 +560,14 @@ export default function StartPageSettings() {
heightImg={"110px"} heightImg={"110px"}
sx={{ maxWidth: "300px" }} sx={{ maxWidth: "300px" }}
imageUrl={quiz.config.logo} imageUrl={quiz.config.logo}
onFileChange={file => { originalImageUrl={quiz.config.originalLogo}
onImageUploadClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.logo = url;
quiz.config.originalLogo = url;
});
}}
onImageSaveClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => { uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.logo = url; quiz.config.logo = url;
}); });
@ -620,7 +648,14 @@ export default function StartPageSettings() {
heightImg={"110px"} heightImg={"110px"}
sx={{ maxWidth: "300px" }} sx={{ maxWidth: "300px" }}
imageUrl={quiz.config.logo} imageUrl={quiz.config.logo}
onFileChange={file => { originalImageUrl={quiz.config.originalLogo}
onImageUploadClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.logo = url;
quiz.config.originalLogo = url;
});
}}
onImageSaveClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => { uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.logo = url; quiz.config.logo = url;
}); });

@ -11,7 +11,10 @@ import {
} from "@mui/material"; } from "@mui/material";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useState } from "react"; import { UploadImageModal } from "../../pages/Questions/UploadImage/UploadImageModal";
import { useEffect, useState } from "react";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { useDisclosure } from "../../utils/useDisclosure";
interface Props { interface Props {
@ -19,71 +22,69 @@ interface Props {
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
heightImg: string; heightImg: string;
widthImg?: string; widthImg?: string;
onFileChange?: (file: File) => void;
onDeleteClick?: () => void;
imageUrl: string | null; imageUrl: string | null;
originalImageUrl: string | null;
onImageUploadClick: (image: Blob) => void;
onImageSaveClick: (image: Blob) => void;
onDeleteClick: () => void;
} }
//Научи функцию принимать данные для валидации //Научи функцию принимать данные для валидации
export const DropZone = ({ text, sx, heightImg, widthImg, onFileChange, onDeleteClick, imageUrl }: Props) => { export const DropZone = ({ text, sx, heightImg, widthImg, imageUrl, originalImageUrl, onImageUploadClick, onImageSaveClick, onDeleteClick }: Props) => {
const theme = useTheme(); const theme = useTheme();
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
const [ready, setReady] = useState(false); const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
setCropModalImageBlob,
} = useCropModalState();
if (!quiz) return null; // TODO throw and catch with error boundary if (!quiz) return null; // TODO throw and catch with error boundary
const imgHC = async (imgInp: HTMLInputElement) => { async function handleImageUpload(file: File) {
if (!quiz) return; onImageUploadClick?.(file);
closeImageUploadModal();
openCropModal(file);
}
const file = imgInp.files?.[0]; return (
<Box sx={{
if (!file) return; position: "relative",
if (file.size > 5 * 2 ** 20) return enqueueSnackbar("Размер картинки слишком велик");
onFileChange?.(file);
};
const dragenterHC = () => {
setReady(true);
};
const dragexitHC = () => {
setReady(false);
};
const dropHC = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setReady(false);
const file = event.dataTransfer.files[0];
if (file.size < 5 * 2 ** 20) return enqueueSnackbar("Размер картинки слишком велик");
onFileChange?.(file);
};
const dragOverHC = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
};
return imageUrl ? (
<Box
onDragEnter={dragenterHC}
onDragExit={dragexitHC}
onDrop={dropHC}
onDragOver={dragOverHC}
sx={{
width: "100%", width: "100%",
height: "120px", height: "120px",
position: "relative", backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px",
...sx,
}}>
<UploadImageModal
isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={onImageSaveClick}
/>
<ButtonBase
onClick={imageUrl ? () => openCropModal(imageUrl) : openImageUploadModal}
sx={{
width: "100%",
height: "100%",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
backgroundColor: theme.palette.background.default,
border: `1px solid ${ready ? "red" : theme.palette.grey2.main}`,
borderRadius: "8px", borderRadius: "8px",
...sx,
}} }}
> >
{imageUrl ?
<img <img
height={heightImg} height={heightImg}
width={widthImg} width={widthImg}
@ -91,51 +92,9 @@ export const DropZone = ({ text, sx, heightImg, widthImg, onFileChange, onDelete
style={{ style={{
objectFit: "scale-down", objectFit: "scale-down",
}} }}
alt="img"
/> />
<IconButton :
onClick={onDeleteClick} <>
sx={{
position: "absolute",
right: 0,
top: 0,
color: theme.palette.orange.main,
fontSize: "16px",
lineHeight: "19px",
cursor: "pointer",
}}
>
<DeleteIcon />
</IconButton>
</Box>
) : (
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
<input
onChange={(event) => imgHC(event.target)}
hidden
accept="image/*"
multiple
type="file"
/>
<Box
onDragEnter={dragenterHC}
onDragExit={dragexitHC}
onDrop={dropHC}
onDragOver={dragOverHC}
sx={{
width: "100%",
height: "120px",
position: "relative",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: theme.palette.background.default,
border: `1px solid ${ready ? "red" : theme.palette.grey2.main}`,
borderRadius: "8px",
opacity: imageUrl ? "0.5" : 1,
...sx,
}}
>
<UploadIcon /> <UploadIcon />
<Typography <Typography
sx={{ sx={{
@ -150,7 +109,25 @@ export const DropZone = ({ text, sx, heightImg, widthImg, onFileChange, onDelete
> >
{text} {text}
</Typography> </Typography>
</Box> </>
}
</ButtonBase> </ButtonBase>
{imageUrl &&
<IconButton
onClick={onDeleteClick}
sx={{
position: "absolute",
right: 0,
top: 0,
color: theme.palette.orange.main,
borderRadius: "8px",
borderBottomRightRadius: 0,
borderTopLeftRadius: 0,
}}
>
<DeleteIcon />
</IconButton>
}
</Box>
); );
}; };

@ -1,69 +0,0 @@
import { create } from "zustand";
import { devtools } from "zustand/middleware";
type CropModalStore = {
imageBlob: Blob | null;
originalImageUrl: string | null;
};
export const initialState: CropModalStore = {
imageBlob: null,
originalImageUrl: null,
};
export const useCropModalStore = create<CropModalStore>()(
devtools(
() => initialState,
{
name: "CropModalStore",
enabled: process.env.NODE_ENV === "development",
trace: process.env.NODE_ENV === "development",
}
),
);
export const setCropModal = async (
imageBlob: Blob | string | null,
originalImageUrl: string | null | undefined,
) => {
if (typeof imageBlob === "string") {
const response = await fetch(imageBlob);
imageBlob = await response.blob();
}
useCropModalStore.setState({
imageBlob,
originalImageUrl: originalImageUrl ?? null,
}, false, {
type: "setCropModal",
imageBlob,
originalImageUrl,
});
};
export const closeCropModal = () => useCropModalStore.setState(
initialState,
false,
"closeCropModal"
);
export const setCropModalImageBlob = async (image: Blob | string | null) => {
if (typeof image === "string") {
const response = await fetch(image);
image = await response.blob();
}
useCropModalStore.setState({
imageBlob: image,
}, false, {
type: "setCropModalImageBlob",
image,
});
};
export const setCropModalOriginalImageUrl = (originalImageUrl: string | null | undefined) => useCropModalStore.setState(
{ originalImageUrl: originalImageUrl ?? null },
false,
"setCropModalOriginalImageUrl"
);

@ -16,7 +16,6 @@ import { FC, useMemo, useRef, useState } from "react";
import ReactCrop, { Crop, PixelCrop } from "react-image-crop"; import ReactCrop, { Crop, PixelCrop } from "react-image-crop";
import "react-image-crop/dist/ReactCrop.css"; import "react-image-crop/dist/ReactCrop.css";
import { canvasPreview } from "./utils/canvasPreview"; import { canvasPreview } from "./utils/canvasPreview";
import { setCropModalImageBlob, useCropModalStore } from "@root/cropModal";
const styleSlider: SxProps<Theme> = { const styleSlider: SxProps<Theme> = {
@ -45,19 +44,20 @@ const styleSlider: SxProps<Theme> = {
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
imageBlob: Blob | null;
originalImageUrl: string | null;
setCropModalImageBlob: (imageBlob: Blob) => void;
onClose: () => void; onClose: () => void;
onSaveImageClick?: (imageBlob: Blob) => void; onSaveImageClick: (imageBlob: Blob) => void;
} }
export const CropModal: FC<Props> = ({isOpen, onSaveImageClick, onClose }) => { export const CropModal: FC<Props> = ({ isOpen, imageBlob, originalImageUrl, setCropModalImageBlob, onSaveImageClick, onClose }) => {
const theme = useTheme(); const theme = useTheme();
const [crop, setCrop] = useState<Crop>(); const [crop, setCrop] = useState<Crop>();
const [completedCrop, setCompletedCrop] = useState<PixelCrop>(); const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
const imageBlob = useCropModalStore(state => state.imageBlob);
const originalImageUrl = useCropModalStore(state => state.originalImageUrl);
const [darken, setDarken] = useState(0); const [darken, setDarken] = useState(0);
const [rotate, setRotate] = useState(0); const [rotate, setRotate] = useState(0);
const [width, setWidth] = useState<number>(0); const [width, setWidth] = useState<number>(240);
const cropImageElementRef = useRef<HTMLImageElement>(null); const cropImageElementRef = useRef<HTMLImageElement>(null);
const isMobile = useMediaQuery(theme.breakpoints.down(786)); const isMobile = useMediaQuery(theme.breakpoints.down(786));
@ -99,7 +99,6 @@ export const CropModal: FC<Props> = ({isOpen, onSaveImageClick, onClose }) => {
const response = await fetch(originalImageUrl); const response = await fetch(originalImageUrl);
const blob = await response.blob(); const blob = await response.blob();
onSaveImageClick?.(blob);
setCropModalImageBlob(blob); setCropModalImageBlob(blob);
setCrop(undefined); setCrop(undefined);
setCompletedCrop(undefined); setCompletedCrop(undefined);
@ -126,7 +125,7 @@ export const CropModal: FC<Props> = ({isOpen, onSaveImageClick, onClose }) => {
return ( return (
<Modal <Modal
open={ isOpen} open={isOpen}
onClose={onClose} onClose={onClose}
aria-labelledby="modal-modal-title" aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description" aria-describedby="modal-modal-description"
@ -297,3 +296,35 @@ export const CropModal: FC<Props> = ({isOpen, onSaveImageClick, onClose }) => {
</Modal> </Modal>
); );
}; };
export function useCropModalState(initialOpenState = false) {
const [isCropModalOpen, setOpened] = useState(initialOpenState);
const [imageBlob, setCropModalImageBlob] = useState<Blob | null>(null);
const [originalImageUrl, setOriginalImageUrl] = useState<string | null>(null);
const closeCropModal = () => {
setOpened(false);
setCropModalImageBlob(null);
setOriginalImageUrl(null);
};
async function openCropModal(image: Blob | string, originalImageUrl: string | null | undefined = null) {
if (typeof image === "string") {
const response = await fetch(image);
image = await response.blob();
}
setCropModalImageBlob(image);
setOriginalImageUrl(originalImageUrl);
setOpened(true);
}
return {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
setCropModalImageBlob,
originalImageUrl,
} as const;
}