Merge branch 'new-crop-modal' into dev
This commit is contained in:
commit
6df10d1f03
@ -34,7 +34,7 @@ export const register = async (
|
|||||||
return [registerResponse];
|
return [registerResponse];
|
||||||
} catch (nativeError) {
|
} catch (nativeError) {
|
||||||
const [error] = parseAxiosError(nativeError);
|
const [error] = parseAxiosError(nativeError);
|
||||||
console.log(error)
|
console.error(error)
|
||||||
|
|
||||||
return [null, `Не удалось зарегестрировать аккаунт. ${error}`];
|
return [null, `Не удалось зарегестрировать аккаунт. ${error}`];
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/assets/icons/AmoTrash.tsx
Normal file
26
src/assets/icons/AmoTrash.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
color?: string;
|
||||||
|
height?: string;
|
||||||
|
width?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AmoTrash({ color, height, width }: Props) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19.4994 6H4.5" stroke="#FC2012" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M5.5 18.7492V8.74609H18.5V18.7492C18.5 20.1299 17.3807 21.2492 16 21.2492H8C6.61929 21.2492 5.5 20.1299 5.5 18.7492Z" fill="#FC2012" stroke="#F02B2B"/>
|
||||||
|
<path d="M15.75 6V4.5C15.75 4.10218 15.592 3.72064 15.3107 3.43934C15.0294 3.15804 14.6478 3 14.25 3H9.75C9.35218 3 8.97064 3.15804 8.68934 3.43934C8.40804 3.72064 8.25 4.10218 8.25 4.5V6" stroke="#FC2012" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
166
src/model/CropModal/CropModal.ts
Normal file
166
src/model/CropModal/CropModal.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import type { PercentCrop } from "react-image-crop";
|
||||||
|
|
||||||
|
|
||||||
|
export const workSpaceTypesList = {
|
||||||
|
images: {
|
||||||
|
desktop: {
|
||||||
|
step: "desktop",
|
||||||
|
ratio: {
|
||||||
|
width: 317,
|
||||||
|
height: 257
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tablet: {
|
||||||
|
step: "tablet",
|
||||||
|
ratio: {
|
||||||
|
width: 455,
|
||||||
|
height: 257
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
step: "mobile",
|
||||||
|
ratio: {
|
||||||
|
width: 160,
|
||||||
|
height: 183
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
varimg: {
|
||||||
|
desktop: {
|
||||||
|
step: "desktop",
|
||||||
|
ratio: {
|
||||||
|
width: 450,
|
||||||
|
height: 450
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
step: "mobile",
|
||||||
|
ratio: {
|
||||||
|
width: 335,
|
||||||
|
height: 335,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
desktop: {
|
||||||
|
step: "desktop",
|
||||||
|
ratio: {
|
||||||
|
width: 450,
|
||||||
|
height: 450
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
step: "mobile",
|
||||||
|
ratio: {
|
||||||
|
width: 335,
|
||||||
|
height: 335,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
desktop: {
|
||||||
|
step: "desktop",
|
||||||
|
ratio: {
|
||||||
|
width: 450,
|
||||||
|
height: 450
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
step: "mobile",
|
||||||
|
ratio: {
|
||||||
|
width: 335,
|
||||||
|
height: 335,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
desktop: {
|
||||||
|
step: "desktop",
|
||||||
|
ratio: {
|
||||||
|
width: 700,
|
||||||
|
height: 306
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
step: "mobile",
|
||||||
|
ratio: {
|
||||||
|
width: 335,
|
||||||
|
height: 236
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type WorkSpaceTypesList = typeof workSpaceTypesList;
|
||||||
|
export type Writable<T> = { -readonly [K in keyof T]: T[K] };
|
||||||
|
|
||||||
|
|
||||||
|
export type CropOnOpenType = {
|
||||||
|
open: boolean;
|
||||||
|
originalImageUrl?:string;
|
||||||
|
imageBlob?: Blob;
|
||||||
|
editedUrlImagesList?: Record<Partial<ScreenStepsTypes>, string>;
|
||||||
|
questionId: string;
|
||||||
|
questionType: AcceptedQuestionTypes;
|
||||||
|
quizId: string;
|
||||||
|
variantId?: string;
|
||||||
|
|
||||||
|
selfClose: () => void;
|
||||||
|
setPictureUploading: (is: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkSpaceTypes = WorkSpaceTypesList[AcceptedQuestionTypes];
|
||||||
|
export interface CropModalProps {
|
||||||
|
open: boolean;
|
||||||
|
editedImages: Record<keyof WorkSpaceTypes, EditedImage>;
|
||||||
|
workSpaceTypes: WorkSpaceTypes;
|
||||||
|
originalImageUrl: string;
|
||||||
|
|
||||||
|
setEditedImages: (callback: (editedImages: Record<keyof WorkSpaceTypes, EditedImage>) => Record<keyof WorkSpaceTypes, EditedImage>) => void;
|
||||||
|
onSaveImageClick: () => void;
|
||||||
|
closeCropModal: CropOnCloseType;
|
||||||
|
onDeleteClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AcceptedQuestionTypes = keyof WorkSpaceTypesList; //"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 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,
|
||||||
|
}
|
||||||
@ -224,8 +224,6 @@ export const SwitchPages = ({
|
|||||||
}
|
}
|
||||||
body.FieldsRule = FieldsRule;
|
body.FieldsRule = FieldsRule;
|
||||||
|
|
||||||
console.log(body)
|
|
||||||
|
|
||||||
if (firstRules) {
|
if (firstRules) {
|
||||||
setIntegrationRules(quiz.backendId.toString(), body);
|
setIntegrationRules(quiz.backendId.toString(), body);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -38,7 +38,6 @@ const AnswerItem = memo<AnswerItemProps>(
|
|||||||
const setOwnPlaceholder = (replText: string) => {
|
const setOwnPlaceholder = (replText: string) => {
|
||||||
updateQuestion(questionId, (question) => {
|
updateQuestion(questionId, (question) => {
|
||||||
question.content.ownPlaceholder = replText;
|
question.content.ownPlaceholder = replText;
|
||||||
console.log(question)
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -35,8 +35,6 @@ function CsComponent() {
|
|||||||
const modalQuestionParentContentId = useUiTools((state) => state.modalQuestionParentContentId);
|
const modalQuestionParentContentId = useUiTools((state) => state.modalQuestionParentContentId);
|
||||||
const modalQuestionTargetContentId = useUiTools((state) => state.modalQuestionTargetContentId);
|
const modalQuestionTargetContentId = useUiTools((state) => state.modalQuestionTargetContentId);
|
||||||
const trashQuestions = useQuestionsStore((state) => state.questions);
|
const trashQuestions = useQuestionsStore((state) => state.questions);
|
||||||
console.log("trashQuestions")
|
|
||||||
console.log(trashQuestions)
|
|
||||||
const cyRef = useRef<Core | null>(null);
|
const cyRef = useRef<Core | null>(null);
|
||||||
const { removeNode } = useRemoveNode({ cyRef });
|
const { removeNode } = useRemoveNode({ cyRef });
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,22 @@
|
|||||||
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import { addQuestionVariant, clearQuestionImages, uploadQuestionImage } from "@root/questions/actions";
|
import {
|
||||||
|
addQuestionVariant,
|
||||||
|
clearQuestionImages,
|
||||||
|
uploadQuestionImage,
|
||||||
|
} from "@root/questions/actions";
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
|
import { useEffect, useMemo, 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 "@frontend/squzanswerer";
|
import type { QuizQuestionVarImg } from "@frontend/squzanswerer/dist-package/model/questionTypes/varimg";
|
||||||
import { useDisclosure } from "@/utils/useDisclosure";
|
import { useDisclosure } from "@/utils/useDisclosure";
|
||||||
import { AnswerDraggableList } from "../../AnswerDraggableList";
|
import { AnswerDraggableList } from "../../AnswerDraggableList";
|
||||||
import ImageEditAnswerItem from "../../AnswerDraggableList/ImageEditAnswerItem";
|
import ImageEditAnswerItem from "../../AnswerDraggableList/ImageEditAnswerItem";
|
||||||
import ButtonsOptions from "../ButtonsLayout/ButtonsOptions";
|
import ButtonsOptions from "../ButtonsLayout/ButtonsOptions";
|
||||||
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;
|
||||||
@ -18,33 +24,48 @@ interface Props {
|
|||||||
setOpenBranchingPage: (a: boolean) => void;
|
setOpenBranchingPage: (a: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OptionsAndPicture({ question, setOpenBranchingPage }: Props) {
|
export default function OptionsAndPicture({
|
||||||
|
question,
|
||||||
|
setOpenBranchingPage,
|
||||||
|
}: Props) {
|
||||||
const [switchState, setSwitchState] = useState("setting");
|
const [switchState, setSwitchState] = useState("setting");
|
||||||
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
|
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
|
||||||
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
const [openCropModal, setOpenCropModal] = useState(false);
|
||||||
|
|
||||||
|
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const variant = question.content.variants.find(
|
||||||
|
(variant) => variant.id === selectedVariantId,
|
||||||
|
)
|
||||||
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] = useDisclosure();
|
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = 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;
|
||||||
|
|
||||||
setPictureUploading(true);
|
setPictureUploading(true);
|
||||||
|
|
||||||
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
|
|
||||||
if (!("variants" in question.content)) return;
|
|
||||||
|
|
||||||
const variant = question.content.variants.find((variant) => variant.id === selectedVariantId);
|
|
||||||
if (!variant) return;
|
|
||||||
|
|
||||||
variant.extendedText = url;
|
|
||||||
variant.originalImageUrl = url;
|
|
||||||
});
|
|
||||||
closeImageUploadModal();
|
closeImageUploadModal();
|
||||||
openCropModal(file, url);
|
const url = await uploadQuestionImage(
|
||||||
|
question.id,
|
||||||
|
quizQid,
|
||||||
|
file,
|
||||||
|
(question, url) => {
|
||||||
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const variant = question.content.variants.find(
|
||||||
|
(variant) => variant.id === selectedVariantId,
|
||||||
|
);
|
||||||
|
if (!variant) return;
|
||||||
|
|
||||||
|
variant.extendedText = url;
|
||||||
|
variant.originalImageUrl = url;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setOpenCropModal(true)
|
||||||
|
|
||||||
setPictureUploading(false);
|
setPictureUploading(false);
|
||||||
};
|
};
|
||||||
@ -55,7 +76,9 @@ export default function OptionsAndPicture({ question, setOpenBranchingPage }: Pr
|
|||||||
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
|
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
|
||||||
if (!("variants" in question.content)) return;
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
const variant = question.content.variants.find((variant) => variant.id === selectedVariantId);
|
const variant = question.content.variants.find(
|
||||||
|
(variant) => variant.id === selectedVariantId,
|
||||||
|
);
|
||||||
if (!variant) return;
|
if (!variant) return;
|
||||||
|
|
||||||
variant.extendedText = url;
|
variant.extendedText = url;
|
||||||
@ -73,9 +96,7 @@ export default function OptionsAndPicture({ question, setOpenBranchingPage }: Pr
|
|||||||
<Box sx={{ pl: "20px", pr: "20px" }}>
|
<Box sx={{ pl: "20px", pr: "20px" }}>
|
||||||
<AnswerDraggableList
|
<AnswerDraggableList
|
||||||
questionId={question.id}
|
questionId={question.id}
|
||||||
variants={question.content.variants
|
variants={question.content.variants.map((variant, index) => (
|
||||||
.filter(variant => !variant.isOwn ? true : question.content.own && variant.isOwn)
|
|
||||||
.map((variant, index) => (
|
|
||||||
<ImageEditAnswerItem
|
<ImageEditAnswerItem
|
||||||
key={variant.id}
|
key={variant.id}
|
||||||
index={index}
|
index={index}
|
||||||
@ -84,7 +105,7 @@ export default function OptionsAndPicture({ question, setOpenBranchingPage }: Pr
|
|||||||
largeCheck={question.content.largeCheck}
|
largeCheck={question.content.largeCheck}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
openCropModal={openCropModal}
|
openCropModal={() => {setOpenCropModal(true)}}
|
||||||
openImageUploadModal={openImageUploadModal}
|
openImageUploadModal={openImageUploadModal}
|
||||||
pictureUploding={pictureUploding}
|
pictureUploding={pictureUploding}
|
||||||
setSelectedVariantId={setSelectedVariantId}
|
setSelectedVariantId={setSelectedVariantId}
|
||||||
@ -98,17 +119,16 @@ export default function OptionsAndPicture({ question, setOpenBranchingPage }: Pr
|
|||||||
onClose={closeImageUploadModal}
|
onClose={closeImageUploadModal}
|
||||||
handleImageChange={handleImageUpload}
|
handleImageChange={handleImageUpload}
|
||||||
/>
|
/>
|
||||||
<CropModal
|
<CropModalInit
|
||||||
isOpen={isCropModalOpen}
|
originalImageUrl={variant?.originalImageUrl}
|
||||||
imageBlob={imageBlob}
|
editedUrlImagesList={variant?.editedUrlImagesList}
|
||||||
originalImageUrl={originalImageUrl}
|
questionId={question.id.toString()}
|
||||||
setCropModalImageBlob={setCropModalImageBlob}
|
questionType={question.type}
|
||||||
onClose={closeCropModal}
|
quizId={quizQid}
|
||||||
onSaveImageClick={handleCropModalSaveClick}
|
variantId={variant?.id}
|
||||||
onDeleteClick={() => {
|
open={openCropModal}
|
||||||
if (selectedVariantId) clearQuestionImages(question.id, selectedVariantId);
|
selfClose={() => setOpenCropModal(false)}
|
||||||
}}
|
setPictureUploading={setPictureUploading}
|
||||||
cropAspectRatio={{ width: 300, height: 300 }}
|
|
||||||
/>
|
/>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import { clearQuestionImages, uploadQuestionImage } from "@root/questions/actions";
|
import {
|
||||||
|
clearQuestionImages,
|
||||||
|
uploadQuestionImage,
|
||||||
|
} from "@root/questions/actions";
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
|
import { useMemo, 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 "@frontend/squzanswerer";
|
import type { QuizQuestionVarImg } from "@frontend/squzanswerer/dist-package/model/questionTypes/varimg";
|
||||||
|
|
||||||
|
//@/model/questionTypes/images";
|
||||||
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
|
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
|
||||||
import { useDisclosure } from "@/utils/useDisclosure";
|
import { useDisclosure } from "@/utils/useDisclosure";
|
||||||
import { AnswerDraggableList } from "../../AnswerDraggableList";
|
import { AnswerDraggableList } from "../../AnswerDraggableList";
|
||||||
@ -13,65 +17,68 @@ import ButtonsOptions from "../ButtonsLayout/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: QuizQuestionVarImg;
|
||||||
openBranchingPage: boolean;
|
openBranchingPage: boolean;
|
||||||
setOpenBranchingPage: (a: boolean) => void;
|
setOpenBranchingPage: (a: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OptionsPicture({ question, openBranchingPage, setOpenBranchingPage }: Props) {
|
export default function OptionsPicture({
|
||||||
|
question,
|
||||||
|
openBranchingPage,
|
||||||
|
setOpenBranchingPage,
|
||||||
|
}: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const {onClickAddAnAnswer} = useAddAnswer();
|
const onClickAddAnAnswer = useAddAnswer();
|
||||||
const quizQid = useCurrentQuiz()?.qid;
|
const quizQid = useCurrentQuiz()?.qid;
|
||||||
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
|
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
|
||||||
|
const [openCropModal, setOpenCropModal] = useState(false);
|
||||||
|
|
||||||
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
||||||
|
const variant = question.content.variants.find(
|
||||||
|
(variant) => variant.id === selectedVariantId,
|
||||||
|
)
|
||||||
|
|
||||||
const [switchState, setSwitchState] = useState("setting");
|
const [switchState, setSwitchState] = useState("setting");
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||||
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
|
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = 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;
|
||||||
|
|
||||||
setPictureUploading(true);
|
setPictureUploading(true);
|
||||||
|
|
||||||
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
|
|
||||||
if (!("variants" in question.content)) return;
|
|
||||||
|
|
||||||
const variant = question.content.variants.find((variant) => variant.id === selectedVariantId);
|
|
||||||
if (!variant) return;
|
|
||||||
|
|
||||||
variant.extendedText = url;
|
|
||||||
variant.originalImageUrl = url;
|
|
||||||
});
|
|
||||||
|
|
||||||
closeImageUploadModal();
|
closeImageUploadModal();
|
||||||
openCropModal(file, url);
|
const url = await uploadQuestionImage(
|
||||||
|
question.id,
|
||||||
|
quizQid,
|
||||||
|
file,
|
||||||
|
(question, url) => {
|
||||||
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const variant = question.content.variants.find(
|
||||||
|
(variant) => variant.id === selectedVariantId,
|
||||||
|
);
|
||||||
|
if (!variant) return;
|
||||||
|
|
||||||
|
variant.extendedText = url;
|
||||||
|
variant.originalImageUrl = url;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setOpenCropModal(true)
|
||||||
|
|
||||||
setPictureUploading(false);
|
setPictureUploading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleCropModalSaveClick(imageBlob: Blob) {
|
|
||||||
if (!selectedVariantId) return;
|
|
||||||
|
|
||||||
uploadQuestionImage(question.id, quizQid, 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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ padding: "20px" }}>
|
<Box sx={{ padding: "20px" }}>
|
||||||
<AnswerDraggableList
|
<AnswerDraggableList
|
||||||
questionId={question.id}
|
questionId={question.id}
|
||||||
variants={question.content.variants
|
variants={question.content.variants.map((variant, index) => (
|
||||||
.filter(variant => !variant.isOwn ? true : question.content.own && variant.isOwn)
|
|
||||||
.map((variant, index) => (
|
|
||||||
<ImageEditAnswerItem
|
<ImageEditAnswerItem
|
||||||
key={variant.id}
|
key={variant.id}
|
||||||
index={index}
|
index={index}
|
||||||
@ -80,7 +87,7 @@ export default function OptionsPicture({ question, openBranchingPage, setOpenBra
|
|||||||
largeCheck={question.content.largeCheck}
|
largeCheck={question.content.largeCheck}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
openCropModal={openCropModal}
|
openCropModal={() => {setOpenCropModal(true)}}
|
||||||
openImageUploadModal={openImageUploadModal}
|
openImageUploadModal={openImageUploadModal}
|
||||||
pictureUploding={pictureUploding}
|
pictureUploding={pictureUploding}
|
||||||
setSelectedVariantId={setSelectedVariantId}
|
setSelectedVariantId={setSelectedVariantId}
|
||||||
@ -94,17 +101,16 @@ export default function OptionsPicture({ question, openBranchingPage, setOpenBra
|
|||||||
onClose={closeImageUploadModal}
|
onClose={closeImageUploadModal}
|
||||||
handleImageChange={handleImageUpload}
|
handleImageChange={handleImageUpload}
|
||||||
/>
|
/>
|
||||||
<CropModal
|
<CropModalInit
|
||||||
isOpen={isCropModalOpen}
|
originalImageUrl={variant?.originalImageUrl}
|
||||||
imageBlob={imageBlob}
|
editedUrlImagesList={variant?.editedUrlImagesList}
|
||||||
originalImageUrl={originalImageUrl}
|
questionId={question.id.toString()}
|
||||||
setCropModalImageBlob={setCropModalImageBlob}
|
questionType={question.type}
|
||||||
onClose={closeCropModal}
|
quizId={quizQid}
|
||||||
onSaveImageClick={handleCropModalSaveClick}
|
variantId={variant?.id}
|
||||||
onDeleteClick={() => {
|
open={openCropModal}
|
||||||
if (selectedVariantId) clearQuestionImages(question.id, selectedVariantId);
|
selfClose={() => setOpenCropModal(false)}
|
||||||
}}
|
setPictureUploading={setPictureUploading}
|
||||||
cropAspectRatio={{ width: 452, height: 300 }}
|
|
||||||
/>
|
/>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
|
|||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { DropZone } from "../../../pages/startPage/dropZone";
|
import { DropZone } from "../../../pages/startPage/dropZone";
|
||||||
|
import { CropModalInit } from "@/ui_kit/Modal/CropModal";
|
||||||
|
|
||||||
type UploadImageProps = {
|
type UploadImageProps = {
|
||||||
question: AnyTypedQuizQuestion;
|
question: AnyTypedQuizQuestion;
|
||||||
@ -15,6 +16,8 @@ type UploadImageProps = {
|
|||||||
|
|
||||||
export default function UploadImage({ question, cropAspectRatio }: UploadImageProps) {
|
export default function UploadImage({ question, cropAspectRatio }: UploadImageProps) {
|
||||||
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
|
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
|
||||||
|
const [openCropModal, setOpenCropModal] = useState(false);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
|
|
||||||
@ -33,43 +36,53 @@ export default function UploadImage({ question, cropAspectRatio }: UploadImagePr
|
|||||||
Загрузить изображение
|
Загрузить изображение
|
||||||
</Typography>
|
</Typography>
|
||||||
{pictureUploding ? (
|
{pictureUploding ? (
|
||||||
<Skeleton
|
<Skeleton variant="rounded" sx={{ height: "120px", width: "300px" }} />
|
||||||
variant="rounded"
|
|
||||||
sx={{ height: "120px", width: "300px" }}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<DropZone
|
<DropZone
|
||||||
text={"5 MB максимум"}
|
text={"5 MB максимум"}
|
||||||
sx={{ maxWidth: "300px", width: "100%" }}
|
sx={{ maxWidth: "300px", width: "100%" }}
|
||||||
cropAspectRatio={cropAspectRatio}
|
cropAspectRatio={cropAspectRatio}
|
||||||
imageUrl={question.content.back}
|
imageUrl={question.content.originalBack}
|
||||||
originalImageUrl={question.content.originalBack}
|
originalImageUrl={question.content.originalBack}
|
||||||
onImageUploadClick={async (file) => {
|
onImageUploadClick={async (file) => {
|
||||||
setPictureUploading(true);
|
setPictureUploading(true);
|
||||||
|
|
||||||
await uploadQuestionImage(question.id, quiz.qid, file, (question, url) => {
|
await uploadQuestionImage(
|
||||||
question.content.back = url;
|
question.id,
|
||||||
question.content.originalBack = url;
|
quiz.qid,
|
||||||
});
|
file,
|
||||||
|
(question, url) => {
|
||||||
|
question.content.back = url;
|
||||||
|
question.content.originalBack = url;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setOpenCropModal(true)
|
||||||
setPictureUploading(false);
|
setPictureUploading(false);
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
onDeleteClick={() => {
|
onDeleteClick={() => {
|
||||||
updateQuestion(question.id, (question) => {
|
updateQuestion(question.id, (question) => {
|
||||||
question.content.back = null;
|
question.content.back = null;
|
||||||
|
question.content.originalBack = null;
|
||||||
|
if ("editedUrlImagesList" in question.content) question.content.editedUrlImagesList = null;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onImageSaveClick={async (file) => {
|
onImageSavedClick={() => {
|
||||||
setPictureUploading(true);
|
setOpenCropModal(true)
|
||||||
|
|
||||||
await uploadQuestionImage(question.id, quiz.qid, file, (question, url) => {
|
|
||||||
question.content.back = url;
|
|
||||||
});
|
|
||||||
|
|
||||||
setPictureUploading(false);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<CropModalInit
|
||||||
|
originalImageUrl={question.content.originalBack}
|
||||||
|
editedUrlImagesList={question.content?.editedUrlImagesList}
|
||||||
|
questionId={question.id.toString()}
|
||||||
|
questionType={question.type}
|
||||||
|
quizId={quiz.qid}
|
||||||
|
open={openCropModal}
|
||||||
|
selfClose={() => setOpenCropModal(false)}
|
||||||
|
setPictureUploading={setPictureUploading}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,6 @@ export const ListOfImagesCardAnswer = ({
|
|||||||
quizId,
|
quizId,
|
||||||
questionId,
|
questionId,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
console.log('"' + answer + '"')
|
|
||||||
console.log(splitUserText('"' + answer + '"'))
|
|
||||||
|
|
||||||
return <Box
|
return <Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -36,9 +34,6 @@ export const ListOfImagesCardAnswer = ({
|
|||||||
.filter(text => text.length)
|
.filter(text => text.length)
|
||||||
.map(text => {
|
.map(text => {
|
||||||
const { Image, Description } = JSON.parse(text)
|
const { Image, Description } = JSON.parse(text)
|
||||||
console.log("Image, Descripton")
|
|
||||||
console.log(Image, Description)
|
|
||||||
console.log("Image, Descripton")
|
|
||||||
return (<>
|
return (<>
|
||||||
<img
|
<img
|
||||||
width={40}
|
width={40}
|
||||||
|
|||||||
@ -145,15 +145,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>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import UploadIcon from "@icons/UploadIcon";
|
import UploadIcon from "@icons/UploadIcon";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
ButtonBase,
|
ButtonBase,
|
||||||
@ -10,11 +10,13 @@ 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";
|
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { useState } from "react";
|
|
||||||
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";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
|
||||||
|
|
||||||
const allowedFileTypes = ["image/png", "image/jpeg", "image/gif"];
|
const allowedFileTypes = ["image/png", "image/jpeg", "image/gif"];
|
||||||
|
|
||||||
@ -25,7 +27,7 @@ interface Props {
|
|||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
originalImageUrl: string | null;
|
originalImageUrl: string | null;
|
||||||
onImageUploadClick: (image: Blob) => void;
|
onImageUploadClick: (image: Blob) => void;
|
||||||
onImageSaveClick: (image: Blob) => void;
|
onImageSavedClick?: () => void;
|
||||||
onDeleteClick: () => void;
|
onDeleteClick: () => void;
|
||||||
cropAspectRatio?: {
|
cropAspectRatio?: {
|
||||||
width: number;
|
width: number;
|
||||||
@ -41,22 +43,15 @@ export const DropZone = ({
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
originalImageUrl,
|
originalImageUrl,
|
||||||
onImageUploadClick,
|
onImageUploadClick,
|
||||||
onImageSaveClick,
|
onImageSavedClick,
|
||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
cropAspectRatio,
|
cropAspectRatio,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@ -68,7 +63,6 @@ export const DropZone = ({
|
|||||||
|
|
||||||
onImageUploadClick?.(file);
|
onImageUploadClick?.(file);
|
||||||
closeImageUploadModal();
|
closeImageUploadModal();
|
||||||
openCropModal(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
@ -102,18 +96,8 @@ export const DropZone = ({
|
|||||||
onClose={closeImageUploadModal}
|
onClose={closeImageUploadModal}
|
||||||
handleImageChange={handleImageUpload}
|
handleImageChange={handleImageUpload}
|
||||||
/>
|
/>
|
||||||
<CropModal
|
|
||||||
isOpen={isCropModalOpen}
|
|
||||||
imageBlob={imageBlob}
|
|
||||||
originalImageUrl={originalImageUrl}
|
|
||||||
setCropModalImageBlob={setCropModalImageBlob}
|
|
||||||
onClose={closeCropModal}
|
|
||||||
onSaveImageClick={onImageSaveClick}
|
|
||||||
cropAspectRatio={cropAspectRatio}
|
|
||||||
/>
|
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
onClick={
|
onClick={ () => onImageSavedClick &&imageUrl ? onImageSavedClick() : openImageUploadModal()
|
||||||
imageUrl ? () => openCropModal(imageUrl) : openImageUploadModal
|
|
||||||
}
|
}
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -152,24 +136,25 @@ export const DropZone = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
{imageUrl && (
|
{imageUrl && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={onDeleteClick}
|
onClick={onDeleteClick}
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
color: theme.palette.orange.main,
|
color: theme.palette.orange.main,
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
borderBottomRightRadius: 0,
|
borderBottomRightRadius: 0,
|
||||||
borderTopLeftRadius: 0,
|
borderTopLeftRadius: 0,
|
||||||
...deleteIconSx,
|
...deleteIconSx,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { produce } from "immer";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { defaultQuestionByType } from "../../constants/default";
|
import { defaultQuestionByType } from "../../constants/default";
|
||||||
|
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
|
||||||
import { replaceEmptyLinesToSpace } from "../../utils/replaceEmptyLinesToSpace";
|
import { replaceEmptyLinesToSpace } from "../../utils/replaceEmptyLinesToSpace";
|
||||||
import { RequestQueue } from "../../utils/requestQueue";
|
import { RequestQueue } from "../../utils/requestQueue";
|
||||||
import { QuestionsStore, useQuestionsStore } from "./store";
|
import { QuestionsStore, useQuestionsStore } from "./store";
|
||||||
|
|||||||
@ -1,45 +1,61 @@
|
|||||||
import InfoIcon from "@icons/InfoIcon";
|
|
||||||
import UploadIcon from "@icons/UploadIcon";
|
|
||||||
import { Box, Button, ButtonBase, Skeleton, Tooltip, Typography, useTheme } from "@mui/material";
|
|
||||||
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
|
|
||||||
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
|
||||||
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
|
|
||||||
import UploadBox from "@ui_kit/UploadBox";
|
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { UploadImageModal } from "../pages/Questions/UploadImage/UploadImageModal";
|
import {
|
||||||
import { VideoElement } from "../pages/startPage/VideoElement";
|
Box,
|
||||||
import { useCurrentQuiz } from "../stores/quizes/hooks";
|
Button,
|
||||||
import { useDisclosure } from "../utils/useDisclosure";
|
ButtonBase,
|
||||||
import { QuizQuestionPage, QuizQuestionResult } from "@frontend/squzanswerer";
|
Skeleton,
|
||||||
import UploadVideoModal from "@/pages/Questions/UploadVideoModal";
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
|
||||||
|
|
||||||
interface Props {
|
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
||||||
question: QuizQuestionPage | QuizQuestionResult;
|
import { UploadImageModal } from "@/pages/Questions/UploadImage/UploadImageModal";
|
||||||
|
import { VideoElement } from "@/pages/startPage/VideoElement";
|
||||||
|
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
||||||
|
import { useDisclosure } from "@/utils/useDisclosure";
|
||||||
|
import UploadBox from "@ui_kit/UploadBox";
|
||||||
|
import UploadIcon from "@icons/UploadIcon";
|
||||||
|
import InfoIcon from "@icons/InfoIcon";
|
||||||
|
|
||||||
|
import imge from "@/assets/card-1.png"
|
||||||
|
import { CropModalInit } from "./Modal/CropModal";
|
||||||
|
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
|
||||||
|
|
||||||
|
interface Iprops {
|
||||||
|
question: AnyTypedQuizQuestion;
|
||||||
cropAspectRatio: {
|
cropAspectRatio: {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio }) => {
|
export const MediaSelectionAndDisplay: FC<Iprops> = ({
|
||||||
|
question,
|
||||||
|
cropAspectRatio,
|
||||||
|
}) => {
|
||||||
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
|
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
|
||||||
const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false);
|
const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false);
|
||||||
|
const [openCropModal, setOpenCropModal] = useState(false);
|
||||||
const quizQid = useCurrentQuiz()?.qid;
|
const quizQid = useCurrentQuiz()?.qid;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } =
|
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] =
|
||||||
useCropModalState();
|
useDisclosure();
|
||||||
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
|
|
||||||
const [isVideoUploadDialogOpen, setIsVideoUploadDialogOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
async function handleImageUpload(file: File) {
|
async function handleImageUpload(file: File) {
|
||||||
setPictureUploading(true);
|
setPictureUploading(true);
|
||||||
|
|
||||||
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
|
|
||||||
question.content.back = url;
|
|
||||||
question.content.originalBack = url;
|
|
||||||
});
|
|
||||||
closeImageUploadModal();
|
closeImageUploadModal();
|
||||||
openCropModal(file, url);
|
const url = await uploadQuestionImage(
|
||||||
|
question.id,
|
||||||
|
quizQid,
|
||||||
|
file,
|
||||||
|
(question, url) => {
|
||||||
|
question.content.back = url;
|
||||||
|
question.content.originalBack = url;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setOpenCropModal(true)
|
||||||
|
|
||||||
setPictureUploading(false);
|
setPictureUploading(false);
|
||||||
}
|
}
|
||||||
@ -95,11 +111,10 @@ export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio
|
|||||||
}}
|
}}
|
||||||
variant="text"
|
variant="text"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
updateQuestion(question.id, (question) => {
|
updateQuestion(
|
||||||
if (!("useImage" in question.content)) return;
|
question.id,
|
||||||
|
(question) => (question.content.useImage = true),
|
||||||
question.content.useImage = true;
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Изображение
|
Изображение
|
||||||
@ -124,32 +139,30 @@ export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio
|
|||||||
Видео
|
Видео
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<UploadImageModal
|
|
||||||
isOpen={isImageUploadOpen}
|
<Box
|
||||||
onClose={closeImageUploadModal}
|
sx={{
|
||||||
handleImageChange={handleImageUpload}
|
display: "flex",
|
||||||
/>
|
flexDirection: "column",
|
||||||
<CropModal
|
|
||||||
isOpen={isCropModalOpen}
|
|
||||||
imageBlob={imageBlob}
|
|
||||||
originalImageUrl={originalImageUrl}
|
|
||||||
setCropModalImageBlob={setCropModalImageBlob}
|
|
||||||
onClose={closeCropModal}
|
|
||||||
onSaveImageClick={handleCropModalSaveClick}
|
|
||||||
onDeleteClick={() => {
|
|
||||||
updateQuestion(question.id, (question) => {
|
|
||||||
question.content.back = null;
|
|
||||||
question.content.originalBack = null;
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
cropAspectRatio={cropAspectRatio}
|
>
|
||||||
/>
|
<UploadImageModal
|
||||||
<UploadVideoModal
|
isOpen={isImageUploadOpen}
|
||||||
open={isVideoUploadDialogOpen}
|
onClose={closeImageUploadModal}
|
||||||
onClose={() => setIsVideoUploadDialogOpen(false)}
|
handleImageChange={handleImageUpload}
|
||||||
onUpload={handleVideoUpload}
|
/>
|
||||||
video={question.content.video}
|
<CropModalInit
|
||||||
/>
|
originalImageUrl={question.content.originalBack}
|
||||||
|
editedUrlImagesList={question.content?.editedUrlImagesList}
|
||||||
|
questionId={question.id}
|
||||||
|
questionType={question.type}
|
||||||
|
quizId={quizQid}
|
||||||
|
open={openCropModal}
|
||||||
|
selfClose={() => setOpenCropModal(false)}
|
||||||
|
setPictureUploading={setPictureUploading}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{question.content.useImage && (
|
{question.content.useImage && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -161,14 +174,14 @@ export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AddOrEditImageButton
|
<AddOrEditImageButton
|
||||||
imageSrc={question.content.back ?? undefined}
|
imageSrc={question.content.back}
|
||||||
uploading={pictureUploding}
|
uploading={pictureUploding}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
if (question.content.back) {
|
if (question.content.back) {
|
||||||
return openCropModal(question.content.back, question.content.originalBack);
|
setOpenCropModal(true)
|
||||||
|
} else {
|
||||||
|
openImageUploadModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
openImageUploadModal();
|
|
||||||
}}
|
}}
|
||||||
onPlusClick={() => {
|
onPlusClick={() => {
|
||||||
openImageUploadModal();
|
openImageUploadModal();
|
||||||
@ -221,6 +234,27 @@ export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio
|
|||||||
my: "20px",
|
my: "20px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
onChange={async (event) => {
|
||||||
|
setBackgroundUploading(true);
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
await uploadQuestionImage(
|
||||||
|
question.id,
|
||||||
|
quizQid,
|
||||||
|
file,
|
||||||
|
(question, url) => {
|
||||||
|
question.content.video = url;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setBackgroundUploading(false);
|
||||||
|
}}
|
||||||
|
hidden
|
||||||
|
accept=".mp4"
|
||||||
|
multiple
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
<UploadBox
|
<UploadBox
|
||||||
icon={<UploadIcon />}
|
icon={<UploadIcon />}
|
||||||
sx={{
|
sx={{
|
||||||
@ -246,7 +280,8 @@ export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)
|
||||||
</Box>
|
}
|
||||||
|
</Box >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,438 +0,0 @@
|
|||||||
import { devlog } from "@frontend/kitui";
|
|
||||||
import { ResetIcon } from "@icons/ResetIcon";
|
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
Modal,
|
|
||||||
Slider,
|
|
||||||
SxProps,
|
|
||||||
Theme,
|
|
||||||
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";
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CropModal: FC<Props> = ({
|
|
||||||
isOpen,
|
|
||||||
imageBlob,
|
|
||||||
originalImageUrl,
|
|
||||||
setCropModalImageBlob,
|
|
||||||
onSaveImageClick,
|
|
||||||
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 cropImageElementRef = useRef<HTMLImageElement>(null);
|
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(786));
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<Modal
|
|
||||||
open={isOpen}
|
|
||||||
onClose={() => {
|
|
||||||
resetEditState();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
aria-labelledby="modal-modal-title"
|
|
||||||
aria-describedby="modal-modal-description"
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
bgcolor: "background.paper",
|
|
||||||
boxShadow: 24,
|
|
||||||
padding: "20px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
width: isMobile ? "343px" : "620px",
|
|
||||||
height: isMobile ? "80vh" : undefined,
|
|
||||||
display: isMobile ? "flex" : undefined,
|
|
||||||
flexDirection: isMobile ? "column" : undefined,
|
|
||||||
justifyContent: isMobile ? "flex-start" : undefined,
|
|
||||||
overflow: isMobile ? "auto" : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
height: "320px",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{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",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
{onDeleteClick && (
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
onDeleteClick?.();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
height: "48px",
|
|
||||||
width: "48px",
|
|
||||||
p: 0,
|
|
||||||
color: theme.palette.orange.main,
|
|
||||||
borderRadius: "50%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
marginTop: "40px",
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
gap: "10px",
|
|
||||||
flexWrap: isMobile ? "wrap" : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={handleSaveOriginalImage}
|
|
||||||
disableRipple
|
|
||||||
sx={{
|
|
||||||
height: "48px",
|
|
||||||
color: "#7E2AEA",
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: "1px solid #7E2AEA",
|
|
||||||
px: "20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Сохранить оригинал
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSaveModifiedImage}
|
|
||||||
disableRipple
|
|
||||||
variant="contained"
|
|
||||||
sx={{
|
|
||||||
height: "48px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: "1px solid #7E2AEA",
|
|
||||||
marginRight: "10px",
|
|
||||||
px: "20px",
|
|
||||||
ml: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Сохранить редактированное
|
|
||||||
</Button>
|
|
||||||
</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);
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
53
src/ui_kit/Modal/CropModal/AlertModalDeleteImage.tsx
Normal file
53
src/ui_kit/Modal/CropModal/AlertModalDeleteImage.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Box, Button, Modal, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
cancelDelete: () => void;
|
||||||
|
deleteImage: () => void;
|
||||||
|
}
|
||||||
|
export default ({
|
||||||
|
open,
|
||||||
|
cancelDelete,
|
||||||
|
deleteImage,
|
||||||
|
}: Props) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down(450));
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={cancelDelete}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
bgcolor: "background.paper",
|
||||||
|
boxShadow: 24,
|
||||||
|
borderRadius: "12px",
|
||||||
|
width: isMobile ? "200px" : "350px",
|
||||||
|
height: "350px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{
|
||||||
|
// height: isMobile ? "91px" : "70px",
|
||||||
|
backgroundColor: "#F2F3F7",
|
||||||
|
padding: isMobile ? "25px 20px 24px 20px" : "25px 43px 24px 20px",
|
||||||
|
borderRadius: "8px 8px 0px 0px",
|
||||||
|
color: "#9A9AAF",
|
||||||
|
fontSize: "18px",
|
||||||
|
lineHeight: "21.33px"
|
||||||
|
}}>
|
||||||
|
Вы уверены, что хотите удалить всю картинку и каждую настройку?
|
||||||
|
</Typography>
|
||||||
|
<Button sx={{margin: "25px"}} variant="contained" onClick={cancelDelete}>нет</Button>
|
||||||
|
<Button sx={{margin: "25px"}} variant="outlined" onClick={deleteImage}>да</Button>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
328
src/ui_kit/Modal/CropModal/CropGeneral.tsx
Normal file
328
src/ui_kit/Modal/CropModal/CropGeneral.tsx
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
import { FC, useCallback, useEffect, 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";
|
||||||
|
import { devlog } from "@frontend/kitui";
|
||||||
|
|
||||||
|
const styleSlider: SxProps<Theme> = {
|
||||||
|
color: "#7E2AEA",
|
||||||
|
height: "10px",
|
||||||
|
p: "18px 0",
|
||||||
|
"& .MuiSlider-track": {
|
||||||
|
border: "none",
|
||||||
|
},
|
||||||
|
"& .MuiSlider-rail": {
|
||||||
|
backgroundColor: "#F2F3F7",
|
||||||
|
border: `1px solid #9A9AAF`,
|
||||||
|
},
|
||||||
|
"& .MuiSlider-thumb": {
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
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 { crop, darken } = editedImage.newRules;
|
||||||
|
|
||||||
|
const cropImageElementRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
const [imageWidth, setImageWidth] = useState<number | null>(null);
|
||||||
|
const [imageHeight, setImageHeight] = useState<number | null>(null);
|
||||||
|
|
||||||
|
async function handleRotateClick() {
|
||||||
|
|
||||||
|
if (cropImageElementRef.current !== null) {
|
||||||
|
try {
|
||||||
|
const blob = await getRotatedImageBlob(cropImageElementRef.current);
|
||||||
|
|
||||||
|
editedImagesChange((old) => {
|
||||||
|
const newRotate = old.newRules.rotate + 90;
|
||||||
|
|
||||||
|
return {
|
||||||
|
newRules: {
|
||||||
|
...old.newRules,
|
||||||
|
rotate: newRotate > 360 ? 0 : newRotate
|
||||||
|
},
|
||||||
|
url: (URL.createObjectURL(blob))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
devlog("getRotatedImageBlob error", error);
|
||||||
|
enqueueSnackbar("Не удалось изменить изображение");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcCrop = () => {
|
||||||
|
if (cropImageElementRef.current) {
|
||||||
|
editedImagesChange((old) => ({
|
||||||
|
newRules: {
|
||||||
|
...old.newRules,
|
||||||
|
crop: getInitialCrop(
|
||||||
|
cropImageElementRef.current?.width,
|
||||||
|
cropImageElementRef.current?.height,
|
||||||
|
cropAspectRatio
|
||||||
|
? cropAspectRatio.width / cropAspectRatio.height
|
||||||
|
: 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!crop.width && !crop.height) { calcCrop() }
|
||||||
|
}, [crop])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: "320px",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: "0 20px",
|
||||||
|
marginTop: isMobile ? "19px" : "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ReactCrop
|
||||||
|
crop={crop}
|
||||||
|
onChange={(_, percentCrop) => editedImagesChange((old) => {
|
||||||
|
return ({
|
||||||
|
newRules: {
|
||||||
|
...old.newRules,
|
||||||
|
crop: percentCrop
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
|
||||||
|
minWidth={5}
|
||||||
|
minHeight={5}
|
||||||
|
locked
|
||||||
|
aspect={
|
||||||
|
cropAspectRatio
|
||||||
|
? cropAspectRatio.width / cropAspectRatio.height
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
id="imgid"
|
||||||
|
onLoad={(e) => {
|
||||||
|
setImageWidth(e.currentTarget.naturalWidth);
|
||||||
|
setImageHeight(e.currentTarget.naturalHeight);
|
||||||
|
|
||||||
|
calcCrop()
|
||||||
|
}}
|
||||||
|
ref={cropImageElementRef}
|
||||||
|
alt="Crop me"
|
||||||
|
src={editedImage.url}
|
||||||
|
style={{
|
||||||
|
filter: `brightness(${100 - editedImage.newRules.darken}%)`,
|
||||||
|
maxWidth: "100%",
|
||||||
|
height: "320px",
|
||||||
|
maxHeight: "320px",
|
||||||
|
display: "block",
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
crossOrigin = 'anonymous'
|
||||||
|
/>
|
||||||
|
</ReactCrop>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: isMobile ? "20px" : "48px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "end",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "0 20px",
|
||||||
|
flexDirection: isMobile ? "column" : "",
|
||||||
|
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton onClick={handleRotateClick}
|
||||||
|
sx={{
|
||||||
|
mb: "11px",
|
||||||
|
p: "0",
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ResetIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
flexDirection: isMobile ? "column" : "",
|
||||||
|
width: isMobile ? "100%" : "auto",
|
||||||
|
gap: "24px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
|
||||||
|
Размер
|
||||||
|
</Typography>
|
||||||
|
<Slider
|
||||||
|
sx={[
|
||||||
|
styleSlider,
|
||||||
|
{
|
||||||
|
width: isMobile ? undefined : "248px",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={crop.width}
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
step={0.1}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
if (typeof newValue === "number") handleSizeChange(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ color: "#9A9AAF", fontSize: "16px", ml: "-1px", }}>
|
||||||
|
Затемнение
|
||||||
|
</Typography>
|
||||||
|
<Slider
|
||||||
|
sx={[
|
||||||
|
styleSlider,
|
||||||
|
{
|
||||||
|
|
||||||
|
width: isMobile ? undefined : "248px",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={darken}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
onChange={(_, newValue) => editedImagesChange((old) => ({
|
||||||
|
newRules: {
|
||||||
|
...old.newRules,
|
||||||
|
darken: newValue as number
|
||||||
|
}
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</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,
|
||||||
|
);
|
||||||
|
}
|
||||||
116
src/ui_kit/Modal/CropModal/CropModal.tsx
Normal file
116
src/ui_kit/Modal/CropModal/CropModal.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { FC, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import WorkSpace from "./WorkSpace";
|
||||||
|
import { NavigationPanel } from "./NavigationPanel";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Modal,
|
||||||
|
Typography,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULTCROPRULES,
|
||||||
|
type CropModalProps,
|
||||||
|
type EditedImage,
|
||||||
|
type ScreenStepsTypes
|
||||||
|
} from "@/model/CropModal/CropModal";
|
||||||
|
|
||||||
|
const PriorityOfSteps = ["desktop", "tablet", "mobile", "small"];
|
||||||
|
|
||||||
|
export type EditedImagesChangeType = (changed: (old: EditedImage) => Partial<EditedImage>) => void;
|
||||||
|
|
||||||
|
export const CropModal: FC<CropModalProps> = ({
|
||||||
|
open,
|
||||||
|
editedImages,
|
||||||
|
workSpaceTypes,
|
||||||
|
originalImageUrl,
|
||||||
|
|
||||||
|
setEditedImages,
|
||||||
|
onSaveImageClick,
|
||||||
|
closeCropModal,
|
||||||
|
onDeleteClick,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down(786));
|
||||||
|
|
||||||
|
const [currentStep, setCurrentStep] = useState<number>(0);
|
||||||
|
|
||||||
|
const currentStepName: ScreenStepsTypes = useMemo(() => {
|
||||||
|
const mainSteps = Object.keys(workSpaceTypes);
|
||||||
|
const POS = PriorityOfSteps.filter(POS => mainSteps.find(e => POS === e))
|
||||||
|
return POS[currentStep] as ScreenStepsTypes
|
||||||
|
}, [currentStep])
|
||||||
|
|
||||||
|
const editedImagesChange: EditedImagesChangeType = (changed) => {
|
||||||
|
setEditedImages(old => {
|
||||||
|
const newData = { ...old };
|
||||||
|
newData[currentStepName] = { ...old[currentStepName], ...changed(old[currentStepName]) };
|
||||||
|
return newData;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetImage = () => {
|
||||||
|
editedImagesChange(() => ({
|
||||||
|
url: originalImageUrl,
|
||||||
|
newRules: DEFAULTCROPRULES,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={closeCropModal}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
bgcolor: "background.paper",
|
||||||
|
boxShadow: 24,
|
||||||
|
borderRadius: "12px",
|
||||||
|
width: isMobile ? "343px" : "620px",
|
||||||
|
height: isMobile ? "80vh" : undefined,
|
||||||
|
display: isMobile ? "flex" : undefined,
|
||||||
|
flexDirection: isMobile ? "column" : undefined,
|
||||||
|
justifyContent: isMobile ? "flex-start" : undefined,
|
||||||
|
overflow: isMobile ? "auto" : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{
|
||||||
|
// height: isMobile ? "91px" : "70px",
|
||||||
|
backgroundColor: "#F2F3F7",
|
||||||
|
padding: isMobile ? "25px 20px 24px 20px" : "25px 43px 24px 20px",
|
||||||
|
borderRadius: "8px 8px 0px 0px",
|
||||||
|
color: "#9A9AAF",
|
||||||
|
fontSize: "18px",
|
||||||
|
lineHeight: "21.33px"
|
||||||
|
}}>
|
||||||
|
Настройте вариант отображения картинки на разных девайсах
|
||||||
|
</Typography>
|
||||||
|
<WorkSpace
|
||||||
|
//Информация о изменяемой сейчас картинке
|
||||||
|
editedImage={editedImages[currentStepName]}
|
||||||
|
//По каким правилам меняем
|
||||||
|
cropAspectRatio={workSpaceTypes[currentStepName].ratio}
|
||||||
|
currentStep={currentStep}
|
||||||
|
currentStepName={currentStepName}
|
||||||
|
|
||||||
|
editedImagesChange={editedImagesChange}
|
||||||
|
onDeleteClick={onDeleteClick}
|
||||||
|
/>
|
||||||
|
<NavigationPanel
|
||||||
|
currentStep={currentStep}
|
||||||
|
setCurrentStep={setCurrentStep}
|
||||||
|
totalSteps={Object.keys(workSpaceTypes).length}
|
||||||
|
onSaveImageClick={onSaveImageClick}
|
||||||
|
resetImage={resetImage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import {Box} from "@mui/material";
|
||||||
|
|
||||||
|
export default function DevaceDesktopIcon() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: "36px",
|
||||||
|
width: "36px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: "6px",
|
||||||
|
backgroundColor: "#EEE4FC"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2" y="17" width="20" height="3" rx="1" stroke="#7E2AEA" strokeWidth="1.5"/>
|
||||||
|
<rect x="3" y="5" width="18" height="12" rx="1" stroke="#7E2AEA" strokeWidth="1.5"/>
|
||||||
|
<path d="M14 5.5L10 5.5" stroke="#7E2AEA" strokeWidth="2.5" strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import {Box} from "@mui/material";
|
||||||
|
|
||||||
|
export default function DevaceMobileIcon() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: "36px",
|
||||||
|
width: "36px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: "6px",
|
||||||
|
backgroundColor: "#EEE4FC"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="6" y="2" width="12" height="20" rx="3" stroke="#7E2AEA" strokeWidth="1.5"/>
|
||||||
|
<path d="M14 2.5L10 2.5" stroke="#7E2AEA" strokeWidth="2.5" strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/ui_kit/Modal/CropModal/IconCropModal/DevaceSmallIcon.tsx
Normal file
23
src/ui_kit/Modal/CropModal/IconCropModal/DevaceSmallIcon.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {Box} from "@mui/material";
|
||||||
|
|
||||||
|
export default function DevaceSmallIcon() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: "36px",
|
||||||
|
width: "36px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: "6px",
|
||||||
|
backgroundColor: "#EEE4FC"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="7.5" y="2" width="9" height="20" rx="3" stroke="#7E2AEA" strokeWidth="1.5"/>
|
||||||
|
<path d="M13.5 2.5L10.5 2.5" stroke="#7E2AEA" strokeWidth="2.5" strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import {Box} from "@mui/material";
|
||||||
|
|
||||||
|
export default function DevaceTabletIcon() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: "36px",
|
||||||
|
width: "36px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: "6px",
|
||||||
|
backgroundColor: "#EEE4FC"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19.5 19.5V4.5C19.5 3.67157 18.8284 3 18 3L6 3C5.17157 3 4.5 3.67157 4.5 4.5L4.5 19.5C4.5 20.3284 5.17157 21 6 21H18C18.8284 21 19.5 20.3284 19.5 19.5Z" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M14 4L10 4" stroke="#7E2AEA" strokeWidth="2.5" strokeLinecap="round"/>
|
||||||
|
<path d="M4.5 18H19.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
112
src/ui_kit/Modal/CropModal/NavigationPanel.tsx
Normal file
112
src/ui_kit/Modal/CropModal/NavigationPanel.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
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: "18px",
|
||||||
|
padding: "0 20px 20px",
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
gap: "5px",
|
||||||
|
flexWrap: isMobile ? "wrap" : undefined,
|
||||||
|
justifyContent: "space-between"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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: "10px",
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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 19px",
|
||||||
|
width: isMobile ? "100%" : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{lastStep ?
|
||||||
|
"Сохранить редактированное" : "Далее"
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
127
src/ui_kit/Modal/CropModal/WorkSpace.tsx
Normal file
127
src/ui_kit/Modal/CropModal/WorkSpace.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
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 type { CropAspectRatio, EditedImage, ScreenStepsTypes } from "@/model/CropModal/CropModal"
|
||||||
|
import { EditedImagesChangeType } from "./CropModal";
|
||||||
|
import AmoTrash from "@/assets/icons/AmoTrash";
|
||||||
|
|
||||||
|
|
||||||
|
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: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function WorkSpace({
|
||||||
|
editedImage,
|
||||||
|
currentStep,
|
||||||
|
currentStepName,
|
||||||
|
cropAspectRatio,
|
||||||
|
editedImagesChange,
|
||||||
|
onDeleteClick,
|
||||||
|
}: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const currentModel = useMemo(() => (
|
||||||
|
modalModels[currentStepName]
|
||||||
|
), [currentStepName]);
|
||||||
|
|
||||||
|
// console.log(" промежуточный рендер которому должно быть похуй")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: "12px",
|
||||||
|
padding: "0 20px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<IconButton
|
||||||
|
onClick={onDeleteClick}
|
||||||
|
sx={{
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
p: 0,
|
||||||
|
color: theme.palette.orange.main,
|
||||||
|
borderRadius: "50%",
|
||||||
|
mb:"5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AmoTrash/>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography sx={{
|
||||||
|
fontSize: "13.8px",
|
||||||
|
color: "#9A9AAF",
|
||||||
|
lineHeight: "16px",
|
||||||
|
}}>
|
||||||
|
{currentStep + 1 + " шаг"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
<CropGeneral
|
||||||
|
editedImage={editedImage}
|
||||||
|
cropAspectRatio={cropAspectRatio}
|
||||||
|
editedImagesChange={editedImagesChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
247
src/ui_kit/Modal/CropModal/index.tsx
Normal file
247
src/ui_kit/Modal/CropModal/index.tsx
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
import { FC, useEffect, useState } from "react";
|
||||||
|
import { CropModal } from "./CropModal";
|
||||||
|
import {
|
||||||
|
type ScreenStepsTypes,
|
||||||
|
type CropOnOpenType,
|
||||||
|
type EditedImages,
|
||||||
|
type CropOnDeleteIamgeClick,
|
||||||
|
type Writable,
|
||||||
|
workSpaceTypesList,
|
||||||
|
DEFAULTCROPRULES,
|
||||||
|
EditedImage,
|
||||||
|
WorkSpaceTypes,
|
||||||
|
} from "@/model/CropModal/CropModal"
|
||||||
|
import { isImageBlobAGifFile } from "@/utils/isImageBlobAGifFile";
|
||||||
|
import AlertModalDeleteImage from "./AlertModalDeleteImage"
|
||||||
|
import { getModifiedImageBlob } from "./utils/imageManipulation";
|
||||||
|
import { updateQuestion, uploadQuestionImage } from "@/stores/questions/actions";
|
||||||
|
|
||||||
|
export const CropModalInit: FC<CropOnOpenType> = ({
|
||||||
|
open,
|
||||||
|
originalImageUrl,
|
||||||
|
imageBlob,
|
||||||
|
editedUrlImagesList,
|
||||||
|
questionId,
|
||||||
|
questionType,
|
||||||
|
quizId,
|
||||||
|
variantId,
|
||||||
|
|
||||||
|
selfClose,
|
||||||
|
setPictureUploading,
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const [acceptedOriginalImageUrl, setOriginalImageUrl] = useState("");
|
||||||
|
const [editedImages, setEditedImages] = useState<Record<keyof WorkSpaceTypes, EditedImage> | null>(null);
|
||||||
|
const [readyDelete, setReadyDelete] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
//Если нам не дали с чем работать, то и работать не нужно
|
||||||
|
if (Boolean(imageBlob) || Boolean(originalImageUrl)) {
|
||||||
|
(async () => {
|
||||||
|
let newImageBlob = imageBlob;
|
||||||
|
if (originalImageUrl !== undefined) {
|
||||||
|
const response = await fetch(originalImageUrl);
|
||||||
|
newImageBlob = await response.blob();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newImageBlob) {
|
||||||
|
const isGif = await isImageBlobAGifFile(newImageBlob);
|
||||||
|
if (isGif) {
|
||||||
|
saveImagesAndRules(newImageBlob);
|
||||||
|
return setPictureUploading(false);
|
||||||
|
}
|
||||||
|
//Для работы нам нужны урлы. Оригинальной и редактированных картинок
|
||||||
|
let newOriginalImageUrl = originalImageUrl || URL.createObjectURL(newImageBlob)
|
||||||
|
if (questionId) {
|
||||||
|
if (questionType) {
|
||||||
|
const workSpaceTypesCONST = workSpaceTypesList[questionType];
|
||||||
|
|
||||||
|
type WritableWorkSpaceTypesCONST = Writable<typeof workSpaceTypesCONST>;
|
||||||
|
const writableEditedImagesList: WritableWorkSpaceTypesCONST = { ...workSpaceTypesList[questionType] }; // Теперь object не readonly
|
||||||
|
|
||||||
|
type Keys = keyof typeof writableEditedImagesList;
|
||||||
|
const newEditedImagesList = {} as Record<Keys, EditedImage>;
|
||||||
|
|
||||||
|
|
||||||
|
//Если вопрос умничка и знает что ему нужно
|
||||||
|
if (Boolean(editedUrlImagesList)) {
|
||||||
|
|
||||||
|
|
||||||
|
for (let k in editedUrlImagesList) {
|
||||||
|
let key = k as Keys;
|
||||||
|
|
||||||
|
if (key in writableEditedImagesList) {
|
||||||
|
newEditedImagesList[key] = {
|
||||||
|
...writableEditedImagesList[key],
|
||||||
|
url: editedUrlImagesList[key] ?? originalImageUrl,
|
||||||
|
newRules: DEFAULTCROPRULES
|
||||||
|
} as EditedImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // Если в первый раз, то создаём ему бланк
|
||||||
|
for (let k in workSpaceTypesCONST) {
|
||||||
|
let key = k as Keys;
|
||||||
|
|
||||||
|
if (key in writableEditedImagesList) {
|
||||||
|
newEditedImagesList[key] = {
|
||||||
|
...writableEditedImagesList[key],
|
||||||
|
url: originalImageUrl,
|
||||||
|
newRules: DEFAULTCROPRULES
|
||||||
|
} as EditedImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setOriginalImageUrl(newOriginalImageUrl);
|
||||||
|
setEditedImages(newEditedImagesList);
|
||||||
|
} else {
|
||||||
|
throw new Error("Не передан тип вопроса")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Не передан id вопроса")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [open, originalImageUrl, editedUrlImagesList, questionId, questionType])
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setReadyDelete(false)
|
||||||
|
setOriginalImageUrl("");
|
||||||
|
setEditedImages(null);
|
||||||
|
selfClose()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCropModalDeleteImageClick = () => {
|
||||||
|
if (variantId) {//Работаем с вариантом
|
||||||
|
updateQuestion(questionId, (question) => {
|
||||||
|
if ("variants" in question.content) {
|
||||||
|
|
||||||
|
const variant = question.content.variants.find(
|
||||||
|
(variant) => variant.id === variantId,
|
||||||
|
);
|
||||||
|
if (!variant) return console.error("В вопросе не был найден вариант ", variantId);
|
||||||
|
|
||||||
|
variant.extendedText = "";
|
||||||
|
variant.originalImageUrl = "";
|
||||||
|
if ("editedUrlImagesList" in variant) variant.editedUrlImagesList = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {//Работаем с контентом
|
||||||
|
updateQuestion(questionId, (question) => {
|
||||||
|
question.content.back = null;
|
||||||
|
question.content.originalBack = null;
|
||||||
|
if ("editedUrlImagesList" in question.content) question.content.editedUrlImagesList = null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//сохранить пустую строку и дефолтные настройки картинки в самом вопросе, не информируя БД о удалении картинки
|
||||||
|
closeModal()
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveImagesAndRules = async () => {
|
||||||
|
setPictureUploading(true)
|
||||||
|
if (editedImages === null) return;
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
|
||||||
|
//массив запросов
|
||||||
|
const requests: Promise<void>[] = [];
|
||||||
|
|
||||||
|
//Преобразовываем и отправляем на бек картинки.
|
||||||
|
|
||||||
|
if (variantId) {//Работаем с вариантом
|
||||||
|
for (let k in editedImages) {
|
||||||
|
requests.push(new Promise(async (resolve) => {
|
||||||
|
|
||||||
|
let key = k;
|
||||||
|
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.crossOrigin = 'anonymous';
|
||||||
|
img.src = editedImages[key].url;
|
||||||
|
const blob = await getModifiedImageBlob(img, editedImages[key].newRules.crop, editedImages[key].newRules.darken);
|
||||||
|
|
||||||
|
uploadQuestionImage(questionId, quizId, blob, (question, url) => {
|
||||||
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const variant = question.content.variants.find(
|
||||||
|
(variant) => variant.id === variantId,
|
||||||
|
);
|
||||||
|
if (!variant) return;
|
||||||
|
if (!variant.editedUrlImagesList) variant.editedUrlImagesList = {}
|
||||||
|
|
||||||
|
variant.editedUrlImagesList[key] = url;
|
||||||
|
resolve()
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {//Работаем с контентом
|
||||||
|
|
||||||
|
for (let k in editedImages) {
|
||||||
|
requests.push(new Promise(async (resolve) => {
|
||||||
|
|
||||||
|
let key = k;
|
||||||
|
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.crossOrigin = 'anonymous';
|
||||||
|
img.src = editedImages[key].url;
|
||||||
|
const blob = await getModifiedImageBlob(img, editedImages[key].newRules.crop, editedImages[key].newRules.darken);
|
||||||
|
|
||||||
|
uploadQuestionImage(questionId, quizId, blob, (question, url) => {
|
||||||
|
if (!question.content.editedUrlImagesList) question.content.editedUrlImagesList = {};
|
||||||
|
|
||||||
|
question.content.editedUrlImagesList[key] = url;
|
||||||
|
resolve()
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(requests)
|
||||||
|
setPictureUploading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (acceptedOriginalImageUrl.length === 0) return <></>
|
||||||
|
if (workSpaceTypesList[questionType] === undefined) return <></>
|
||||||
|
if (editedImages === null) return <></>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CropModal
|
||||||
|
open={open}
|
||||||
|
editedImages={editedImages}
|
||||||
|
workSpaceTypes={workSpaceTypesList[questionType]}
|
||||||
|
originalImageUrl={acceptedOriginalImageUrl}
|
||||||
|
|
||||||
|
setEditedImages={setEditedImages}
|
||||||
|
onSaveImageClick={saveImagesAndRules}
|
||||||
|
closeCropModal={closeModal}
|
||||||
|
onDeleteClick={() => setReadyDelete(true)}
|
||||||
|
/>
|
||||||
|
<AlertModalDeleteImage
|
||||||
|
open={readyDelete}
|
||||||
|
cancelDelete={() => setReadyDelete(false)}
|
||||||
|
deleteImage={handleCropModalDeleteImageClick}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// function handleCropModalSaveClick(imageBlob: Blob) { OptionsPicture
|
||||||
|
// if (!selectedVariantId) return;
|
||||||
|
|
||||||
|
// uploadQuestionImage(question.id, quizQid, 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;
|
||||||
|
// });
|
||||||
|
// }
|
||||||
44
src/ui_kit/Modal/utils/imageManipulation.ts → src/ui_kit/Modal/CropModal/utils/imageManipulation.ts
44
src/ui_kit/Modal/utils/imageManipulation.ts → src/ui_kit/Modal/CropModal/utils/imageManipulation.ts
@ -1,4 +1,4 @@
|
|||||||
import { PixelCrop } from "react-image-crop";
|
import { PercentCrop } from "react-image-crop";
|
||||||
|
|
||||||
export function getRotatedImageBlob(image: HTMLImageElement) {
|
export function getRotatedImageBlob(image: HTMLImageElement) {
|
||||||
return new Promise<Blob>((resolve, reject) => {
|
return new Promise<Blob>((resolve, reject) => {
|
||||||
@ -23,7 +23,7 @@ export function getRotatedImageBlob(image: HTMLImageElement) {
|
|||||||
|
|
||||||
export function getModifiedImageBlob(
|
export function getModifiedImageBlob(
|
||||||
image: HTMLImageElement,
|
image: HTMLImageElement,
|
||||||
crop: PixelCrop,
|
crop: PercentCrop,
|
||||||
darken: number,
|
darken: number,
|
||||||
) {
|
) {
|
||||||
return new Promise<Blob>((resolve, reject) => {
|
return new Promise<Blob>((resolve, reject) => {
|
||||||
@ -31,37 +31,25 @@ export function getModifiedImageBlob(
|
|||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return reject(new Error("No 2d context"));
|
if (!ctx) return reject(new Error("No 2d context"));
|
||||||
|
|
||||||
const scale = 1;
|
// Пропорции исходного изображения
|
||||||
const scaleX = image.naturalWidth / image.width;
|
const originalWidth = image.width;
|
||||||
const scaleY = image.naturalHeight / image.height;
|
const originalHeight = image.height;
|
||||||
const pixelRatio = window.devicePixelRatio;
|
|
||||||
|
|
||||||
canvas.width = Math.floor(crop.width * scaleX * pixelRatio);
|
// Вычисляем размеры для обрезки в пикселях
|
||||||
canvas.height = Math.floor(crop.height * scaleY * pixelRatio);
|
const cropWidth = (crop.width / 100) * originalWidth;
|
||||||
|
const cropHeight = (crop.height / 100) * originalHeight;
|
||||||
|
const cropX = (crop.x / 100) * originalWidth;
|
||||||
|
const cropY = (crop.y / 100) * originalHeight;
|
||||||
|
|
||||||
ctx.scale(pixelRatio, pixelRatio);
|
// Устанавливаем размеры канваса
|
||||||
ctx.imageSmoothingQuality = "high";
|
canvas.width = cropWidth;
|
||||||
|
canvas.height = cropHeight;
|
||||||
|
|
||||||
const cropX = crop.x * scaleX;
|
// Обрезаем изображение
|
||||||
const cropY = crop.y * scaleY;
|
|
||||||
|
|
||||||
const centerX = image.naturalWidth / 2;
|
|
||||||
const centerY = image.naturalHeight / 2;
|
|
||||||
|
|
||||||
ctx.translate(-cropX, -cropY);
|
|
||||||
ctx.translate(centerX, centerY);
|
|
||||||
ctx.scale(scale, scale);
|
|
||||||
ctx.translate(-centerX, -centerY);
|
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
image,
|
image,
|
||||||
0,
|
cropX, cropY, cropWidth, cropHeight, // Исходная область (x, y, width, height)
|
||||||
0,
|
0, 0, cropWidth, cropHeight // Целевая область (x, y, width, height)
|
||||||
image.naturalWidth,
|
|
||||||
image.naturalHeight,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
image.naturalWidth,
|
|
||||||
image.naturalHeight,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (darken > 0) {
|
if (darken > 0) {
|
||||||
@ -30,7 +30,7 @@ export const parseAxiosError = (nativeError: unknown): [string, number?] => {
|
|||||||
|
|
||||||
//ДЛЯ ОПЛАТЫ ТАРИФА
|
//ДЛЯ ОПЛАТЫ ТАРИФА
|
||||||
if(error.response.status === 402) {
|
if(error.response.status === 402) {
|
||||||
console.log(error.response?.data.message)
|
console.error(error.response?.data.message)
|
||||||
return error.response?.data.message
|
return error.response?.data.message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user