модалка переделана под новую модель работы, открывается хардкодом. Пока не редактирует

This commit is contained in:
Nastya 2024-06-09 19:38:30 +03:00
parent 5fd2534216
commit bb4775e18e
16 changed files with 902 additions and 775 deletions

@ -0,0 +1,68 @@
import type { PercentCrop } from "react-image-crop";
export type CropOnOpenType = {
originalImageUrl?:string;
imageBlob?: Blob;
editedUrlImagesList?: Record<Partial<ScreenStepsTypes>, 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<Partial<ScreenStepsTypes>, EditedImage>
export type EditedImage = {
step: string,
url: string,
newRules: EditedImageNewRules
}
export type WorkSpaceTypesList = Record<AcceptedQuestionTypes, WorkSpaceModel[]>
export type WorkSpaceModel = {
step: Partial<ScreenStepsTypes>,
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,
}

@ -71,7 +71,7 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
company: [], company: [],
buyers: [], buyers: [],
}); });
const [tags, setTags] = useState<TTags>({ const [taщgs, setTags] = useState<TTags>({
deal: [], deal: [],
contact: [], contact: [],
company: [], company: [],

@ -5,7 +5,6 @@ import {
uploadQuestionImage, uploadQuestionImage,
} from "@root/questions/actions"; } from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal/CropModal";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg"; import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
@ -15,6 +14,9 @@ import ImageEditAnswerItem from "../AnswerDraggableList/ImageEditAnswerItem";
import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict"; import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict";
import { UploadImageModal } from "../UploadImage/UploadImageModal"; import { UploadImageModal } from "../UploadImage/UploadImageModal";
import SwitchOptionsAndPict from "./switchOptionsAndPict"; import SwitchOptionsAndPict from "./switchOptionsAndPict";
import { CropModalInit } from "@/ui_kit/Modal/CropModal";
import imge from "@/assets/card-1.png"
interface Props { interface Props {
question: QuizQuestionVarImg; question: QuizQuestionVarImg;
@ -34,16 +36,7 @@ export default function OptionsAndPicture({
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const quizQid = useCurrentQuiz()?.qid; const quizQid = useCurrentQuiz()?.qid;
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
useDisclosure();
const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
originalImageUrl,
setCropModalImageBlob,
} = useCropModalState();
const handleImageUpload = async (file: File) => { const handleImageUpload = async (file: File) => {
if (!selectedVariantId) return; if (!selectedVariantId) return;
@ -67,7 +60,6 @@ export default function OptionsAndPicture({
}, },
); );
closeImageUploadModal(); closeImageUploadModal();
openCropModal(file, url);
setPictureUploading(false); setPictureUploading(false);
}; };
@ -107,7 +99,7 @@ export default function OptionsAndPicture({
largeCheck={question.content.largeCheck} largeCheck={question.content.largeCheck}
variant={variant} variant={variant}
isMobile={isMobile} isMobile={isMobile}
openCropModal={openCropModal} openCropModal={() => {}}
openImageUploadModal={openImageUploadModal} openImageUploadModal={openImageUploadModal}
pictureUploding={pictureUploding} pictureUploding={pictureUploding}
setSelectedVariantId={setSelectedVariantId} setSelectedVariantId={setSelectedVariantId}
@ -119,18 +111,17 @@ export default function OptionsAndPicture({
onClose={closeImageUploadModal} onClose={closeImageUploadModal}
handleImageChange={handleImageUpload} handleImageChange={handleImageUpload}
/> />
<CropModal <CropModalInit
isOpen={isCropModalOpen} originalImageUrl={imge}
imageBlob={imageBlob} editedUrlImagesList={{
originalImageUrl={originalImageUrl} "desktop": imge,
setCropModalImageBlob={setCropModalImageBlob} "tablet": imge,
onClose={closeCropModal} "mobile": imge,
onSaveImageClick={handleCropModalSaveClick} "small": imge,
onDeleteClick={() => {
if (selectedVariantId)
clearQuestionImages(question.id, selectedVariantId);
}} }}
cropAspectRatio={{ width: 300, height: 300 }} questionId={question.backendId.toString()}
questionType={question.type}
quizId={quizQid}
/> />
<Box <Box
sx={{ sx={{

@ -4,7 +4,6 @@ import {
uploadQuestionImage, uploadQuestionImage,
} from "@root/questions/actions"; } from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal/CropModal";
import { useState } from "react"; import { useState } from "react";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import type { QuizQuestionImages } from "../../../model/questionTypes/images"; import type { QuizQuestionImages } from "../../../model/questionTypes/images";
@ -16,6 +15,8 @@ import ButtonsOptions from "../ButtonsOptions";
import { UploadImageModal } from "../UploadImage/UploadImageModal"; import { UploadImageModal } from "../UploadImage/UploadImageModal";
import SwitchAnswerOptionsPict from "./switchOptionsPict"; import SwitchAnswerOptionsPict from "./switchOptionsPict";
import imge from "@/assets/card-1.png"
import { CropModalInit } from "@/ui_kit/Modal/CropModal";
interface Props { interface Props {
question: QuizQuestionImages; question: QuizQuestionImages;
openBranchingPage: boolean; openBranchingPage: boolean;
@ -38,14 +39,6 @@ export default function OptionsPicture({
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] =
useDisclosure(); useDisclosure();
const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
originalImageUrl,
setCropModalImageBlob,
} = useCropModalState();
const handleImageUpload = async (file: File) => { const handleImageUpload = async (file: File) => {
if (!selectedVariantId) return; if (!selectedVariantId) return;
@ -70,7 +63,6 @@ export default function OptionsPicture({
); );
closeImageUploadModal(); closeImageUploadModal();
openCropModal(file, url);
setPictureUploading(false); setPictureUploading(false);
}; };
@ -104,7 +96,7 @@ export default function OptionsPicture({
largeCheck={question.content.largeCheck} largeCheck={question.content.largeCheck}
variant={variant} variant={variant}
isMobile={isMobile} isMobile={isMobile}
openCropModal={openCropModal} openCropModal={()=>{}}
openImageUploadModal={openImageUploadModal} openImageUploadModal={openImageUploadModal}
pictureUploding={pictureUploding} pictureUploding={pictureUploding}
setSelectedVariantId={setSelectedVariantId} setSelectedVariantId={setSelectedVariantId}
@ -116,18 +108,17 @@ export default function OptionsPicture({
onClose={closeImageUploadModal} onClose={closeImageUploadModal}
handleImageChange={handleImageUpload} handleImageChange={handleImageUpload}
/> />
<CropModal <CropModalInit
isOpen={isCropModalOpen} originalImageUrl={imge}
imageBlob={imageBlob} editedUrlImagesList={{
originalImageUrl={originalImageUrl} "desktop": imge,
setCropModalImageBlob={setCropModalImageBlob} "tablet": imge,
onClose={closeCropModal} "mobile": imge,
onSaveImageClick={handleCropModalSaveClick} "small": imge,
onDeleteClick={() => {
if (selectedVariantId)
clearQuestionImages(question.id, selectedVariantId);
}} }}
cropAspectRatio={{ width: 452, height: 300 }} questionId={question.backendId.toString()}
questionType={question.type}
quizId={quizQid}
/> />
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}> <Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Link <Link

@ -147,15 +147,15 @@ export default function EditPage({
{quizConfig && ( {quizConfig && (
<> <>
<Stepper activeStep={currentStep} /> <Stepper activeStep={currentStep} />
<SwitchStepPages <SwitchStepPages
activeStep={currentStep} activeStep={currentStep}
quizType={quizConfig.type} quizType={quizConfig.type}
quizResults={quizConfig.results} quizResults={quizConfig.results}
quizStartPageType={quizConfig.startpageType} quizStartPageType={quizConfig.startpageType}
openBranchingPage={openBranchingPage} openBranchingPage={openBranchingPage}
setOpenBranchingPage={setOpenBranchingPage} setOpenBranchingPage={setOpenBranchingPage}
widthMain={widthMain} widthMain={widthMain}
/> />
</> </>
)} )}
</Box> </Box>

@ -10,10 +10,11 @@ import {
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal/CropModal";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { UploadImageModal } from "../../pages/Questions/UploadImage/UploadImageModal"; import { UploadImageModal } from "../../pages/Questions/UploadImage/UploadImageModal";
import { useDisclosure } from "../../utils/useDisclosure"; 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"]; const allowedFileTypes = ["image/png", "image/jpeg", "image/gif"];
@ -49,13 +50,6 @@ export const DropZone = ({
const [isDropReady, setIsDropReady] = useState<boolean>(false); const [isDropReady, setIsDropReady] = useState<boolean>(false);
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] =
useDisclosure(); useDisclosure();
const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
setCropModalImageBlob,
} = useCropModalState();
if (!quiz) return null; if (!quiz) return null;
@ -67,7 +61,6 @@ export const DropZone = ({
onImageUploadClick?.(file); onImageUploadClick?.(file);
closeImageUploadModal(); closeImageUploadModal();
openCropModal(file);
} }
const onDrop = (event: React.DragEvent<HTMLDivElement>) => { const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
@ -101,18 +94,20 @@ export const DropZone = ({
onClose={closeImageUploadModal} onClose={closeImageUploadModal}
handleImageChange={handleImageUpload} handleImageChange={handleImageUpload}
/> />
<CropModal <CropModalInit
isOpen={isCropModalOpen} originalImageUrl={imge}
imageBlob={imageBlob} editedUrlImagesList={{
originalImageUrl={originalImageUrl} "desktop": imge,
setCropModalImageBlob={setCropModalImageBlob} "tablet": imge,
onClose={closeCropModal} "mobile": imge,
onSaveImageClick={onImageSaveClick} "small": imge,
cropAspectRatio={cropAspectRatio} }}
questionId={'2'}
questionType={"images"}
quizId={"12"}
/> />
<ButtonBase <ButtonBase
onClick={ onClick={ openImageUploadModal
imageUrl ? () => openCropModal(imageUrl) : openImageUploadModal
} }
sx={{ sx={{
width: "100%", width: "100%",

@ -9,7 +9,6 @@ import {
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions"; import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal/CropModal";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton"; import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import { UploadImageModal } from "../pages/Questions/UploadImage/UploadImageModal"; import { UploadImageModal } from "../pages/Questions/UploadImage/UploadImageModal";
@ -21,6 +20,9 @@ import UploadIcon from "@icons/UploadIcon";
import InfoIcon from "@icons/InfoIcon"; import InfoIcon from "@icons/InfoIcon";
import { VideoElement } from "../pages/startPage/VideoElement"; import { VideoElement } from "../pages/startPage/VideoElement";
import imge from "@/assets/card-1.png"
import { CropModalInit } from "./Modal/CropModal";
interface Iprops { interface Iprops {
resultData: AnyTypedQuizQuestion; resultData: AnyTypedQuizQuestion;
cropAspectRatio: { cropAspectRatio: {
@ -37,14 +39,6 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false); const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false);
const quizQid = useCurrentQuiz()?.qid; const quizQid = useCurrentQuiz()?.qid;
const theme = useTheme(); const theme = useTheme();
const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
originalImageUrl,
setCropModalImageBlob,
} = useCropModalState();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] =
useDisclosure(); useDisclosure();
async function handleImageUpload(file: File) { async function handleImageUpload(file: File) {
@ -60,7 +54,6 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
}, },
); );
closeImageUploadModal(); closeImageUploadModal();
openCropModal(file, url);
setPictureUploading(false); setPictureUploading(false);
} }
@ -134,20 +127,17 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
onClose={closeImageUploadModal} onClose={closeImageUploadModal}
handleImageChange={handleImageUpload} handleImageChange={handleImageUpload}
/> />
<CropModal <CropModalInit
isOpen={isCropModalOpen} originalImageUrl={imge}
imageBlob={imageBlob} editedUrlImagesList={{
originalImageUrl={originalImageUrl} "desktop": imge,
setCropModalImageBlob={setCropModalImageBlob} "tablet": imge,
onClose={closeCropModal} "mobile": imge,
onSaveImageClick={handleCropModalSaveClick} "small": imge,
onDeleteClick={() => {
updateQuestion(resultData.id, (question) => {
question.content.back = null;
question.content.originalBack = null;
});
}} }}
cropAspectRatio={cropAspectRatio} questionId={"12"}
questionType={"images"}
quizId={quizQid}
/> />
</Box> </Box>

@ -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<Theme> = {
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<Props> = ({
editedImage,
cropAspectRatio,
editedImagesChange,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(786));
const cropImageElementRef = useRef<HTMLImageElement>(null);
const [imageWidth, setImageWidth] = useState<number | null>(null);
const [imageHeight, setImageHeight] = useState<number | null>(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 (
<>
<Box
sx={{
height: "320px",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "0 20px"
}}
>
<ReactCrop
crop={editedImage.newRules.crop}
onChange={(_, percentCrop) => editedImagesChange((old) => ({
newRules: {
...old.newRules,
crop: percentCrop
}
}))}
minWidth={5}
minHeight={5}
locked
aspect={
cropAspectRatio
? cropAspectRatio.width / cropAspectRatio.height
: undefined
}
>
<img
onLoad={(e) => {
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",
}}
/>
</ReactCrop>
</Box>
<Box
sx={{
mt: "40px",
display: isMobile ? "block" : "flex",
alignItems: "end",
justifyContent: "space-between",
padding: "0 20px"
}}
>
<IconButton onClick={handleRotateClick}>
<ResetIcon />
</IconButton>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Размер
</Typography>
<Slider
sx={[
styleSlider,
{
width: isMobile ? undefined : "200px",
},
]}
value={editedImage.newRules.crop.width ?? 1}
min={1}
max={100}
step={0.1}
onChange={(_, newValue) => {
if (typeof newValue === "number") handleSizeChange(newValue);
}}
/>
</Box>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Затемнение
</Typography>
<Slider
sx={[
styleSlider,
{
width: isMobile ? undefined : "200px",
},
]}
value={editedImage.newRules.darken}
min={0}
max={100}
step={1}
onChange={(_, newValue) => editedImagesChange((old) => ({
newRules: {
...old.newRules,
darken: newValue as number
}
}))}
/>
</Box>
</Box>
</>
)
};
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,
);
}

@ -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 { import {
Box, Box,
Button,
Modal, Modal,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } 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 { import {
getModifiedImageBlob, DEFAULTCROPRULES,
getRotatedImageBlob, type CropModalProps,
} from "../utils/imageManipulation"; type EditedImage,
import DevaceMobileIcon from "@ui_kit/Modal/CropModal/IconCropModal/DevaceMobileIcon"; type ScreenStepsTypes
import BackArrowIcon from "@icons/BackArrowIcon"; } from "@/model/CropModal/CropModal";
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";
interface Props { export type EditedImagesChangeType = (changed: (old: EditedImage) => Partial<EditedImage>) => void;
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 const CropModal: FC<Props> = ({ export const CropModal: FC<CropModalProps> = ({
isOpen, editedImages,
imageBlob, workSpaceTypes,
originalImageUrl, originalImageUrl,
setCropModalImageBlob,
setEditedImages,
onSaveImageClick, onSaveImageClick,
closeCropModal,
onDeleteClick, onDeleteClick,
onClose,
cropAspectRatio,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const [percentCrop, setPercentCrop] = useState<PercentCrop | undefined>(
undefined,
);
const [darken, setDarken] = useState(0);
const [imageWidth, setImageWidth] = useState<number | null>(null);
const [imageHeight, setImageHeight] = useState<number | null>(null);
const [modalStep, setModalStep] = useState<string>(stepsScreen[0]);
const [lastStep, setLastStep] = useState<boolean>(null)
const cropImageElementRef = useRef<HTMLImageElement>(null);
const isMobile = useMediaQuery(theme.breakpoints.down(786)); const isMobile = useMediaQuery(theme.breakpoints.down(786));
let stepIndex = stepsScreen.indexOf(modalStep)
const modalTitle = { const [currentStep, setCurrentStep] = useState<number>(1);
"desktop": {name: "Десктоп", icon: <DevaceDesktopIcon/>},
"tablet": {name:"Планшет", icon: <DevaceTabletIcon/>}, const currentStepName: ScreenStepsTypes = useMemo(() => (
"mobile": {name:"Телефон", icon: <DevaceMobileIcon/>}, workSpaceTypes[currentStep].step
"small": {name:"Самые узкие экраны", icon: <DevaceSmallIcon/>} ), [currentStep])
const editedImagesChange: EditedImagesChangeType = (changed) => {
setEditedImages(old => {
old[currentStepName] = { ...old[currentStepName], ...changed(old[currentStepName]) }
return old
})
} }
const resetImage = () => {
const handleNextStep = () => { editedImagesChange(() => ({
if (stepIndex === stepsScreen.length - 1) { url: originalImageUrl,
onClose() newRules: DEFAULTCROPRULES,
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;
});
} }
return ( return (
<Modal <Modal
open={isOpen} open
onClose={() => { onClose={closeCropModal}
resetEditState();
onClose();
}}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
> >
<Box <Box
sx={{ sx={{
@ -206,7 +64,7 @@ export const CropModal: FC<Props> = ({
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
bgcolor: "background.paper", bgcolor: "background.paper",
boxShadow: 24, boxShadow: 24,
borderRadius: "8px", borderRadius: "12px",
width: isMobile ? "343px" : "620px", width: isMobile ? "343px" : "620px",
height: isMobile ? "80vh" : undefined, height: isMobile ? "80vh" : undefined,
display: isMobile ? "flex" : undefined, display: isMobile ? "flex" : undefined,
@ -215,174 +73,37 @@ export const CropModal: FC<Props> = ({
overflow: isMobile ? "auto" : undefined, overflow: isMobile ? "auto" : undefined,
}} }}
> >
<Box <Typography sx={{
sx={{ // height: isMobile ? "91px" : "70px",
height: isMobile ? "91px" : "70px", backgroundColor: "#F2F3F7",
backgroundColor: "#F2F3F7", padding: "25px 43px 24px 20px",
padding: "20px", borderRadius: "8px 8px 0px 0px",
borderRadius: "8px 8px 0px 0px", color: "#9A9AAF",
}} fontSize: "18px",
> lineHeight: "21.33px"
<Typography sx={{ color: "#9A9AAF", fontSize: "18px" }}> }}>
Настройте вариант отображения картинки на разных девайсах Настройте вариант отображения картинки на разных девайсах
</Typography> </Typography>
</Box> <WorkSpace
<SwitchCaseCrop //Информация о изменяемой сейчас картинке
imageUrl={imageUrl} editedImage={editedImages[currentStepName]}
handleSizeChange={handleSizeChange} //По каким правилам меняем
handleRotateClick={handleRotateClick} cropAspectRatio={workSpaceTypes[currentStep].ratio}
getInitialCrop={getInitialCrop} currentStep={currentStep}
cropAspectRatio={cropAspectRatio} currentStepName={currentStepName}
onDeleteClick={onDeleteClick}
onClose={onClose}
modalProps={modalTitle}
modalStep={modalStep}
stepIndex={stepIndex}
cropImageElementRef={cropImageElementRef}
/>
<Box
sx={{
marginTop: "40px",
padding: "0 20px 20px",
width: "100%",
display: "flex",
gap: "5px",
flexWrap: isMobile ? "wrap" : undefined,
}}
>
<Button
onClick={handleSaveOriginalImage}
disableRipple
sx={{
height: "48px",
color: "#7E2AEA",
borderRadius: "8px",
border: "1px solid #7E2AEA",
px: "20px",
width: isMobile ? "100%" : undefined,
}}
>
Сохранить оригинал
</Button>
<Box
sx={{
display: "flex",
gap: "5px",
ml: "auto",
}}
>
<Button
onClick={()=> handlePrevStep()}
variant="contained"
sx={{
fontSize: "16px",
padding: "10px 15px",
color: "#FFFFFF",
border: "1px solid #9A9AAF",
background: "#FFFFFF",
"&:hover": {
color: "#FFFFFF",
border: `1px solid ${theme.palette.primary.dark}`,
},
}}
> editedImagesChange={editedImagesChange}
<BackArrowIcon color={"#7E2AEA"}/> onDeleteClick={onDeleteClick}
</Button> />
<Button <NavigationPanel
onClick={() => { currentStep={currentStep}
handleNextStep() setCurrentStep={setCurrentStep}
// handleSaveModifiedImage() totalSteps={workSpaceTypes.length}
} onSaveImageClick={onSaveImageClick}
} resetImage={resetImage}
disableRipple />
variant="contained"
sx={{
height: "48px",
borderRadius: "8px",
border: "1px solid #7E2AEA",
p: "10px 15px",
width: isMobile ? "100%" : undefined,
// ml: "auto",
}}
>
{lastStep ?
"Сохранить редактированное" : "Далее"
}
</Button>
</Box>
</Box>
</Box> </Box>
</Modal> </Modal>
); );
}; };
export function useCropModalState(initialOpenState = false) {
const [isCropModalOpen, setOpened] = useState(initialOpenState);
const [imageBlob, setCropModalImageBlob] = useState<Blob | null>(null);
const [originalImageUrl, setOriginalImageUrl] = useState<string | null>(null);
const closeCropModal = 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,
);
}

@ -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<Props> = ({
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 (
<Box
sx={{
marginTop: "40px",
padding: "0 20px 20px",
width: "100%",
display: "flex",
gap: "5px",
flexWrap: isMobile ? "wrap" : undefined,
}}
>
<Button
onClick={resetImage}
disableRipple
sx={{
height: "48px",
color: "#7E2AEA",
borderRadius: "8px",
border: "1px solid #7E2AEA",
px: "20px",
width: isMobile ? "100%" : undefined,
}}
>
Сохранить оригинал
</Button>
<Box
sx={{
display: "flex",
gap: "5px",
ml: "auto",
}}
>
<Button
onClick={handlePrevStep}
variant="contained"
sx={{
fontSize: "16px",
padding: "10px 15px",
color: "#FFFFFF",
border: "1px solid #9A9AAF",
background: "#FFFFFF",
"&:hover": {
color: "#FFFFFF",
border: `1px solid ${theme.palette.primary.dark}`,
},
}}
>
<BackArrowIcon color={"#7E2AEA"} />
</Button>
<Button
onClick={handleNextStep}
disableRipple
variant="contained"
sx={{
height: "48px",
borderRadius: "8px",
border: "1px solid #7E2AEA",
p: "10px 15px",
width: isMobile ? "100%" : undefined,
}}
>
{lastStep ?
"Сохранить редактированное" : "Далее"
}
</Button>
</Box>
</Box>
)
}

@ -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: <DevaceDesktopIcon/>},
"tablet": {name:"Планшет", icon: <DevaceTabletIcon/>},
"mobile": {name:"Телефон", icon: <DevaceMobileIcon/>},
"small": {name:"Самые узкие экраны", icon: <DevaceSmallIcon/>}
}
interface Props{
imageUrl: null | string;
handleSizeChange: (a: number)=> void;
handleRotateClick: Promise<void>;
getInitialCrop: (imageWidth: number, imageHeight: number, aspectRatio: number)=> PercentCrop;
modalProps: {string:{}};
modalStep: string;
stepIndex: number;
onClose: () => void;
cropImageElementRef: MutableRefObject<HTMLImageElement>;
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(
<>
<CropGeneral
imageUrl={imageUrl}
handleSizeChange={handleSizeChange}
handleRotateClick={handleRotateClick}
getInitialCrop={getInitialCrop}
cropAspectRatio={cropAspectRatio}
onDeleteClick={onDeleteClick}
onClose={onClose}
modalProps={modalProps.desktop}
modalStep={stepIndex}
cropImageElementRef={cropImageElementRef}
/>
</>
)
}
case "tablet": {
return(
<>
<CropGeneral
imageUrl={imageUrl}
handleSizeChange={handleSizeChange}
handleRotateClick={handleRotateClick}
getInitialCrop={getInitialCrop}
cropAspectRatio={cropAspectRatio}
onDeleteClick={onDeleteClick}
onClose={onClose}
modalProps={modalProps.tablet}
modalStep={stepIndex}
cropImageElementRef={cropImageElementRef}
/>
</>
)
}
case "mobile": {
return(
<CropGeneral
imageUrl={imageUrl}
handleSizeChange={handleSizeChange}
handleRotateClick={handleRotateClick}
getInitialCrop={getInitialCrop}
cropAspectRatio={cropAspectRatio}
onDeleteClick={onDeleteClick}
onClose={onClose}
modalProps={modalProps.mobile}
modalStep={stepIndex}
cropImageElementRef={cropImageElementRef}
/>
)
}
case "small": {
return(
<CropGeneral
imageUrl={imageUrl}
handleSizeChange={handleSizeChange}
handleRotateClick={handleRotateClick}
getInitialCrop={getInitialCrop}
cropAspectRatio={cropAspectRatio}
onDeleteClick={onDeleteClick}
onClose={onClose}
modalProps={modalProps.small}
modalStep={stepIndex}
cropImageElementRef={cropImageElementRef}
/>
)
}
}
}

@ -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: <DevaceDesktopIcon /> },
"tablet": { name: "Планшет", icon: <DevaceTabletIcon /> },
"mobile": { name: "Телефон", icon: <DevaceMobileIcon /> },
"small": { name: "Самые узкие экраны", icon: <DevaceSmallIcon /> }
};
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 (
<>
<Box
sx={{
mt: "5px",
padding: "0 20px",
display: "flex",
alignItems: "center",
justifyContent: "space-between"
}}
>
<Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "13px"
}}
>
<Typography
sx={{
color: "#4D4D4D",
fontSize: "24px",
fontWeight: 500,
lineHeight: "28.44px",
}}
>{currentModel.name}</Typography>
{currentModel.icon}
</Box>
<Typography sx={{
fontSize: "14px",
color: "#9A9AAF",
lineHeight: "16.59px",
}}>
{currentStep + 1 + " шаг"}
</Typography>
</Box>
<IconButton
// onClick={onDeleteClick}
sx={{
height: "48px",
width: "48px",
p: 0,
color: theme.palette.orange.main,
borderRadius: "50%",
}}
>
<DeleteIcon />
</IconButton>
</Box>
<CropGeneral
editedImage={editedImage}
cropAspectRatio={cropAspectRatio}
editedImagesChange={editedImagesChange}
/>
</>
);
}

@ -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<void>;
getInitialCrop: (imageWidth: number, imageHeight: number, aspectRatio: number)=> PercentCrop;
modalProps: any;
modalStep: number;
onClose: () => void;
cropImageElementRef: MutableRefObject<HTMLImageElement>;
onDeleteClick?: () => void;
cropAspectRatio?: {
width: number;
height: number;
};
}
const styleSlider: SxProps<Theme> = {
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<PercentCrop | undefined>(
undefined,
);
const [imageWidth, setImageWidth] = useState<number | null>(null);
const [imageHeight, setImageHeight] = useState<number | null>(null);
const [darken, setDarken] = useState(0);
const step = modalStep + 1
return (
<>
<Box
sx={{
padding: "20px 20px 0",
display: "flex",
alignItems: "center",
justifyContent: "space-between"
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "12px"
}}
>
<Typography>{modalProps.name}</Typography>
{modalProps.icon}
</Box>
{onDeleteClick && (
<IconButton
onClick={() => {
onDeleteClick?.();
onClose();
}}
sx={{
height: "48px",
width: "48px",
p: 0,
color: theme.palette.orange.main,
borderRadius: "50%",
}}
>
<DeleteIcon />
</IconButton>
)}
</Box>
<Typography sx={{p: "0 20px 20px", fontSize: "14px", color: "#9A9AAF"}}>
{step + " шаг"}</Typography>
<Box
sx={{
height: "320px",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "0 20px"
}}
>
{imageUrl && (
<ReactCrop
crop={percentCrop}
onChange={(_, percentCrop) => setPercentCrop(percentCrop)}
minWidth={5}
minHeight={5}
locked
aspect={
cropAspectRatio
? cropAspectRatio.width / cropAspectRatio.height
: undefined
}
>
<img
onLoad={(e) => {
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",
}}
/>
</ReactCrop>
)}
</Box>
<Box
sx={{
mt: "40px",
display: isMobile ? "block" : "flex",
alignItems: "end",
justifyContent: "space-between",
padding: "0 20px"
}}
>
<IconButton onClick={handleRotateClick}>
<ResetIcon />
</IconButton>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Размер
</Typography>
<Slider
sx={[
styleSlider,
{
width: isMobile ? undefined : "200px",
},
]}
value={percentCrop?.width ?? 1}
min={1}
max={100}
step={0.1}
onChange={(_, newValue) => {
if (typeof newValue === "number") handleSizeChange(newValue);
}}
/>
</Box>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Затемнение
</Typography>
<Slider
sx={[
styleSlider,
{
width: isMobile ? undefined : "200px",
},
]}
value={darken}
min={0}
max={100}
step={1}
onChange={(_, newValue) => setDarken(newValue as number)}
/>
</Box>
</Box>
</>
)
}

@ -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<CropOnOpenType> = ({
originalImageUrl,
imageBlob,
editedUrlImagesList,
questionId,
questionType,
quizId,
selfClose,
setPictureUploading,
}) => {
const [acceptedOriginalImageUrl, setOriginalImageUrl] = useState("");
const [editedImages, setEditedImages] = useState<EditedImages>({} 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 <CropModal
editedImages={editedImages}
workSpaceTypes={workSpaceTypesList[questionType]}
originalImageUrl={acceptedOriginalImageUrl}
setEditedImages={setEditedImages}
onSaveImageClick={saveImagesAndRules}
closeCropModal={closeModal}
onDeleteClick={handleCropModalDeleteImageClick}
/>
};