diff --git a/src/assets/icons/CropIcon.tsx b/src/assets/icons/CropIcon.tsx index 080ca8e6..520e90d9 100644 --- a/src/assets/icons/CropIcon.tsx +++ b/src/assets/icons/CropIcon.tsx @@ -1,4 +1,4 @@ -import { FC } from "react"; +import { FC, SVGProps } from "react"; export const CropIcon: FC = () => ( ( - {(provided, snapshot) => ( - + {(provided) => ( + - {dropPlaceIndex === index && !snapshot.mode && ( - - - - - )} )} diff --git a/src/pages/Questions/DraggableList/QuestionPageCard.tsx b/src/pages/Questions/DraggableList/QuestionPageCard.tsx index 5607ebde..a60c3b53 100644 --- a/src/pages/Questions/DraggableList/QuestionPageCard.tsx +++ b/src/pages/Questions/DraggableList/QuestionPageCard.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { useParams } from "react-router-dom"; import { Box, @@ -16,6 +17,7 @@ import SwitchQuestionsPage from "../SwitchQuestionsPage"; import { questionStore, updateQuestionsList, + createQuestion, copyQuestion, removeQuestion, } from "@root/questions"; @@ -39,12 +41,14 @@ import Slider from "@icons/questionsPage/slider"; import Download from "@icons/questionsPage/download"; import Page from "@icons/questionsPage/page"; import RatingIcon from "@icons/questionsPage/rating"; +import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg"; import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; interface Props { totalIndex: number; draggableProps: DraggableProvidedDragHandleProps | null | undefined; + isDragging: boolean; } const IconAndrom = (isExpanded: boolean, switchState: string) => { @@ -133,7 +137,9 @@ const IconAndrom = (isExpanded: boolean, switchState: string) => { export default function QuestionsPageCard({ totalIndex, draggableProps, + isDragging, }: Props) { + const [plusVisible, setPlusVisible] = useState(false); const quizId = Number(useParams().quizId); const theme = useTheme(); const { listQuestions } = questionStore(); @@ -141,125 +147,165 @@ export default function QuestionsPageCard({ listQuestions[quizId][totalIndex]; return ( - - + - - { - updateQuestionsList(quizId, totalIndex, { - title: e.target.value, - }); - console.log(listQuestions[quizId][totalIndex].title); - }} - InputProps={{ - startAdornment: ( - - {IconAndrom(isExpanded, switchState)} - - ), - }} - sx={{ - "& .MuiInputBase-root": { - color: isExpanded ? "#9A9AAF" : "white", - backgroundColor: isExpanded - ? theme.palette.background.default - : "transparent", - height: "48px", - borderRadius: "10px", - }, - }} - inputProps={{ - sx: { - borderRadius: "10px", - fontSize: "18px", - lineHeight: "21px", - py: 0, - paddingLeft: switchState.length === 0 ? 0 : "18px", - }, - }} - /> - - - - updateQuestionsList(quizId, totalIndex, { expanded: !isExpanded }) - } - > - {isExpanded ? : } - - {isExpanded ? ( - <> - ) : ( - - } - checkedIcon={} - /> - } - label={""} - sx={{ - color: theme.palette.grey2.main, - ml: "-9px", - mr: 0, - userSelect: "none", - }} - /> - copyQuestion(quizId, totalIndex)}> - - - removeQuestion(quizId, totalIndex)} - > - - - - )} - - - - - - - {isExpanded && ( - {switchState.length === 0 ? ( - + + { + updateQuestionsList(quizId, totalIndex, { + title: e.target.value, + }); + console.log(listQuestions[quizId][totalIndex].title); + }} + InputProps={{ + startAdornment: ( + + {IconAndrom(isExpanded, switchState)} + + ), + }} + sx={{ + "& .MuiInputBase-root": { + color: isExpanded ? "#9A9AAF" : "white", + backgroundColor: isExpanded + ? theme.palette.background.default + : "transparent", + height: "48px", + borderRadius: "10px", + }, + }} + inputProps={{ + sx: { + borderRadius: "10px", + fontSize: "18px", + lineHeight: "21px", + py: 0, + paddingLeft: switchState.length === 0 ? 0 : "18px", + }, + }} + /> + + + + updateQuestionsList(quizId, totalIndex, { expanded: !isExpanded }) + } + > + {isExpanded ? ( + + ) : ( + + )} + + {isExpanded ? ( + <> ) : ( - + + } + checkedIcon={} + /> + } + label={""} + sx={{ + color: theme.palette.grey2.main, + ml: "-9px", + mr: 0, + userSelect: "none", + }} + /> + copyQuestion(quizId, totalIndex)}> + + + removeQuestion(quizId, totalIndex)} + > + + + )} + + + + + - )} - + {isExpanded && ( + + {switchState.length === 0 ? ( + + ) : ( + + )} + + )} + + setPlusVisible(true)} + onMouseLeave={() => setPlusVisible(false)} + sx={{ + maxWidth: "825px", + display: "flex", + alignItems: "center", + height: "40px", + cursor: "pointer", + }} + > + createQuestion(quizId, totalIndex + 1)} + sx={{ + display: plusVisible && !isDragging ? "flex" : "none", + width: "100%", + alignItems: "center", + columnGap: "10px", + }} + > + + + + + ); } diff --git a/src/pages/Questions/DraggableList/index.tsx b/src/pages/Questions/DraggableList/index.tsx index 09aa689b..8db7887e 100644 --- a/src/pages/Questions/DraggableList/index.tsx +++ b/src/pages/Questions/DraggableList/index.tsx @@ -12,16 +12,9 @@ import { reorder } from "./helper"; import type { DropResult } from "react-beautiful-dnd"; export const DraggableList = () => { - const [draggableId, setDraggableId] = useState(-1); - const [dropPlaceIndex, setDropPlaceIndex] = useState(-1); const quizId = Number(useParams().quizId); const { listQuestions } = questionStore(); - const onDrop = () => { - setDraggableId(-1); - setDropPlaceIndex(-1); - }; - const onDragEnd = ({ destination, source }: DropResult) => { if (destination) { const newItems = reorder( @@ -35,29 +28,15 @@ export const DraggableList = () => { }; return ( - setDraggableId(Number(draggableId))} - onDragUpdate={({ destination }) => { - setDropPlaceIndex(destination?.index ?? -1); - }} - onDragEnd={onDragEnd} - > + - {(provided) => ( - + {(provided, snapshot) => ( + {listQuestions[quizId]?.map((_, index) => ( ))} {provided.placeholder} diff --git a/src/pages/Questions/SliderOptions/SliderOptions.tsx b/src/pages/Questions/SliderOptions/SliderOptions.tsx index c1ae6e54..838ec199 100644 --- a/src/pages/Questions/SliderOptions/SliderOptions.tsx +++ b/src/pages/Questions/SliderOptions/SliderOptions.tsx @@ -52,6 +52,7 @@ export default function SliderOptions({ totalIndex }: Props) { }); }} onBlur={({ target }) => { + const start = listQuestions[quizId][totalIndex].content.start; const min = Number(target.value); const max = Number( listQuestions[quizId][totalIndex].content.range.split("—")[1] @@ -68,6 +69,15 @@ export default function SliderOptions({ totalIndex }: Props) { content: clonContent, }); } + + if (start < min) { + updateQuestionsList(quizId, totalIndex, { + content: { + ...listQuestions[quizId][totalIndex].content, + start: min, + }, + }); + } }} /> @@ -88,10 +98,13 @@ export default function SliderOptions({ totalIndex }: Props) { }); }} onBlur={({ target }) => { + const start = listQuestions[quizId][totalIndex].content.start; + const step = listQuestions[quizId][totalIndex].content.step; const min = Number( listQuestions[quizId][totalIndex].content.range.split("—")[0] ); const max = Number(target.value); + const range = max - min; if (max <= min) { const clonContent = listQuestions[quizId][totalIndex].content; @@ -104,6 +117,34 @@ export default function SliderOptions({ totalIndex }: Props) { content: clonContent, }); } + + if (start > max) { + updateQuestionsList(quizId, totalIndex, { + content: { + ...listQuestions[quizId][totalIndex].content, + start: max, + }, + }); + } + + if (step > max) { + updateQuestionsList(quizId, totalIndex, { + content: { + ...listQuestions[quizId][totalIndex].content, + step: max, + }, + }); + + if (range % step) { + setStepError( + `Шаг должен делить без остатка диапазон ${max} - ${min} = ${ + max - min + }` + ); + } else { + setStepError(""); + } + } }} /> @@ -120,7 +161,12 @@ export default function SliderOptions({ totalIndex }: Props) { Начальное значение { const clonContent = listQuestions[quizId][totalIndex].content; @@ -129,21 +175,6 @@ export default function SliderOptions({ totalIndex }: Props) { content: clonContent, }); }} - onBlur={({ target }) => { - const start = Number(target.value); - const min = Number( - listQuestions[quizId][totalIndex].content.range.split("—")[0] - ); - - if (start < min) { - updateQuestionsList(quizId, totalIndex, { - content: { - ...listQuestions[quizId][totalIndex].content, - start: min, - }, - }); - } - }} /> @@ -171,11 +202,11 @@ export default function SliderOptions({ totalIndex }: Props) { const range = max - min; const step = Number(target.value); - if (step > range) { + if (step > max) { updateQuestionsList(quizId, totalIndex, { content: { ...listQuestions[quizId][totalIndex].content, - step: range, + step: max, }, }); } diff --git a/src/pages/Questions/UploadImage/index.tsx b/src/pages/Questions/UploadImage/index.tsx index 47f65929..1148dd96 100644 --- a/src/pages/Questions/UploadImage/index.tsx +++ b/src/pages/Questions/UploadImage/index.tsx @@ -2,7 +2,7 @@ import { useParams } from "react-router-dom"; import { useState } from "react"; import { Typography, Box, useTheme, ButtonBase } from "@mui/material"; import UploadBox from "@ui_kit/UploadBox"; -import { CroppingModal } from "@ui_kit/Modal/CroppingModal"; +import { CropModal } from "@ui_kit/Modal/CropModal"; import UploadIcon from "../../../assets/icons/UploadIcon"; import * as React from "react"; import { questionStore, updateQuestionsList } from "@root/questions"; @@ -67,7 +67,7 @@ export default function UploadImage({ totalIndex }: UploadImageProps) { /> - setOpened(false)} picture={listQuestions[quizId][totalIndex].content.back} diff --git a/src/pages/Result/DescriptionForm/ImageAndVideoButtons.tsx b/src/pages/Result/DescriptionForm/ImageAndVideoButtons.tsx index 4512c9fa..da289224 100644 --- a/src/pages/Result/DescriptionForm/ImageAndVideoButtons.tsx +++ b/src/pages/Result/DescriptionForm/ImageAndVideoButtons.tsx @@ -3,7 +3,7 @@ import { Box, Typography, useTheme } from "@mui/material"; import AddImage from "@icons/questionsPage/addImage"; import AddVideofile from "@icons/questionsPage/addVideofile"; import { useState } from "react"; -import { CroppingModal } from "@ui_kit/Modal/CroppingModal"; +import { CropModal } from "@ui_kit/Modal/CropModal"; export default function ImageAndVideoButtons() { const theme = useTheme(); @@ -13,7 +13,7 @@ export default function ImageAndVideoButtons() { return ( setOpened(true)} /> - setOpened(false)} /> + setOpened(false)} /> { +export const createQuestion = (quizId: number, placeIndex = -1) => { const id = getRandom(1000000, 10000000); const newData = { ...questionStore.getState()["listQuestions"] }; @@ -132,73 +132,77 @@ export const createQuestion = (quizId: number) => { newData[quizId] = []; } - newData[quizId].push({ - id, - title: "", - description: "", - type: "", - required: true, - deleted: true, - page: 0, - content: { - largeCheck: false, - large: "", - multi: false, - own: false, - innerNameCheck: false, - innerName: "", - back: "", - placeholder: "", - type: "all", - autofill: true, - default: "", - images: [], - number: false, - single: false, - xy: "", - format: "carousel", - text: "", - picture: "", - video: "", - dateRange: false, - time: false, - form: "star", - steps: 5, - range: "0—100", - start: 50, - step: 1, - chooseRange: false, - required: false, - replText: "", - ratingExpanded: false, - ratingDescription: "", - variants: [ - { - answer: "", - hints: "", - }, - ], - hint: { + newData[quizId].splice( + placeIndex < 0 ? newData[quizId].length : placeIndex, + 0, + { + id, + title: "", + description: "", + type: "", + required: true, + deleted: true, + page: 0, + content: { + largeCheck: false, + large: "", + multi: false, + own: false, + innerNameCheck: false, + innerName: "", + back: "", + placeholder: "", + type: "all", + autofill: true, + default: "", + images: [], + number: false, + single: false, + xy: "", + format: "carousel", text: "", + picture: "", video: "", - }, - rule: { - or: true, - show: true, - reqs: [ + dateRange: false, + time: false, + form: "star", + steps: 5, + range: "0—100", + start: 50, + step: 1, + chooseRange: false, + required: false, + replText: "", + ratingExpanded: false, + ratingDescription: "", + variants: [ { - id: "", - vars: [], + answer: "", + hints: "", }, ], + hint: { + text: "", + video: "", + }, + rule: { + or: true, + show: true, + reqs: [ + { + id: "", + vars: [], + }, + ], + }, }, - }, - version: 0, - parent_ids: [0], - created_at: "", - updated_at: "", - expanded: false, - }); + version: 0, + parent_ids: [0], + created_at: "", + updated_at: "", + expanded: false, + } + ); questionStore.setState({ listQuestions: newData }); }; diff --git a/src/ui_kit/CustomNumberField.tsx b/src/ui_kit/CustomNumberField.tsx index 37fab7c2..8797d74f 100644 --- a/src/ui_kit/CustomNumberField.tsx +++ b/src/ui_kit/CustomNumberField.tsx @@ -30,16 +30,28 @@ export default function CustomNumberField({ const inputValue = event.target.value; if ( - Number(inputValue) >= min && - Number(inputValue) <= max && - (inputValue === "" || - inputValue.match(/^\d*$/) || - (inputValue[0] === "-" && inputValue.slice(1).match(/^\d*$/))) + inputValue === "" || + inputValue.match(/^\d*$/) || + (inputValue[0] === "-" && inputValue.slice(1).match(/^\d*$/)) ) { onChange?.({ ...event, target: { ...event.target, value: inputValue } }); } }; + const onInputBlur = (event: FocusEvent) => { + const inputValue = event.target.value; + + if (Number(inputValue) < min) { + onChange?.({ ...event, target: { ...event.target, value: String(min) } }); + } + + if (Number(inputValue) > max) { + onChange?.({ ...event, target: { ...event.target, value: String(max) } }); + } + + onBlur?.(event); + }; + return ( ); diff --git a/src/ui_kit/Modal/CropModal.tsx b/src/ui_kit/Modal/CropModal.tsx new file mode 100644 index 00000000..eeba0875 --- /dev/null +++ b/src/ui_kit/Modal/CropModal.tsx @@ -0,0 +1,356 @@ +import React, { useState, useRef, useEffect, FC } from "react"; +import ReactCrop, { Crop, PixelCrop } from "react-image-crop"; +import { + Box, + Button, + Modal, + Slider, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; + +import { canvasPreview } from "./utils/canvasPreview"; +import { useDebounceEffect } from "./utils/useDebounceEffect"; + +import { ResetIcon } from "@icons/ResetIcon"; + +import "react-image-crop/dist/ReactCrop.css"; +import { CropIcon } from "@icons/CropIcon"; + +interface Iprops { + opened: boolean; + onClose: React.Dispatch>; + picture?: string; +} + +export const CropModal: FC = ({ opened, onClose, picture }) => { + const [imgSrc, setImgSrc] = useState(""); + + const imgRef = useRef(null); + const fileInputRef = useRef(null); + const previewCanvasRef = useRef(null); + const hiddenAnchorRef = useRef(null); + const blobUrlRef = useRef(""); + const [crop, setCrop] = useState(); + const [completedCrop, setCompletedCrop] = useState(); + const [rotate, setRotate] = useState(0); + const [darken, setDarken] = useState(0); + + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(786)); + + useEffect(() => { + if (picture) { + setImgSrc(picture); + } + }, [picture]); + + const styleModal = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: isMobile ? "343px" : "620px", + bgcolor: "background.paper", + boxShadow: 24, + padding: "20px", + borderRadius: "8px", + }; + + const styleSlider = { + width: isMobile ? "350px" : "250px", + 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`, + }, + }, + }; + + const rotateImage = () => { + const newRotation = (rotate + 90) % 360; + setRotate(newRotation); + }; + + const onSelectFile = (event: React.ChangeEvent) => { + if (event.target.files && event.target.files.length > 0) { + setCrop(undefined); + const reader = new FileReader(); + reader.addEventListener("load", () => + setImgSrc(reader.result?.toString() || "") + ); + reader.readAsDataURL(event.target.files[0]); + } + }; + + const onDownloadCropClick = () => { + if (!previewCanvasRef.current) { + throw new Error("Crop canvas does not exist"); + } + + const canvasCopy = document.createElement("canvas"); + const ctx = canvasCopy.getContext("2d"); + canvasCopy.width = previewCanvasRef.current.width; + canvasCopy.height = previewCanvasRef.current.height; + ctx!.filter = `brightness(${100 - darken}%)`; + ctx!.drawImage(previewCanvasRef.current, 0, 0); + + canvasCopy.toBlob((blob) => { + if (!blob) { + throw new Error("Failed to create blob"); + } + if (blobUrlRef.current) { + URL.revokeObjectURL(blobUrlRef.current); + } + blobUrlRef.current = URL.createObjectURL(blob); + hiddenAnchorRef.current!.href = blobUrlRef.current; + hiddenAnchorRef.current!.click(); + + setImgSrc(blobUrlRef.current); + }); + }; + + useDebounceEffect( + async () => { + if ( + completedCrop?.width && + completedCrop?.height && + imgRef.current && + previewCanvasRef.current + ) { + canvasPreview( + imgRef.current, + previewCanvasRef.current, + completedCrop, + rotate + ); + } + }, + 100, + [completedCrop, rotate] + ); + + const [width, setWidth] = useState(0); + + const getImageSize = () => { + if (imgRef.current) { + const imageWidth = imgRef.current.naturalWidth; + const imageHeight = imgRef.current.naturalHeight; + + const aspect = imageWidth / imageHeight; + + console.log(aspect); + + console.log(width); + + if (aspect <= 1.333) { + setWidth(240); + } + if (aspect >= 1.5) { + setWidth(580); + } + if (aspect >= 1.778) { + setWidth(580); + } + } + }; + + return ( + <> + + + + {imgSrc && ( + setCrop(percentCrop)} + onComplete={(c) => setCompletedCrop(c)} + maxWidth={500} + minWidth={50} + maxHeight={320} + minHeight={50} + > + Crop me + + )} + + + + {crop?.width ? Math.round(crop.width) + "px" : ""} + + + {crop?.height ? Math.round(crop.height) + "px" : ""} + + + + + + + + Размер + + { + setWidth(newValue as number); + }} + /> + + + + Затемнение + + setDarken(newValue as number)} + /> + + + + + + + + + + {completedCrop && ( +
+ +
+ )} +
+ + Hidden download + +
+ + ); +}; diff --git a/src/ui_kit/Modal/CroppingModal.tsx b/src/ui_kit/Modal/CroppingModal.tsx deleted file mode 100644 index 2c59daa6..00000000 --- a/src/ui_kit/Modal/CroppingModal.tsx +++ /dev/null @@ -1,345 +0,0 @@ -import React, { FC, useEffect, useRef, useState } from "react"; -import { saveAs } from "file-saver"; -import ReactCrop, { Crop } from "react-image-crop"; -import "react-image-crop/dist/ReactCrop.css"; -import { - Box, - Button, - Modal, - Slider, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material"; - -import quiz from "../../assets/quiz-template-6.png"; -import { ResetIcon } from "@icons/ResetIcon"; - -interface Iprops { - opened: boolean; - onClose: () => void; - picture?: string | ArrayBuffer; -} - -export const CroppingModal: FC = ({ opened, onClose, picture }) => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(786)); - - const style = { - position: "absolute" as "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - width: isMobile ? "343px" : "620px", - bgcolor: "background.paper", - boxShadow: 24, - padding: "20px", - borderRadius: "8px", - }; - - const styleSlider = { - width: isMobile ? "350px" : "250px", - 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`, - }, - }, - }; - - const [src, setSrc] = useState(quiz); - const [crop, setCrop] = useState({ - unit: "px", - y: 0, - x: 0, - width: 100, - height: 100, - }); - const [completedCrop, setCompletedCrop] = useState(null); - const [imageSize, setImageSize] = useState(580); - const [darken, setDarken] = useState(0); - const fileInputRef = useRef(null); - - console.log(src); - - useEffect(() => { - if (picture) { - setSrc(picture); - } - }, [picture]); - - const onCropComplete = (crop: Crop) => { - setCompletedCrop(crop); - }; - - const handleFileChange = (e: React.ChangeEvent) => { - if (e.target.files && e.target.files.length > 0) { - const file = e.target.files[0]; - const reader = new FileReader(); - reader.onload = (event) => { - if (event.target && event.target.result) { - setSrc(event.target.result); - } - }; - reader.readAsDataURL(file); - } - }; - - const handleDownloadClick = async () => { - if (completedCrop && src) { - const croppedImageUrl = await getCroppedAndDarkenedImg( - src, - completedCrop, - "cropped.jpeg", - darken - ); - saveAs(croppedImageUrl, "cropped-image.jpeg"); - } - }; - - const getCroppedAndDarkenedImg = ( - image: string | ArrayBuffer, - crop: Crop, - fileName: string, - darken: number - ): Promise => { - const img = new Image(); - img.src = image as string; - - let scaleX = 360 / 580; - let scaleY = 219 / 320; - - if (img.naturalWidth) { - scaleX = img.naturalWidth / 580; - } - - if (img.naturalHeight) { - scaleY = img.naturalHeight / 320; - } - - console.log(scaleX); - - console.log(scaleY); - - const canvas = document.createElement("canvas"); - canvas.width = crop.width!; - canvas.height = crop.height!; - const ctx = canvas.getContext("2d"); - - if (!ctx) { - throw new Error("Canvas context is null"); - } - - ctx.drawImage( - img, - crop.x! * scaleX, - crop.y! * scaleY, - crop.width! * scaleX, - crop.height! * scaleY, - 0, - 0, - crop.width!, - crop.height! - ); - - const imageData = ctx.getImageData(0, 0, crop.width!, crop.height!); - - const newImageData = imageData.data.map((value, index) => { - if ((index + 1) % 4 === 0) { - return value; - } - - return value * (1 - darken / 100); - }); - - imageData.data.set(newImageData); - - ctx.putImageData(imageData, 0, 0); - - return new Promise((resolve, reject) => { - canvas.toBlob((blob) => { - if (!blob) { - reject(new Error("Canvas is empty")); - return; - } - const file = new File([blob], fileName, { type: "image/jpeg" }); - const imageUrl = window.URL.createObjectURL(file); - resolve(imageUrl); - }, "image/jpeg"); - }); - }; - - return ( - - - - setCrop(newCrop)} - onComplete={onCropComplete} - > - {src && ( - Crop - )} - - - - - - {Math.round(crop.width)} - - x - - {Math.round(crop.width)} - - px - - - - { - setCrop((prevCrop: Crop) => ({ - ...prevCrop, - unit: "px", - x: 210, - y: 10, - width: 210, - height: 300, - })); - - setDarken(0); - setImageSize(580); - }} - style={{ marginBottom: "10px", cursor: "pointer" }} - /> - - - Размер - - setImageSize(newValue as number)} - /> - - - - Затемнение - - setDarken(newValue as number)} - /> - - - - - - - - - - ); -}; diff --git a/src/ui_kit/Modal/ImageCrop.tsx b/src/ui_kit/Modal/ImageCrop.tsx index 5c0c75e0..fb9877d6 100644 --- a/src/ui_kit/Modal/ImageCrop.tsx +++ b/src/ui_kit/Modal/ImageCrop.tsx @@ -1,13 +1,13 @@ import { Box, Button } from "@mui/material"; import { FC, useState } from "react"; -import { CroppingModal } from "./CroppingModal"; +import { CropModal } from "./CropModal"; const ImageCrop: FC = () => { const [opened, setOpened] = useState(false); return ( - setOpened(false)} /> + setOpened(false)} /> ); }; diff --git a/src/ui_kit/Modal/modal.css b/src/ui_kit/Modal/modal.css new file mode 100644 index 00000000..34f8fe98 --- /dev/null +++ b/src/ui_kit/Modal/modal.css @@ -0,0 +1,76 @@ +.ReactCrop__drag-bar, +.ord-e { + background-color: #7e2aea; +} + +.ReactCrop__crop-selection:not(.ReactCrop--no-animate .ReactCrop__crop-selection) { + background-image: none; +} + +.ReactCrop__drag-bar.ord-e { + right: 0; + top: 0; + width: 3px; + height: 100%; + margin-right: -3px; +} + +.ReactCrop__drag-bar.ord-s { + bottom: 0; + left: 0; + width: 100%; + height: 3px; + margin-bottom: -3px; +} + +.ReactCrop__drag-bar.ord-n { + bottom: 0; + left: 0; + width: 100%; + height: 3px; + margin-bottom: -3px; +} + +.ReactCrop__drag-bar.ord-w { + top: 0; + left: 0; + width: 3px; + height: 100%; + margin-left: -3px; +} + +/* кружочки */ + +.ReactCrop .ord-nw:after { + background-color: #7e2aea; +} + +.ReactCrop .ord-n:after { + background-color: #7e2aea; +} + +.ReactCrop .ord-ne:after { + background-color: #7e2aea; +} + +.ReactCrop .ord-se:after { + background-color: #7e2aea; +} + +.ReactCrop .ord-e:after { + background-color: #7e2aea; +} + +.ReactCrop .ord-s:after { + background-color: #7e2aea; +} + +.ReactCrop .ord-sw:after { + background-color: #7e2aea; +} + +.ReactCrop .ord-w:after { + background-color: #7e2aea; +} + +/* кружочки */ diff --git a/src/ui_kit/Modal/utils/canvasPreview.ts b/src/ui_kit/Modal/utils/canvasPreview.ts new file mode 100644 index 00000000..02278c37 --- /dev/null +++ b/src/ui_kit/Modal/utils/canvasPreview.ts @@ -0,0 +1,50 @@ +import { PixelCrop } from "react-image-crop"; + +const TO_RADIANS = Math.PI / 180; + +export async function canvasPreview(image: HTMLImageElement, canvas: HTMLCanvasElement, crop: PixelCrop, rotate = 0) { + const scale = 1; + const ctx = canvas.getContext("2d"); + + if (!ctx) { + throw new Error("No 2d context"); + } + + const scaleX = image.naturalWidth / image.width; + const scaleY = image.naturalHeight / image.height; + // devicePixelRatio slightly increases sharpness on retina devices + // at the expense of slightly slower render times and needing to + // size the image back down if you want to download/upload and be + // true to the images natural size. + const pixelRatio = window.devicePixelRatio; + // const pixelRatio = 1 + + canvas.width = Math.floor(crop.width * scaleX * pixelRatio); + canvas.height = Math.floor(crop.height * scaleY * pixelRatio); + + ctx.scale(pixelRatio, pixelRatio); + ctx.imageSmoothingQuality = "high"; + + const cropX = crop.x * scaleX; + const cropY = crop.y * scaleY; + + const rotateRads = rotate * TO_RADIANS; + const centerX = image.naturalWidth / 2; + const centerY = image.naturalHeight / 2; + + ctx.save(); + + // 5) Move the crop origin to the canvas origin (0,0) + ctx.translate(-cropX, -cropY); + // 4) Move the origin to the center of the original position + ctx.translate(centerX, centerY); + // 3) Rotate around the origin + ctx.rotate(rotateRads); + // 2) Scale the image + ctx.scale(scale, scale); + // 1) Move the center of the image to the origin (0,0) + ctx.translate(-centerX, -centerY); + ctx.drawImage(image, 0, 0, image.naturalWidth, image.naturalHeight, 0, 0, image.naturalWidth, image.naturalHeight); + + ctx.restore(); +} diff --git a/src/ui_kit/Modal/utils/useDebounceEffect.ts b/src/ui_kit/Modal/utils/useDebounceEffect.ts new file mode 100644 index 00000000..8b4bca50 --- /dev/null +++ b/src/ui_kit/Modal/utils/useDebounceEffect.ts @@ -0,0 +1,13 @@ +import { useEffect, DependencyList } from "react"; + +export function useDebounceEffect(fn: () => void, waitTime: number, deps?: DependencyList) { + useEffect(() => { + const time = setTimeout(() => { + fn(); + }, waitTime); + + return () => { + clearTimeout(time); + }; + }, [deps, fn, waitTime]); +}