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

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

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

@ -1,172 +1,172 @@
import {
Typography,
Box,
useTheme,
ButtonBase,
Modal,
TextField,
InputAdornment,
Typography,
Box,
useTheme,
ButtonBase,
Modal,
TextField,
InputAdornment,
} from "@mui/material";
import UploadIcon from "../../../assets/icons/UploadIcon";
import SearchIcon from "../../../assets/icons/SearchIcon";
import * as React from "react";
import UnsplashIcon from "../../../assets/icons/Unsplash.svg";
import { useRef, useState, type DragEvent } from "react";
import type { DragEvent } from "react";
interface ModalkaProps {
isOpen: boolean;
onClose: () => void;
imgHC: (imgInp: FileList | null) => void;
handleImageChange: (file: File) => void;
}
export const UploadImageModal: React.FC<ModalkaProps> = ({
imgHC,
isOpen,
onClose,
handleImageChange,
isOpen,
onClose,
}) => {
const theme = useTheme();
const theme = useTheme();
const dropZone = useRef<HTMLDivElement>(null);
const [ready, setReady] = useState(false);
const dropZone = React.useRef<HTMLDivElement>(null);
const [ready, setReady] = React.useState(false);
const handleDragEnter = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setReady(true);
};
const handleDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setReady(true);
};
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
const file = event.dataTransfer.files[0];
if (!file) return;
imgHC(event.dataTransfer.files);
};
handleImageChange(file);
};
return (
<Modal
open={isOpen}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "690px",
bgcolor: "background.paper",
borderRadius: "12px",
boxShadow: 24,
p: 0,
overflow: "hidden",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: "20px",
background: theme.palette.background.default,
}}
return (
<Modal
open={isOpen}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Typography
sx={{ marginBottom: "20px", fontWeight: "bold", color: "#4D4D4D" }}
>
Добавьте изображение
</Typography>
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
<input
onChange={(event) => imgHC(event.target.files)}
hidden
accept="image/*"
multiple
type="file"
data-cy="upload-image-input"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) =>
event.preventDefault()
}
onDrop={handleDrop}
ref={dropZone}
sx={{
width: "580px",
padding: "33px 10px 33px 55px",
display: "flex",
alignItems: "center",
backgroundColor: theme.palette.background.default,
border: `1px solid ${ready ? "red" : theme.palette.grey2.main}`,
borderRadius: "8px",
gap: "55px",
}}
onDragEnter={handleDragEnter} // Применяем обработчик onDragEnter напрямую
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "690px",
bgcolor: "background.paper",
borderRadius: "12px",
boxShadow: 24,
p: 0,
overflow: "hidden",
}}
>
<UploadIcon />
<Box>
<Typography sx={{ color: "#9A9AAF", fontWeight: "bold" }}>
Загрузите или перетяните сюда файл
</Typography>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Принимает JPG, PNG, и GIF формат максимум 5mb
</Typography>
</Box>
</Box>
</ButtonBase>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
margin: "20px 0",
}}
>
<Typography
sx={{
fontWeight: "bold",
color: "#4D4D4D",
}}
>
Или выберите на фотостоке
</Typography>
<img src={UnsplashIcon} alt="" />
</Box>
<TextField
id="search-in-unsplash"
placeholder="Ищите изображения на английском языка"
sx={{
"& .MuiInputBase-input": {
height: "48px",
padding: "0 10px 0 0",
},
"& .MuiOutlinedInput-notchedOutline": {
borderRadius: "8px",
},
"& .Mui-focused .MuiOutlinedInput-notchedOutline": {
border: "1px solid rgba(0, 0, 0, 0.23)",
},
"& .MuiInputBase-root.MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline":
{
borderColor: "rgba(0, 0, 0, 0.23)",
},
}}
InputProps={{
startAdornment: (
<InputAdornment
position="start"
sx={{
outline: "none",
"& svg > path": { stroke: "#9A9AAF" },
}}
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: "20px",
background: theme.palette.background.default,
}}
>
<SearchIcon />
</InputAdornment>
),
}}
/>
</Box>
</Box>
</Modal>
);
<Typography
sx={{ marginBottom: "20px", fontWeight: "bold", color: "#4D4D4D" }}
>
Добавьте изображение
</Typography>
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
<input
onChange={(event) => event.target.files?.[0] && handleImageChange(event.target.files[0])}
hidden
accept="image/*"
multiple
type="file"
data-cy="upload-image-input"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) =>
event.preventDefault()
}
onDrop={handleDrop}
ref={dropZone}
sx={{
width: "580px",
padding: "33px 10px 33px 55px",
display: "flex",
alignItems: "center",
backgroundColor: theme.palette.background.default,
border: `1px solid ${ready ? "red" : theme.palette.grey2.main}`,
borderRadius: "8px",
gap: "55px",
}}
onDragEnter={handleDragEnter} // Применяем обработчик onDragEnter напрямую
>
<UploadIcon />
<Box>
<Typography sx={{ color: "#9A9AAF", fontWeight: "bold" }}>
Загрузите или перетяните сюда файл
</Typography>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Принимает JPG, PNG, и GIF формат максимум 5mb
</Typography>
</Box>
</Box>
</ButtonBase>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
margin: "20px 0",
}}
>
<Typography
sx={{
fontWeight: "bold",
color: "#4D4D4D",
}}
>
Или выберите на фотостоке
</Typography>
<img src={UnsplashIcon} alt="" />
</Box>
<TextField
id="search-in-unsplash"
placeholder="Ищите изображения на английском языка"
sx={{
"& .MuiInputBase-input": {
height: "48px",
padding: "0 10px 0 0",
},
"& .MuiOutlinedInput-notchedOutline": {
borderRadius: "8px",
},
"& .Mui-focused .MuiOutlinedInput-notchedOutline": {
border: "1px solid rgba(0, 0, 0, 0.23)",
},
"& .MuiInputBase-root.MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline":
{
borderColor: "rgba(0, 0, 0, 0.23)",
},
}}
InputProps={{
startAdornment: (
<InputAdornment
position="start"
sx={{
outline: "none",
"& svg > path": { stroke: "#9A9AAF" },
}}
>
<SearchIcon />
</InputAdornment>
),
}}
/>
</Box>
</Box>
</Modal>
);
};

