From 0e1f9aab23f9d2ca822f701119e8bb455f4cc9fd Mon Sep 17 00:00:00 2001 From: nflnkr Date: Mon, 4 Dec 2023 14:57:54 +0300 Subject: [PATCH] fix dropzone & image modals --- .../OptionsAndPicture/OptionsAndPicture.tsx | 46 +-- .../OptionsPicture/OptionsPicture.tsx | 50 +-- .../Questions/PageOptions/PageOptions.tsx | 38 ++- .../UploadImage/UploadImageModal.tsx | 296 +++++++++--------- src/pages/Questions/UploadImage/index.tsx | 91 ++---- src/pages/startPage/StartPageSettings.tsx | 45 ++- src/pages/startPage/dropZone.tsx | 197 +++++------- src/stores/cropModal.ts | 69 ---- src/ui_kit/Modal/CropModal.tsx | 49 ++- 9 files changed, 425 insertions(+), 456 deletions(-) delete mode 100644 src/stores/cropModal.ts diff --git a/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx b/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx index fe2b14d0..c203f580 100644 --- a/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx +++ b/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx @@ -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) { )} /> - - + + (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) { )} /> - - + + { @@ -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) { Изображение - - + + или void; - imgHC: (imgInp: FileList | null) => void; + handleImageChange: (file: File) => void; } export const UploadImageModal: React.FC = ({ - imgHC, - isOpen, - onClose, + handleImageChange, + isOpen, + onClose, }) => { - const theme = useTheme(); + const theme = useTheme(); + const dropZone = useRef(null); + const [ready, setReady] = useState(false); - const dropZone = React.useRef(null); - const [ready, setReady] = React.useState(false); + const handleDragEnter = (event: DragEvent) => { + event.preventDefault(); + setReady(true); + }; - const handleDragEnter = (event: React.DragEvent) => { - event.preventDefault(); - setReady(true); - }; + const handleDrop = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); - const handleDrop = (event: DragEvent) => { - event.preventDefault(); - event.stopPropagation(); + const file = event.dataTransfer.files[0]; + if (!file) return; - imgHC(event.dataTransfer.files); - }; + handleImageChange(file); + }; - return ( - - - - - Добавьте изображение - - - imgHC(event.target.files)} - hidden - accept="image/*" - multiple - type="file" - data-cy="upload-image-input" - /> ) => - 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", + }} > - - - - Загрузите или перетяните сюда файл - - - Принимает JPG, PNG, и GIF формат — максимум 5mb - - - - - - - Или выберите на фотостоке - - - - path": { stroke: "#9A9AAF" }, - }} + - - - ), - }} - /> - - - - ); + + Добавьте изображение + + + event.target.files?.[0] && handleImageChange(event.target.files[0])} + hidden + accept="image/*" + multiple + type="file" + data-cy="upload-image-input" + /> + ) => + 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 напрямую + > + + + + Загрузите или перетяните сюда файл + + + Принимает JPG, PNG, и GIF формат — максимум 5mb + + + + + + + Или выберите на фотостоке + + + + path": { stroke: "#9A9AAF" }, + }} + > + + + ), + }} + /> + + + + ); }; diff --git a/src/pages/Questions/UploadImage/index.tsx b/src/pages/Questions/UploadImage/index.tsx index 9ec7e25f..cb9d47d6 100644 --- a/src/pages/Questions/UploadImage/index.tsx +++ b/src/pages/Questions/UploadImage/index.tsx @@ -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) => { - 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 ( @@ -58,36 +30,29 @@ export default function UploadImage({ question }: UploadImageProps) { > Загрузить изображение - { + uploadQuestionImage(question.id, quiz.qid, file, (question, url) => { + question.content.back = url; + question.content.originalBack = url; + }); }} - > - {question.content.back ? - question background - : - } - text="5 MB максимум" - /> - } - - - + onDeleteClick={() => { + updateQuestion(question.id, question => { + question.content.back = null; + }); + }} + onImageSaveClick={file => { + uploadQuestionImage(question.id, quiz.qid, file, (question, url) => { + question.content.back = url; + }); + }} + /> ); } diff --git a/src/pages/startPage/StartPageSettings.tsx b/src/pages/startPage/StartPageSettings.tsx index 2de92467..144d8b8c 100755 --- a/src/pages/startPage/StartPageSettings.tsx +++ b/src/pages/startPage/StartPageSettings.tsx @@ -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; }); diff --git a/src/pages/startPage/dropZone.tsx b/src/pages/startPage/dropZone.tsx index 70be382d..e659ba1c 100644 --- a/src/pages/startPage/dropZone.tsx +++ b/src/pages/startPage/dropZone.tsx @@ -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; 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) => { - 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) => { - event.preventDefault(); - }; - - return imageUrl ? ( - - img + - - - - - ) : ( - - imgHC(event.target)} - hidden - accept="image/*" - multiple - type="file" + - 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, }} > - - + : + <> + + + {text} + + + } + + {imageUrl && + - {text} - - - + + + } + ); }; diff --git a/src/stores/cropModal.ts b/src/stores/cropModal.ts deleted file mode 100644 index 08fefc7d..00000000 --- a/src/stores/cropModal.ts +++ /dev/null @@ -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()( - 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" -); diff --git a/src/ui_kit/Modal/CropModal.tsx b/src/ui_kit/Modal/CropModal.tsx index d6e64a98..218b3b10 100644 --- a/src/ui_kit/Modal/CropModal.tsx +++ b/src/ui_kit/Modal/CropModal.tsx @@ -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 = { @@ -45,19 +44,20 @@ const styleSlider: SxProps = { 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 = ({isOpen, onSaveImageClick, onClose }) => { +export const CropModal: FC = ({ isOpen, imageBlob, originalImageUrl, setCropModalImageBlob, onSaveImageClick, onClose }) => { const theme = useTheme(); const [crop, setCrop] = useState(); const [completedCrop, setCompletedCrop] = useState(); - 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(0); + const [width, setWidth] = useState(240); const cropImageElementRef = useRef(null); const isMobile = useMediaQuery(theme.breakpoints.down(786)); @@ -99,7 +99,6 @@ export const CropModal: FC = ({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 = ({isOpen, onSaveImageClick, onClose }) => { return ( = ({isOpen, onSaveImageClick, onClose }) => { ); -}; +}; + +export function useCropModalState(initialOpenState = false) { + const [isCropModalOpen, setOpened] = useState(initialOpenState); + const [imageBlob, setCropModalImageBlob] = useState(null); + const [originalImageUrl, setOriginalImageUrl] = useState(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; +}