модалка переделана под новую модель работы, открывается хардкодом. Пока не редактирует
This commit is contained in:
parent
5fd2534216
commit
bb4775e18e
68
src/model/CropModal/CropModal.ts
Normal file
68
src/model/CropModal/CropModal.ts
Normal file
@ -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: [],
|
||||
buyers: [],
|
||||
});
|
||||
const [tags, setTags] = useState<TTags>({
|
||||
const [taщgs, setTags] = useState<TTags>({
|
||||
deal: [],
|
||||
contact: [],
|
||||
company: [],
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
<CropModal
|
||||
isOpen={isCropModalOpen}
|
||||
imageBlob={imageBlob}
|
||||
originalImageUrl={originalImageUrl}
|
||||
setCropModalImageBlob={setCropModalImageBlob}
|
||||
onClose={closeCropModal}
|
||||
onSaveImageClick={handleCropModalSaveClick}
|
||||
onDeleteClick={() => {
|
||||
if (selectedVariantId)
|
||||
clearQuestionImages(question.id, selectedVariantId);
|
||||
<CropModalInit
|
||||
originalImageUrl={imge}
|
||||
editedUrlImagesList={{
|
||||
"desktop": imge,
|
||||
"tablet": imge,
|
||||
"mobile": imge,
|
||||
"small": imge,
|
||||
}}
|
||||
cropAspectRatio={{ width: 300, height: 300 }}
|
||||
questionId={question.backendId.toString()}
|
||||
questionType={question.type}
|
||||
quizId={quizQid}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@ -4,7 +4,6 @@ import {
|
||||
uploadQuestionImage,
|
||||
} from "@root/questions/actions";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal/CropModal";
|
||||
import { useState } from "react";
|
||||
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
|
||||
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
|
||||
@ -16,6 +15,8 @@ import ButtonsOptions from "../ButtonsOptions";
|
||||
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
||||
import SwitchAnswerOptionsPict from "./switchOptionsPict";
|
||||
|
||||
import imge from "@/assets/card-1.png"
|
||||
import { CropModalInit } from "@/ui_kit/Modal/CropModal";
|
||||
interface Props {
|
||||
question: QuizQuestionImages;
|
||||
openBranchingPage: boolean;
|
||||
@ -38,14 +39,6 @@ export default function OptionsPicture({
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] =
|
||||
useDisclosure();
|
||||
const {
|
||||
isCropModalOpen,
|
||||
openCropModal,
|
||||
closeCropModal,
|
||||
imageBlob,
|
||||
originalImageUrl,
|
||||
setCropModalImageBlob,
|
||||
} = useCropModalState();
|
||||
|
||||
const handleImageUpload = async (file: File) => {
|
||||
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}
|
||||
/>
|
||||
<CropModal
|
||||
isOpen={isCropModalOpen}
|
||||
imageBlob={imageBlob}
|
||||
originalImageUrl={originalImageUrl}
|
||||
setCropModalImageBlob={setCropModalImageBlob}
|
||||
onClose={closeCropModal}
|
||||
onSaveImageClick={handleCropModalSaveClick}
|
||||
onDeleteClick={() => {
|
||||
if (selectedVariantId)
|
||||
clearQuestionImages(question.id, selectedVariantId);
|
||||
<CropModalInit
|
||||
originalImageUrl={imge}
|
||||
editedUrlImagesList={{
|
||||
"desktop": imge,
|
||||
"tablet": imge,
|
||||
"mobile": imge,
|
||||
"small": imge,
|
||||
}}
|
||||
cropAspectRatio={{ width: 452, height: 300 }}
|
||||
questionId={question.backendId.toString()}
|
||||
questionType={question.type}
|
||||
quizId={quizQid}
|
||||
/>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||
<Link
|
||||
|
||||
@ -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<boolean>(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<HTMLDivElement>) => {
|
||||
@ -101,18 +94,20 @@ export const DropZone = ({
|
||||
onClose={closeImageUploadModal}
|
||||
handleImageChange={handleImageUpload}
|
||||
/>
|
||||
<CropModal
|
||||
isOpen={isCropModalOpen}
|
||||
imageBlob={imageBlob}
|
||||
originalImageUrl={originalImageUrl}
|
||||
setCropModalImageBlob={setCropModalImageBlob}
|
||||
onClose={closeCropModal}
|
||||
onSaveImageClick={onImageSaveClick}
|
||||
cropAspectRatio={cropAspectRatio}
|
||||
<CropModalInit
|
||||
originalImageUrl={imge}
|
||||
editedUrlImagesList={{
|
||||
"desktop": imge,
|
||||
"tablet": imge,
|
||||
"mobile": imge,
|
||||
"small": imge,
|
||||
}}
|
||||
questionId={'2'}
|
||||
questionType={"images"}
|
||||
quizId={"12"}
|
||||
/>
|
||||
<ButtonBase
|
||||
onClick={
|
||||
imageUrl ? () => openCropModal(imageUrl) : openImageUploadModal
|
||||
onClick={ openImageUploadModal
|
||||
}
|
||||
sx={{
|
||||
width: "100%",
|
||||
|
||||
@ -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<Iprops> = ({
|
||||
const [backgroundUploding, setBackgroundUploading] = useState<boolean>(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<Iprops> = ({
|
||||
},
|
||||
);
|
||||
closeImageUploadModal();
|
||||
openCropModal(file, url);
|
||||
|
||||
setPictureUploading(false);
|
||||
}
|
||||
@ -134,20 +127,17 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
|
||||
onClose={closeImageUploadModal}
|
||||
handleImageChange={handleImageUpload}
|
||||
/>
|
||||
<CropModal
|
||||
isOpen={isCropModalOpen}
|
||||
imageBlob={imageBlob}
|
||||
originalImageUrl={originalImageUrl}
|
||||
setCropModalImageBlob={setCropModalImageBlob}
|
||||
onClose={closeCropModal}
|
||||
onSaveImageClick={handleCropModalSaveClick}
|
||||
onDeleteClick={() => {
|
||||
updateQuestion(resultData.id, (question) => {
|
||||
question.content.back = null;
|
||||
question.content.originalBack = null;
|
||||
});
|
||||
<CropModalInit
|
||||
originalImageUrl={imge}
|
||||
editedUrlImagesList={{
|
||||
"desktop": imge,
|
||||
"tablet": imge,
|
||||
"mobile": imge,
|
||||
"small": imge,
|
||||
}}
|
||||
cropAspectRatio={cropAspectRatio}
|
||||
questionId={"12"}
|
||||
questionType={"images"}
|
||||
quizId={quizQid}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
276
src/ui_kit/Modal/CropModal/CropGeneral.tsx
Normal file
276
src/ui_kit/Modal/CropModal/CropGeneral.tsx
Normal file
@ -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 {
|
||||
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<EditedImage>) => void;
|
||||
|
||||
export const CropModal: FC<Props> = ({
|
||||
isOpen,
|
||||
imageBlob,
|
||||
export const CropModal: FC<CropModalProps> = ({
|
||||
editedImages,
|
||||
workSpaceTypes,
|
||||
originalImageUrl,
|
||||
setCropModalImageBlob,
|
||||
|
||||
setEditedImages,
|
||||
onSaveImageClick,
|
||||
closeCropModal,
|
||||
onDeleteClick,
|
||||
onClose,
|
||||
cropAspectRatio,
|
||||
}) => {
|
||||
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));
|
||||
let stepIndex = stepsScreen.indexOf(modalStep)
|
||||
const modalTitle = {
|
||||
"desktop": {name: "Десктоп", icon: <DevaceDesktopIcon/>},
|
||||
"tablet": {name:"Планшет", icon: <DevaceTabletIcon/>},
|
||||
"mobile": {name:"Телефон", icon: <DevaceMobileIcon/>},
|
||||
"small": {name:"Самые узкие экраны", icon: <DevaceSmallIcon/>}
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<number>(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 (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onClose={() => {
|
||||
resetEditState();
|
||||
onClose();
|
||||
}}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description"
|
||||
open
|
||||
onClose={closeCropModal}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
@ -206,7 +64,7 @@ export const CropModal: FC<Props> = ({
|
||||
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<Props> = ({
|
||||
overflow: isMobile ? "auto" : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
height: isMobile ? "91px" : "70px",
|
||||
<Typography sx={{
|
||||
// height: isMobile ? "91px" : "70px",
|
||||
backgroundColor: "#F2F3F7",
|
||||
padding: "20px",
|
||||
padding: "25px 43px 24px 20px",
|
||||
borderRadius: "8px 8px 0px 0px",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: "#9A9AAF", fontSize: "18px" }}>
|
||||
color: "#9A9AAF",
|
||||
fontSize: "18px",
|
||||
lineHeight: "21.33px"
|
||||
}}>
|
||||
Настройте вариант отображения картинки на разных девайсах
|
||||
</Typography>
|
||||
</Box>
|
||||
<SwitchCaseCrop
|
||||
imageUrl={imageUrl}
|
||||
handleSizeChange={handleSizeChange}
|
||||
handleRotateClick={handleRotateClick}
|
||||
getInitialCrop={getInitialCrop}
|
||||
cropAspectRatio={cropAspectRatio}
|
||||
<WorkSpace
|
||||
//Информация о изменяемой сейчас картинке
|
||||
editedImage={editedImages[currentStepName]}
|
||||
//По каким правилам меняем
|
||||
cropAspectRatio={workSpaceTypes[currentStep].ratio}
|
||||
currentStep={currentStep}
|
||||
currentStepName={currentStepName}
|
||||
|
||||
editedImagesChange={editedImagesChange}
|
||||
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}`,
|
||||
},
|
||||
}}
|
||||
<NavigationPanel
|
||||
currentStep={currentStep}
|
||||
setCurrentStep={setCurrentStep}
|
||||
totalSteps={workSpaceTypes.length}
|
||||
onSaveImageClick={onSaveImageClick}
|
||||
resetImage={resetImage}
|
||||
/>
|
||||
|
||||
>
|
||||
<BackArrowIcon color={"#7E2AEA"}/>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleNextStep()
|
||||
// handleSaveModifiedImage()
|
||||
}
|
||||
}
|
||||
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>
|
||||
</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,
|
||||
);
|
||||
}
|
||||
|
||||
111
src/ui_kit/Modal/CropModal/NavigationPanel.tsx
Normal file
111
src/ui_kit/Modal/CropModal/NavigationPanel.tsx
Normal file
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/ui_kit/Modal/CropModal/WorkSpace.tsx
Normal file
109
src/ui_kit/Modal/CropModal/WorkSpace.tsx
Normal file
@ -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>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
208
src/ui_kit/Modal/CropModal/index.tsx
Normal file
208
src/ui_kit/Modal/CropModal/index.tsx
Normal file
@ -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}
|
||||
/>
|
||||
|
||||
};
|
||||
0
src/ui_kit/Modal/CropModal/utilities.ts
Normal file
0
src/ui_kit/Modal/CropModal/utilities.ts
Normal file
Loading…
Reference in New Issue
Block a user