@ -1,14 +1,11 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { Box, ButtonBase, Typography, useTheme } from "@mui/material";
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 { setCropModal } from "@root/cropModal";
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
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 { UploadImageModal } from "./UploadImageModal";
type UploadImageProps = {
@ -17,34 +14,9 @@ type UploadImageProps = {
export default function UploadImage({ question }: UploadImageProps) {
const theme = useTheme();
const quizQid = useCurrentQuiz()?.qid;
const [isCropModalOpen, openCropModal, closeCropModal] = useDisclosure();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const quiz = useCurrentQuiz();
const handleImageUpload = async (files: FileList | 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;
});
}
if (!quiz) return null;
return (
<Box sx={{ padding: "20px" }}>
@ -58,36 +30,29 @@ export default function UploadImage({ question }: UploadImageProps) {
>
Загрузить изображение
</Typography>
<ButtonBase
onClick={openImageUploadModal}
sx={{
width: "100%",
maxWidth: "260px",
height: "120px",
<DropZone
text={"5 MB максимум"}
heightImg={"110px"}
sx={{ maxWidth: "300px", width: "100%" }}
imageUrl={question.content.back}
originalImageUrl={question.content.originalBack}
onImageUploadClick={file => {
uploadQuestionImage(question.id, quiz.qid, file, (question, url) => {
question.content.back = url;
question.content.originalBack = url;
});
}}
>
{question.content.back ?
<img
src={question.content.back}
alt="question background"
style={{
width: "100%",
height: "100%",
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} />
onDeleteClick={() => {
updateQuestion(question.id, question => {
question.content.back = null;
});
}}
onImageSaveClick={file => {
uploadQuestionImage(question.id, quiz.qid, file, (question, url) => {
question.content.back = url;
});
}}
/>
</Box>
);
}

@ -307,7 +307,14 @@ export default function StartPageSettings() {
heightImg={"110px"}
sx={{ maxWidth: "300px" }}
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) => {
quiz.config.startpage.background.desktop = url;
});
@ -368,7 +375,14 @@ export default function StartPageSettings() {
text={"5 MB максимум"}
heightImg={"110px"}
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) => {
quiz.config.startpage.background.mobile = url;
});
@ -464,7 +478,14 @@ export default function StartPageSettings() {
text={"5 MB максимум"}
heightImg={"110px"}
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) => {
quiz.config.startpage.background.mobile = url;
});
@ -539,7 +560,14 @@ export default function StartPageSettings() {
heightImg={"110px"}
sx={{ maxWidth: "300px" }}
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) => {
quiz.config.logo = url;
});
@ -620,7 +648,14 @@ export default function StartPageSettings() {
heightImg={"110px"}
sx={{ maxWidth: "300px" }}
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) => {
quiz.config.logo = url;
});

