From bb4775e18ec00ca6830807257bf64b6f0409feb8 Mon Sep 17 00:00:00 2001 From: Nastya Date: Sun, 9 Jun 2024 19:38:30 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D0=B0=D0=BB=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=20=D0=BD=D0=BE=D0=B2=D1=83=D1=8E=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=8B,=20=D0=BE=D1=82=D0=BA=D1=80=D1=8B=D0=B2=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D1=85=D0=B0=D1=80=D0=B4=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?=D0=BE=D0=BC.=20=D0=9F=D0=BE=D0=BA=D0=B0=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=B8=D1=80=D1=83=D0=B5?= =?UTF-8?q?=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/CropModal/CropModal.ts | 68 +++ .../IntegrationsModal/AmoCRMModal.tsx | 2 +- .../OptionsAndPicture/OptionsAndPicture.tsx | 39 +- .../OptionsPicture/OptionsPicture.tsx | 35 +- src/pages/startPage/EditPage.tsx | 18 +- src/pages/startPage/dropZone.tsx | 33 +- src/ui_kit/MediaSelectionAndDisplay.tsx | 36 +- src/ui_kit/Modal/CropModal/CropGeneral.tsx | 276 ++++++++++++ src/ui_kit/Modal/CropModal/CropModal.tsx | 409 +++--------------- .../Modal/CropModal/NavigationPanel.tsx | 111 +++++ src/ui_kit/Modal/CropModal/SwitchCaseCrop.tsx | 116 ----- src/ui_kit/Modal/CropModal/WorkSpace.tsx | 109 +++++ src/ui_kit/Modal/CropModal/cropGeneral.tsx | 217 ---------- src/ui_kit/Modal/CropModal/index.tsx | 208 +++++++++ src/ui_kit/Modal/CropModal/utilities.ts | 0 .../utils/imageManipulation.ts | 0 16 files changed, 902 insertions(+), 775 deletions(-) create mode 100644 src/model/CropModal/CropModal.ts create mode 100644 src/ui_kit/Modal/CropModal/CropGeneral.tsx create mode 100644 src/ui_kit/Modal/CropModal/NavigationPanel.tsx delete mode 100644 src/ui_kit/Modal/CropModal/SwitchCaseCrop.tsx create mode 100644 src/ui_kit/Modal/CropModal/WorkSpace.tsx delete mode 100644 src/ui_kit/Modal/CropModal/cropGeneral.tsx create mode 100644 src/ui_kit/Modal/CropModal/index.tsx create mode 100644 src/ui_kit/Modal/CropModal/utilities.ts rename src/ui_kit/Modal/{ => CropModal}/utils/imageManipulation.ts (100%) diff --git a/src/model/CropModal/CropModal.ts b/src/model/CropModal/CropModal.ts new file mode 100644 index 00000000..62287fa7 --- /dev/null +++ b/src/model/CropModal/CropModal.ts @@ -0,0 +1,68 @@ +import type { PercentCrop } from "react-image-crop"; + +export type CropOnOpenType = { + originalImageUrl?:string; + imageBlob?: Blob; + editedUrlImagesList?: Record, string>; + questionId: string; + questionType: AcceptedQuestionTypes; + quizId: string; + + selfClose?: () => void; + setPictureUploading?: (is: boolean) => void; +} +export interface CropModalProps { + editedImages: EditedImages; + workSpaceTypes: WorkSpaceModel[]; + originalImageUrl: string; + + setEditedImages: (callback: (editedImages: EditedImages) => EditedImages) => void; + onSaveImageClick: () => void; + closeCropModal: CropOnCloseType; + onDeleteClick:CropOnDeleteIamgeClick +}; + +export type AcceptedQuestionTypes = "images" | "varimg" | "text" | "variant" | "result"; + + +export type CropOnCloseType = () => void; +export type CropOnDeleteIamgeClick = (callback: () => void) => void; + +export type EditedImages = Record, EditedImage> + +export type EditedImage = { + step: string, + url: string, + newRules: EditedImageNewRules +} + +export type WorkSpaceTypesList = Record + +export type WorkSpaceModel = { + step: Partial, + ratio: CropAspectRatio +}; + +export type CropAspectRatio = { + width: number; + height: number; +}; + +export type ScreenStepsTypes = "desktop" | "tablet" | "mobile" | "small"; + +export type EditedImageNewRules = { + crop: PercentCrop, + darken: number, + rotate: number, +} +export const DEFAULTCROPRULES = { + crop: { + x: 0, + y: 0, + width: 0, + height: 0, + unit: "%" as "%", + }, + rotate: 0, + darken: 0, +} \ No newline at end of file diff --git a/src/pages/IntegrationsPage/IntegrationsModal/AmoCRMModal.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoCRMModal.tsx index 3d5da094..312591a8 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/AmoCRMModal.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/AmoCRMModal.tsx @@ -71,7 +71,7 @@ export const AmoCRMModal: FC = ({ company: [], buyers: [], }); - const [tags, setTags] = useState({ + const [taщgs, setTags] = useState({ deal: [], contact: [], company: [], diff --git a/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx b/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx index ec6e1884..159763cf 100644 --- a/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx +++ b/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx @@ -5,7 +5,6 @@ import { uploadQuestionImage, } from "@root/questions/actions"; import { useCurrentQuiz } from "@root/quizes/hooks"; -import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal/CropModal"; import { useEffect, useState } from "react"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg"; @@ -15,6 +14,9 @@ import ImageEditAnswerItem from "../AnswerDraggableList/ImageEditAnswerItem"; import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict"; import { UploadImageModal } from "../UploadImage/UploadImageModal"; import SwitchOptionsAndPict from "./switchOptionsAndPict"; +import { CropModalInit } from "@/ui_kit/Modal/CropModal"; + +import imge from "@/assets/card-1.png" interface Props { question: QuizQuestionVarImg; @@ -34,16 +36,7 @@ export default function OptionsAndPicture({ const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(790)); const quizQid = useCurrentQuiz()?.qid; - const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = - useDisclosure(); - const { - isCropModalOpen, - openCropModal, - closeCropModal, - imageBlob, - originalImageUrl, - setCropModalImageBlob, - } = useCropModalState(); + const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure(); const handleImageUpload = async (file: File) => { if (!selectedVariantId) return; @@ -67,7 +60,6 @@ export default function OptionsAndPicture({ }, ); closeImageUploadModal(); - openCropModal(file, url); setPictureUploading(false); }; @@ -107,7 +99,7 @@ export default function OptionsAndPicture({ largeCheck={question.content.largeCheck} variant={variant} isMobile={isMobile} - openCropModal={openCropModal} + openCropModal={() => {}} openImageUploadModal={openImageUploadModal} pictureUploding={pictureUploding} setSelectedVariantId={setSelectedVariantId} @@ -119,18 +111,17 @@ export default function OptionsAndPicture({ onClose={closeImageUploadModal} handleImageChange={handleImageUpload} /> - { - if (selectedVariantId) - clearQuestionImages(question.id, selectedVariantId); + { if (!selectedVariantId) return; @@ -70,7 +63,6 @@ export default function OptionsPicture({ ); closeImageUploadModal(); - openCropModal(file, url); setPictureUploading(false); }; @@ -104,7 +96,7 @@ export default function OptionsPicture({ largeCheck={question.content.largeCheck} variant={variant} isMobile={isMobile} - openCropModal={openCropModal} + openCropModal={()=>{}} openImageUploadModal={openImageUploadModal} pictureUploding={pictureUploding} setSelectedVariantId={setSelectedVariantId} @@ -116,18 +108,17 @@ export default function OptionsPicture({ onClose={closeImageUploadModal} handleImageChange={handleImageUpload} /> - { - if (selectedVariantId) - clearQuestionImages(question.id, selectedVariantId); + - + )} diff --git a/src/pages/startPage/dropZone.tsx b/src/pages/startPage/dropZone.tsx index e3aadfd5..bff10630 100644 --- a/src/pages/startPage/dropZone.tsx +++ b/src/pages/startPage/dropZone.tsx @@ -10,10 +10,11 @@ import { useTheme, } from "@mui/material"; import { useCurrentQuiz } from "@root/quizes/hooks"; -import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal/CropModal"; import { enqueueSnackbar } from "notistack"; import { UploadImageModal } from "../../pages/Questions/UploadImage/UploadImageModal"; import { useDisclosure } from "../../utils/useDisclosure"; +import imge from "@/assets/card-1.png" +import { CropModalInit } from "@/ui_kit/Modal/CropModal"; const allowedFileTypes = ["image/png", "image/jpeg", "image/gif"]; @@ -49,13 +50,6 @@ export const DropZone = ({ const [isDropReady, setIsDropReady] = useState(false); const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure(); - const { - isCropModalOpen, - openCropModal, - closeCropModal, - imageBlob, - setCropModalImageBlob, - } = useCropModalState(); if (!quiz) return null; @@ -67,7 +61,6 @@ export const DropZone = ({ onImageUploadClick?.(file); closeImageUploadModal(); - openCropModal(file); } const onDrop = (event: React.DragEvent) => { @@ -101,18 +94,20 @@ export const DropZone = ({ onClose={closeImageUploadModal} handleImageChange={handleImageUpload} /> - openCropModal(imageUrl) : openImageUploadModal + onClick={ openImageUploadModal } sx={{ width: "100%", diff --git a/src/ui_kit/MediaSelectionAndDisplay.tsx b/src/ui_kit/MediaSelectionAndDisplay.tsx index 686f0868..7db03546 100644 --- a/src/ui_kit/MediaSelectionAndDisplay.tsx +++ b/src/ui_kit/MediaSelectionAndDisplay.tsx @@ -9,7 +9,6 @@ import { useTheme, } from "@mui/material"; import { updateQuestion, uploadQuestionImage } from "@root/questions/actions"; -import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal/CropModal"; import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton"; import { UploadImageModal } from "../pages/Questions/UploadImage/UploadImageModal"; @@ -21,6 +20,9 @@ import UploadIcon from "@icons/UploadIcon"; import InfoIcon from "@icons/InfoIcon"; import { VideoElement } from "../pages/startPage/VideoElement"; +import imge from "@/assets/card-1.png" +import { CropModalInit } from "./Modal/CropModal"; + interface Iprops { resultData: AnyTypedQuizQuestion; cropAspectRatio: { @@ -37,14 +39,6 @@ export const MediaSelectionAndDisplay: FC = ({ const [backgroundUploding, setBackgroundUploading] = useState(false); const quizQid = useCurrentQuiz()?.qid; const theme = useTheme(); - const { - isCropModalOpen, - openCropModal, - closeCropModal, - imageBlob, - originalImageUrl, - setCropModalImageBlob, - } = useCropModalState(); const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure(); async function handleImageUpload(file: File) { @@ -60,7 +54,6 @@ export const MediaSelectionAndDisplay: FC = ({ }, ); closeImageUploadModal(); - openCropModal(file, url); setPictureUploading(false); } @@ -134,20 +127,17 @@ export const MediaSelectionAndDisplay: FC = ({ onClose={closeImageUploadModal} handleImageChange={handleImageUpload} /> - { - updateQuestion(resultData.id, (question) => { - question.content.back = null; - question.content.originalBack = null; - }); + diff --git a/src/ui_kit/Modal/CropModal/CropGeneral.tsx b/src/ui_kit/Modal/CropModal/CropGeneral.tsx new file mode 100644 index 00000000..6f2169c6 --- /dev/null +++ b/src/ui_kit/Modal/CropModal/CropGeneral.tsx @@ -0,0 +1,276 @@ +import { FC, useCallback, useRef, useState } from "react"; +import { enqueueSnackbar } from "notistack"; +import ReactCrop, { + PercentCrop, + centerCrop, + convertToPixelCrop, + makeAspectCrop, + type Crop +} from "react-image-crop"; +import { + getModifiedImageBlob, + getRotatedImageBlob, +} from "./utils/imageManipulation"; + +import { + Box, + IconButton, + Slider, + SxProps, + Theme, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { ResetIcon } from "@icons/ResetIcon"; +import "react-image-crop/dist/ReactCrop.css"; +import { EditedImagesChangeType } from "./CropModal"; +import { CropAspectRatio, DEFAULTCROPRULES, EditedImage } from "@/model/CropModal/CropModal"; + +const styleSlider: SxProps = { + color: "#7E2AEA", + height: "12px", + "& .MuiSlider-track": { + border: "none", + }, + "& .MuiSlider-rail": { + backgroundColor: "#F2F3F7", + border: `1px solid #9A9AAF`, + }, + "& .MuiSlider-thumb": { + height: 26, + width: 26, + border: `6px solid #7E2AEA`, + backgroundColor: "white", + boxShadow: `0px 0px 0px 3px white, + 0px 4px 4px 3px #C3C8DD`, + "&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": { + boxShadow: `0px 0px 0px 3px white, + 0px 4px 4px 3px #C3C8DD`, + }, + }, +}; + +interface Props { + editedImage: EditedImage; + cropAspectRatio: CropAspectRatio; + editedImagesChange: EditedImagesChangeType; +}; + +export const CropGeneral: FC = ({ + editedImage, + cropAspectRatio, + editedImagesChange, +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(786)); + + const cropImageElementRef = useRef(null); + + const [imageWidth, setImageWidth] = useState(null); + const [imageHeight, setImageHeight] = useState(null); + + + async function handleRotateClick() { + editedImagesChange((old) => { + const newRotate = old.newRules.rotate + 90; + + return { + newRules: { + ...old.newRules, + rotate: newRotate > 360 ? 0 : newRotate + } + }; + }); + } + + function handleSizeChange(value: number) { + editedImagesChange((old) => { + if (!imageWidth || !imageHeight) return old; + + const crop = makeAspectCrop( + { + unit: "%", + width: value, + x: 0, + y: 0, + }, + cropAspectRatio ? cropAspectRatio.width / cropAspectRatio.height : 1, + imageWidth, + imageHeight, + ); + +//Хз зачем это было нужно, как будет работать - перетещу + + // if (!old.newRules.crop || old.newRules.crop.height === 0 || old.newRules.crop.width === 0) { + // return centerCrop(crop, imageWidth, imageHeight); + // } + + crop.x = Math.min( + 100 - crop.width, + Math.max(0, old.newRules.crop.x + (old.newRules.crop.width - crop.width) / 2), + ); + crop.y = Math.min( + 100 - crop.height, + Math.max(0, old.newRules.crop.y + (old.newRules.crop.height - crop.height) / 2), + ); + + return { + newRules: { + ...old.newRules, + crop + } + }; + }); + } + + return ( + <> + + editedImagesChange((old) => ({ + newRules: { + ...old.newRules, + crop: percentCrop + } + }))} + minWidth={5} + minHeight={5} + locked + aspect={ + cropAspectRatio + ? cropAspectRatio.width / cropAspectRatio.height + : undefined + } + > + { + setImageWidth(e.currentTarget.naturalWidth); + setImageHeight(e.currentTarget.naturalHeight); + + if (cropImageElementRef.current) { + editedImagesChange((old) => ({ + newRules: { + ...old.newRules, + crop: getInitialCrop( + cropImageElementRef.current?.width, + cropImageElementRef.current?.height, + cropAspectRatio + ? cropAspectRatio.width / cropAspectRatio.height + : 1, + ) + } + })) + } + }} + ref={cropImageElementRef} + alt="Crop me" + src={editedImage.url} + style={{ + filter: `brightness(${100 - editedImage.newRules.darken}%)`, + maxWidth: "100%", + maxHeight: "320px", + display: "block", + objectFit: "contain", + }} + /> + + + + + + + + + Размер + + { + if (typeof newValue === "number") handleSizeChange(newValue); + }} + /> + + + + Затемнение + + editedImagesChange((old) => ({ + newRules: { + ...old.newRules, + darken: newValue as number + } + }))} + /> + + + + + ) +}; + +function getInitialCrop( + imageWidth: number | null | undefined, + imageHeight: number | null | undefined, + aspectRatio: number, +): PercentCrop { + if (!imageHeight || !imageWidth) return DEFAULTCROPRULES.crop + const imageAspectRatio = imageWidth / imageHeight; + + return centerCrop( + { + width: + imageAspectRatio < aspectRatio + ? 100 + : (100 * aspectRatio) / imageAspectRatio, + height: + imageAspectRatio < aspectRatio + ? (100 * imageAspectRatio) / aspectRatio + : 100, + unit: "%", + x: 0, + y: 0, + }, + imageWidth, + imageHeight, + ); +} diff --git a/src/ui_kit/Modal/CropModal/CropModal.tsx b/src/ui_kit/Modal/CropModal/CropModal.tsx index 86a3e6c4..652c6008 100644 --- a/src/ui_kit/Modal/CropModal/CropModal.tsx +++ b/src/ui_kit/Modal/CropModal/CropModal.tsx @@ -1,202 +1,60 @@ -import { devlog } from "@frontend/kitui"; +import { FC, useEffect, useMemo, useRef, useState } from "react"; + +import WorkSpace from "./WorkSpace"; +import { NavigationPanel } from "./NavigationPanel"; import { Box, - Button, Modal, Typography, useMediaQuery, useTheme, } from "@mui/material"; -import { enqueueSnackbar } from "notistack"; -import { FC, useCallback, useMemo, useRef, useState } from "react"; -import ReactCrop, { - PercentCrop, - centerCrop, - convertToPixelCrop, - makeAspectCrop, -} from "react-image-crop"; -import "react-image-crop/dist/ReactCrop.css"; -import { isImageBlobAGifFile } from "@utils/isImageBlobAGifFile"; + import { - getModifiedImageBlob, - getRotatedImageBlob, -} from "../utils/imageManipulation"; -import DevaceMobileIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceMobileIcon"; -import BackArrowIcon from "@icons/BackArrowIcon"; -import DevaceDesktopIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceDesktopIcon"; -import DevaceTabletIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceTabletIcon"; -import DevaceSmallIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceSmallIcon"; -import SwitchCaseCrop from "@ui_kit/Modal/CropModal/SwitchCaseCrop"; + DEFAULTCROPRULES, + type CropModalProps, + type EditedImage, + type ScreenStepsTypes +} from "@/model/CropModal/CropModal"; -interface Props { - isOpen: boolean; - imageBlob: Blob | null; - originalImageUrl: string | null; - setCropModalImageBlob: (imageBlob: Blob) => void; - onClose: () => void; - onSaveImageClick: (imageBlob: Blob) => void; - onDeleteClick?: () => void; - cropAspectRatio?: { - width: number; - height: number; - }; -} -const stepsScreen: string[] = ["desktop", "tablet", "mobile", "small"] +export type EditedImagesChangeType = (changed: (old: EditedImage) => Partial) => void; -export const CropModal: FC = ({ - isOpen, - imageBlob, +export const CropModal: FC = ({ + editedImages, + workSpaceTypes, originalImageUrl, - setCropModalImageBlob, + + setEditedImages, onSaveImageClick, + closeCropModal, onDeleteClick, - onClose, - cropAspectRatio, }) => { const theme = useTheme(); - const [percentCrop, setPercentCrop] = useState( - undefined, - ); - - const [darken, setDarken] = useState(0); - const [imageWidth, setImageWidth] = useState(null); - const [imageHeight, setImageHeight] = useState(null); - const [modalStep, setModalStep] = useState(stepsScreen[0]); - const [lastStep, setLastStep] = useState(null) - const cropImageElementRef = useRef(null); const isMobile = useMediaQuery(theme.breakpoints.down(786)); - let stepIndex = stepsScreen.indexOf(modalStep) - const modalTitle = { - "desktop": {name: "Десктоп", icon: }, - "tablet": {name:"Планшет", icon: }, - "mobile": {name:"Телефон", icon: }, - "small": {name:"Самые узкие экраны", icon: } + + const [currentStep, setCurrentStep] = useState(1); + + const currentStepName: ScreenStepsTypes = useMemo(() => ( + workSpaceTypes[currentStep].step + ), [currentStep]) + + const editedImagesChange: EditedImagesChangeType = (changed) => { + setEditedImages(old => { + old[currentStepName] = { ...old[currentStepName], ...changed(old[currentStepName]) } + return old + }) } - - const handleNextStep = () => { - if (stepIndex === stepsScreen.length - 1) { - onClose() - return - } - if (stepIndex === stepsScreen.length - 2) { - setLastStep(true) - } - - let nextStepIndex = stepIndex + 1 - setModalStep(stepsScreen[nextStepIndex]); - }; - const handlePrevStep = () => { - - if (stepIndex === 0) return - if (stepIndex === stepsScreen.length - 1) { - setLastStep(false) - } - let nextStepIndex = stepIndex - 1 - - setModalStep(stepsScreen[nextStepIndex]); - }; - - const imageUrl = useMemo( - () => imageBlob && URL.createObjectURL(imageBlob), - [imageBlob], - ); - - function resetEditState() { - setPercentCrop(undefined); - setDarken(0); - } - - async function handleSaveModifiedImage() { - if (!percentCrop || !imageWidth || !imageHeight) return; - if (!cropImageElementRef.current) throw new Error("No image"); - - const width = cropImageElementRef.current.width; - const height = cropImageElementRef.current.height; - const pixelCrop = convertToPixelCrop(percentCrop, width, height); - - try { - const blob = await getModifiedImageBlob( - cropImageElementRef.current, - pixelCrop, - darken, - ); - - onSaveImageClick?.(blob); - resetEditState(); - onClose(); - } catch (error) { - devlog("getCroppedImageBlob error", error); - enqueueSnackbar("Не удалось изменить изображение"); - } - } - - async function handleRotateClick() { - if (!cropImageElementRef.current) throw new Error("No image"); - - try { - const blob = await getRotatedImageBlob(cropImageElementRef.current); - - setCropModalImageBlob(blob); - setPercentCrop(undefined); - } catch (error) { - devlog("getRotatedImageBlob error", error); - enqueueSnackbar("Не удалось изменить изображение"); - } - } - - async function handleSaveOriginalImage() { - if (!originalImageUrl) return; - - const response = await fetch(originalImageUrl) - const blob = await response.blob(); - - onSaveImageClick?.(blob); - resetEditState(); - onClose(); - } - - function handleSizeChange(value: number) { - setPercentCrop((prev) => { - if (!imageWidth || !imageHeight) return; - - const crop = makeAspectCrop( - { - unit: "%", - width: value, - x: 0, - y: 0, - }, - cropAspectRatio ? cropAspectRatio.width / cropAspectRatio.height : 1, - imageWidth, - imageHeight, - ); - - if (!prev || prev.height === 0 || prev.width === 0) { - return centerCrop(crop, imageWidth, imageHeight); - } - - crop.x = Math.min( - 100 - crop.width, - Math.max(0, prev.x + (prev.width - crop.width) / 2), - ); - crop.y = Math.min( - 100 - crop.height, - Math.max(0, prev.y + (prev.height - crop.height) / 2), - ); - - return crop; - }); + const resetImage = () => { + editedImagesChange(() => ({ + url: originalImageUrl, + newRules: DEFAULTCROPRULES, + })) } return ( { - resetEditState(); - onClose(); - }} - aria-labelledby="modal-modal-title" - aria-describedby="modal-modal-description" + open + onClose={closeCropModal} > = ({ transform: "translate(-50%, -50%)", bgcolor: "background.paper", boxShadow: 24, - borderRadius: "8px", + borderRadius: "12px", width: isMobile ? "343px" : "620px", height: isMobile ? "80vh" : undefined, display: isMobile ? "flex" : undefined, @@ -215,174 +73,37 @@ export const CropModal: FC = ({ overflow: isMobile ? "auto" : undefined, }} > - - - Настройте вариант отображения картинки на разных девайсах - - - - - - - - - + editedImagesChange={editedImagesChange} + onDeleteClick={onDeleteClick} + /> + - ); }; - -export function useCropModalState(initialOpenState = false) { - const [isCropModalOpen, setOpened] = useState(initialOpenState); - const [imageBlob, setCropModalImageBlob] = useState(null); - const [originalImageUrl, setOriginalImageUrl] = useState(null); - - const closeCropModal = useCallback(() => { - setOpened(false); - setCropModalImageBlob(null); - setOriginalImageUrl(null); - }, []); - - const openCropModal = useCallback( - async ( - image: Blob | string, - originalImageUrl: string | null | undefined = null, - ) => { - if (typeof image === "string") { - const response = await fetch(image, { - mode: 'no-cors', - }); - image = await response.blob(); - } - - const isGif = await isImageBlobAGifFile(image); - if (isGif) return; - - setCropModalImageBlob(image); - setOriginalImageUrl(originalImageUrl); - setOpened(true); - }, - [], - ); - - return { - isCropModalOpen, - openCropModal, - closeCropModal, - imageBlob, - setCropModalImageBlob, - originalImageUrl, - } as const; -} - -function getInitialCrop( - imageWidth: number, - imageHeight: number, - aspectRatio: number, -): PercentCrop { - const imageAspectRatio = imageWidth / imageHeight; - - return centerCrop( - { - width: - imageAspectRatio < aspectRatio - ? 100 - : (100 * aspectRatio) / imageAspectRatio, - height: - imageAspectRatio < aspectRatio - ? (100 * imageAspectRatio) / aspectRatio - : 100, - unit: "%", - x: 0, - y: 0, - }, - imageWidth, - imageHeight, - ); -} diff --git a/src/ui_kit/Modal/CropModal/NavigationPanel.tsx b/src/ui_kit/Modal/CropModal/NavigationPanel.tsx new file mode 100644 index 00000000..6cd5485e --- /dev/null +++ b/src/ui_kit/Modal/CropModal/NavigationPanel.tsx @@ -0,0 +1,111 @@ +import { FC } from "react" +import { + Box, + Button, + useMediaQuery, + useTheme, +} from "@mui/material"; +import BackArrowIcon from "@icons/BackArrowIcon"; + +interface Props { + currentStep: number; + setCurrentStep: (setp: number) => void; + totalSteps: number; + onSaveImageClick: () => void; + resetImage: () => void; +} + +export const NavigationPanel: FC = ({ + currentStep, + setCurrentStep, + totalSteps, + onSaveImageClick, + resetImage, +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(786)); + const lastStep = currentStep + 1 === totalSteps; + + const handlePrevStep = () => { + if (currentStep === 0) return; + setCurrentStep(currentStep - 1); + }; + const handleNextStep = () => { + if (lastStep) { + // onSaveImageClick(); + } else { + setCurrentStep(currentStep + 1); + } + }; + + return ( + + + + + + + + + ) +} \ No newline at end of file diff --git a/src/ui_kit/Modal/CropModal/SwitchCaseCrop.tsx b/src/ui_kit/Modal/CropModal/SwitchCaseCrop.tsx deleted file mode 100644 index 97614424..00000000 --- a/src/ui_kit/Modal/CropModal/SwitchCaseCrop.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import CropGeneral from "@ui_kit/Modal/CropModal/cropGeneral"; -import DevaceDesktopIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceDesktopIcon"; -import DevaceTabletIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceTabletIcon"; -import DevaceMobileIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceMobileIcon"; -import DevaceSmallIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceSmallIcon"; -import {PercentCrop} from "react-image-crop"; -import {MutableRefObject} from "react"; - -const modalProps = { - "desktop": {name: "Десктоп", icon: }, - "tablet": {name:"Планшет", icon: }, - "mobile": {name:"Телефон", icon: }, - "small": {name:"Самые узкие экраны", icon: } -} - -interface Props{ - imageUrl: null | string; - handleSizeChange: (a: number)=> void; - handleRotateClick: Promise; - getInitialCrop: (imageWidth: number, imageHeight: number, aspectRatio: number)=> PercentCrop; - modalProps: {string:{}}; - modalStep: string; - stepIndex: number; - onClose: () => void; - cropImageElementRef: MutableRefObject; - onDeleteClick?: () => void; - cropAspectRatio?: { - width: number; - height: number; - }; -} - -export default function SwitchCaseCrop ({imageUrl, - cropAspectRatio, - handleSizeChange, - handleRotateClick, - getInitialCrop, - onDeleteClick, - onClose, - modalStep, - stepIndex, - cropImageElementRef}: Props) { - switch (modalStep) { - case "desktop": { - return( - <> - - - - ) - } - case "tablet": { - return( - <> - - - - - ) - } - case "mobile": { - return( - - ) - } - case "small": { - return( - - ) - } - } -} \ No newline at end of file diff --git a/src/ui_kit/Modal/CropModal/WorkSpace.tsx b/src/ui_kit/Modal/CropModal/WorkSpace.tsx new file mode 100644 index 00000000..5b7470ba --- /dev/null +++ b/src/ui_kit/Modal/CropModal/WorkSpace.tsx @@ -0,0 +1,109 @@ +import { useMemo } from "react"; +import { + Box, + IconButton, + Typography, + useTheme, +} from "@mui/material"; +import { CropGeneral } from "./CropGeneral"; +import DevaceDesktopIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceDesktopIcon"; +import DevaceTabletIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceTabletIcon"; +import DevaceMobileIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceMobileIcon"; +import DevaceSmallIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceSmallIcon"; +import DeleteIcon from "@mui/icons-material/Delete"; + +import type { CropAspectRatio, CropOnDeleteIamgeClick, EditedImage, ScreenStepsTypes } from "@/model/CropModal/CropModal" +import { EditedImagesChangeType } from "./CropModal"; + + +const modalModels = { + "desktop": { name: "Десктоп", icon: }, + "tablet": { name: "Планшет", icon: }, + "mobile": { name: "Телефон", icon: }, + "small": { name: "Самые узкие экраны", icon: } +}; + +interface Props { + editedImage: EditedImage; + cropAspectRatio: CropAspectRatio; + currentStep: number; + currentStepName: ScreenStepsTypes; + + editedImagesChange: EditedImagesChangeType; + onDeleteClick: CropOnDeleteIamgeClick +}; + +export default function WorkSpace({ + editedImage, + currentStep, + currentStepName, + cropAspectRatio, + editedImagesChange, + onDeleteClick, +}: Props) { + const theme = useTheme(); + const currentModel = useMemo(() => ( + modalModels[currentStepName] + ), [currentStepName]); + + return ( + <> + + + + {currentModel.name} + {currentModel.icon} + + + + {currentStep + 1 + " шаг"} + + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/ui_kit/Modal/CropModal/cropGeneral.tsx b/src/ui_kit/Modal/CropModal/cropGeneral.tsx deleted file mode 100644 index 80657e5e..00000000 --- a/src/ui_kit/Modal/CropModal/cropGeneral.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import {Box, IconButton, Slider, SxProps, Theme, Typography, useMediaQuery, useTheme} from "@mui/material"; -import ReactCrop, {PercentCrop} from "react-image-crop"; -import {ResetIcon} from "@icons/ResetIcon"; -import DeleteIcon from "@mui/icons-material/Delete"; -import {MutableRefObject, useRef, useState} from "react"; - -interface Props{ - imageUrl: null | string; - handleSizeChange: (a: number)=> void; - handleRotateClick: Promise; - getInitialCrop: (imageWidth: number, imageHeight: number, aspectRatio: number)=> PercentCrop; - modalProps: any; - modalStep: number; - onClose: () => void; - cropImageElementRef: MutableRefObject; - onDeleteClick?: () => void; - cropAspectRatio?: { - width: number; - height: number; - }; -} - -const styleSlider: SxProps = { - color: "#7E2AEA", - height: "12px", - "& .MuiSlider-track": { - border: "none", - }, - "& .MuiSlider-rail": { - backgroundColor: "#F2F3F7", - border: `1px solid #9A9AAF`, - }, - "& .MuiSlider-thumb": { - height: 26, - width: 26, - border: `6px solid #7E2AEA`, - backgroundColor: "white", - boxShadow: `0px 0px 0px 3px white, - 0px 4px 4px 3px #C3C8DD`, - "&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": { - boxShadow: `0px 0px 0px 3px white, - 0px 4px 4px 3px #C3C8DD`, - }, - }, -}; - -export default function CropGeneral({imageUrl, - cropAspectRatio, - handleSizeChange, - handleRotateClick, - getInitialCrop, - modalProps, - onDeleteClick, - onClose, - modalStep, - cropImageElementRef}: Props) { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(786)); - - const [percentCrop, setPercentCrop] = useState( - undefined, - ); - const [imageWidth, setImageWidth] = useState(null); - const [imageHeight, setImageHeight] = useState(null); - const [darken, setDarken] = useState(0); - const step = modalStep + 1 - - return ( - <> - - - {modalProps.name} - {modalProps.icon} - - {onDeleteClick && ( - { - onDeleteClick?.(); - onClose(); - }} - sx={{ - height: "48px", - width: "48px", - p: 0, - color: theme.palette.orange.main, - borderRadius: "50%", - }} - > - - - )} - - - {step + " шаг"} - - {imageUrl && ( - setPercentCrop(percentCrop)} - minWidth={5} - minHeight={5} - locked - aspect={ - cropAspectRatio - ? cropAspectRatio.width / cropAspectRatio.height - : undefined - } - > - { - setImageWidth(e.currentTarget.naturalWidth); - setImageHeight(e.currentTarget.naturalHeight); - - if (cropImageElementRef.current) { - setPercentCrop( - getInitialCrop( - cropImageElementRef.current.width, - cropImageElementRef.current.height, - cropAspectRatio - ? cropAspectRatio.width / cropAspectRatio.height - : 1, - ), - ); - } - }} - ref={cropImageElementRef} - alt="Crop me" - src={imageUrl} - style={{ - filter: `brightness(${100 - darken}%)`, - maxWidth: "100%", - maxHeight: "320px", - display: "block", - objectFit: "contain", - }} - /> - - )} - - - - - - - - Размер - - { - if (typeof newValue === "number") handleSizeChange(newValue); - }} - /> - - - - Затемнение - - setDarken(newValue as number)} - /> - - - - - ) -} \ No newline at end of file diff --git a/src/ui_kit/Modal/CropModal/index.tsx b/src/ui_kit/Modal/CropModal/index.tsx new file mode 100644 index 00000000..d04f9a3e --- /dev/null +++ b/src/ui_kit/Modal/CropModal/index.tsx @@ -0,0 +1,208 @@ +import { FC, useEffect, useState } from "react"; +import { CropModal } from "./CropModal"; +import { + type ScreenStepsTypes, + type CropOnOpenType, + type WorkSpaceTypesList, + type EditedImages, + type CropOnDeleteIamgeClick, + DEFAULTCROPRULES, +} from "@/model/CropModal/CropModal" +import { isImageBlobAGifFile } from "@/utils/isImageBlobAGifFile"; + +const workSpaceTypesList: WorkSpaceTypesList = { + images: [ + { + step: "desktop", + ratio: { + width: 317, + height: 257 + } + }, + { + step: "tablet", + ratio: { + width: 455, + height: 257 + } + }, + { + step: "mobile", + ratio: { + width: 160, + height: 183 + } + }, + ], + varimg: [ + { + step: "desktop", + ratio: { + width: 450, + height: 450 + } + }, + { + step: "mobile", + ratio: { + width: 335, + height: 335, + } + }, + ], + text: [ + { + step: "desktop", + ratio: { + width: 450, + height: 450 + } + }, + { + step: "mobile", + ratio: { + width: 335, + height: 335, + } + }, + ], + variant: [ + { + step: "desktop", + ratio: { + width: 450, + height: 450 + } + }, + { + step: "mobile", + ratio: { + width: 335, + height: 335, + } + }, + ], + result: [ + { + step: "desktop", + ratio: { + width: 700, + height: 306 + } + }, + { + step: "mobile", + ratio: { + width: 335, + height: 236 + } + }, + ] +}; + +export const CropModalInit: FC = ({ + originalImageUrl, + imageBlob, + editedUrlImagesList, + questionId, + questionType, + quizId, + + selfClose, + setPictureUploading, +}) => { + + const [acceptedOriginalImageUrl, setOriginalImageUrl] = useState(""); + const [editedImages, setEditedImages] = useState({} as EditedImages); + + useEffect(() => { + //Если нам не дали с чем работать, то и работать не нужно + if (Boolean(imageBlob) || Boolean(originalImageUrl)) { + (async () => { + let newImageBlob = imageBlob; + if (originalImageUrl !== undefined) { + const response = await fetch(originalImageUrl, { + mode: 'no-cors', + }); + newImageBlob = await response.blob(); + }; + + if (newImageBlob) { + const isGif = await isImageBlobAGifFile(newImageBlob); + if (isGif) { + saveImagesAndRules(newImageBlob); + return; + } + //Для работы нам нужны урлы. Оригинальной и редактированных картинок + let newOriginalImageUrl = originalImageUrl || URL.createObjectURL(newImageBlob) + if (questionId) { + if (questionType) { + if (Boolean(editedUrlImagesList)) { + + const newEditedImagesList = {} as EditedImages; + + for (let key in editedUrlImagesList) { + newEditedImagesList[key as ScreenStepsTypes] = { + step: key, + url: editedUrlImagesList[key as ScreenStepsTypes], + newRules: DEFAULTCROPRULES + } + } + + + setOriginalImageUrl(newOriginalImageUrl); + setEditedImages(newEditedImagesList); + } + } else { + throw new Error("Не передан тип вопроса") + } + } else { + throw new Error("Не передан id вопроса") + } + } + })() + } + + }, []) + + const closeModal = () => { + + selfClose?.() + } + + const handleCropModalDeleteImageClick: CropOnDeleteIamgeClick = () => { + + //сохранить пустую строку и дефолтные настройки картинки в самом вопросе, не информируя БД о удалении картинки + selfClose?.() + }; + + const saveImagesAndRules = async (blob?: Blob) => { + + // if (!selectedVariantId) return; + + // uploadQuestionImage(questionId, quizId, imageBlob, (question, url) => { + // if (!("variants" in question.content)) return; + + // const variant = question.content.variants.find( + // (variant) => variant.id === selectedVariantId, + // ); + // if (!variant) return; + + // variant.extendedText = url; + // }); + }; + + if (acceptedOriginalImageUrl.length === 0) return <> + + return + +}; \ No newline at end of file diff --git a/src/ui_kit/Modal/CropModal/utilities.ts b/src/ui_kit/Modal/CropModal/utilities.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/ui_kit/Modal/utils/imageManipulation.ts b/src/ui_kit/Modal/CropModal/utils/imageManipulation.ts similarity index 100% rename from src/ui_kit/Modal/utils/imageManipulation.ts rename to src/ui_kit/Modal/CropModal/utils/imageManipulation.ts