fix dropzone & image modals
This commit is contained in:
parent
7824fafc51
commit
0e1f9aab23
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user