@ -11,7 +11,10 @@ import {
} from "@mui/material";
import { useCurrentQuiz } from "@root/quizes/hooks";
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 {
@ -19,138 +22,112 @@ interface Props {
sx?: SxProps<Theme>;
heightImg: string;
widthImg?: string;
onFileChange?: (file: File) => void;
onDeleteClick?: () => void;
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 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) => {
if (!quiz) return;
async function handleImageUpload(file: File) {
onImageUploadClick?.(file);
closeImageUploadModal();
openCropModal(file);
}
const file = imgInp.files?.[0];
if (!file) return;
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%",
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",
...sx,
}}
>
<img
height={heightImg}
width={widthImg}
src={imageUrl}
style={{
objectFit: "scale-down",
}}
alt="img"
return (
<Box sx={{
position: "relative",
width: "100%",
height: "120px",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px",
...sx,
}}>
<UploadImageModal
isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<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"
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={onImageSaveClick}
/>
<Box
onDragEnter={dragenterHC}
onDragExit={dragexitHC}
onDrop={dropHC}
onDragOver={dragOverHC}
<ButtonBase
onClick={imageUrl ? () => openCropModal(imageUrl) : openImageUploadModal}
sx={{
width: "100%",
height: "120px",
position: "relative",
height: "100%",
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 />
<Typography
{imageUrl ?
<img
height={heightImg}
width={widthImg}
src={imageUrl}
style={{
objectFit: "scale-down",
}}
/>
:
<>
<UploadIcon />
<Typography
sx={{
position: "absolute",
right: "10px",
bottom: "10px",
color: theme.palette.orange.main,
fontSize: "16px",
lineHeight: "19px",
textDecoration: "underline",
}}
>
{text}
</Typography>
</>
}
</ButtonBase>
{imageUrl &&
<IconButton
onClick={onDeleteClick}
sx={{
position: "absolute",
right: "10px",
bottom: "10px",
right: 0,
top: 0,
color: theme.palette.orange.main,
fontSize: "16px",
lineHeight: "19px",
textDecoration: "underline",
borderRadius: "8px",
borderBottomRightRadius: 0,
borderTopLeftRadius: 0,
}}
>
{text}
</Typography>
</Box>
</ButtonBase>
<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 "react-image-crop/dist/ReactCrop.css";
import { canvasPreview } from "./utils/canvasPreview";
import { setCropModalImageBlob, useCropModalStore } from "@root/cropModal";
const styleSlider: SxProps<Theme> = {
@ -45,19 +44,20 @@ const styleSlider: SxProps<Theme> = {
interface Props {
isOpen: boolean;
imageBlob: Blob | null;
originalImageUrl: string | null;
setCropModalImageBlob: (imageBlob: Blob) => 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 [crop, setCrop] = useState<Crop>();
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
const imageBlob = useCropModalStore(state => state.imageBlob);
const originalImageUrl = useCropModalStore(state => state.originalImageUrl);
const [darken, setDarken] = 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 isMobile = useMediaQuery(theme.breakpoints.down(786));
@ -99,7 +99,6 @@ export const CropModal: FC<Props> = ({isOpen, onSaveImageClick, onClose }) => {
const response = await fetch(originalImageUrl);
const blob = await response.blob();
onSaveImageClick?.(blob);
setCropModalImageBlob(blob);
setCrop(undefined);
setCompletedCrop(undefined);
@ -126,7 +125,7 @@ export const CropModal: FC<Props> = ({isOpen, onSaveImageClick, onClose }) => {
return (
<Modal
open={ isOpen}
open={isOpen}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
@ -296,4 +295,36 @@ export const CropModal: FC<Props> = ({isOpen, onSaveImageClick, onClose }) => {
</Box>
</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;
}