From 0478f603fa68608d8c6b8cd2f28d7da4bdab6bd5 Mon Sep 17 00:00:00 2001 From: ArtChaos189 Date: Sat, 12 Aug 2023 11:31:21 +0300 Subject: [PATCH] create modal --- package.json | 3 + src/assets/icons/CropIcon.tsx | 10 + src/assets/icons/ResetIcon.tsx | 33 +++ src/assets/icons/questionsPage/addImage.tsx | 76 +++-- src/index.tsx | 2 + .../OptionsAndPicture/OptionsAndPicture.tsx | 101 +++---- .../Questions/PageOptions/PageOptions.tsx | 4 +- .../DescriptionForm/ImageAndVideoButtons.tsx | 8 +- src/ui_kit/Accordion.tsx | 180 ++++++------ src/ui_kit/Modal/CroppingModal.tsx | 276 ++++++++++++++++++ src/ui_kit/Modal/ImageCrop.tsx | 15 + yarn.lock | 30 ++ 12 files changed, 574 insertions(+), 164 deletions(-) create mode 100644 src/assets/icons/CropIcon.tsx create mode 100644 src/assets/icons/ResetIcon.tsx create mode 100644 src/ui_kit/Modal/CroppingModal.tsx create mode 100644 src/ui_kit/Modal/ImageCrop.tsx diff --git a/package.json b/package.json index e76fa912..d1ce3ae6 100755 --- a/package.json +++ b/package.json @@ -18,12 +18,15 @@ "@types/react-dnd": "^3.0.2", "@types/react-dom": "^18.0.0", "file-saver": "^2.0.5", + "html-to-image": "^1.11.11", "jszip": "^3.10.1", "notistack": "^3.0.1", "react": "^18.2.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", + "react-easy-crop": "^5.0.0", + "react-image-crop": "^10.1.5", "react-image-file-resizer": "^0.4.8", "react-router-dom": "^6.6.2", "react-scripts": "5.0.1", diff --git a/src/assets/icons/CropIcon.tsx b/src/assets/icons/CropIcon.tsx new file mode 100644 index 00000000..75233e2f --- /dev/null +++ b/src/assets/icons/CropIcon.tsx @@ -0,0 +1,10 @@ +import { FC } from "react"; + +export const CropIcon: FC = () => ( + + + + + + +); diff --git a/src/assets/icons/ResetIcon.tsx b/src/assets/icons/ResetIcon.tsx new file mode 100644 index 00000000..a0e3d0e6 --- /dev/null +++ b/src/assets/icons/ResetIcon.tsx @@ -0,0 +1,33 @@ +import { CSSProperties, FC } from "react"; + +interface Iporps { + style?: CSSProperties; + onClick?: () => void; +} + +export const ResetIcon: FC = ({ style, onClick }) => ( + + + + +); diff --git a/src/assets/icons/questionsPage/addImage.tsx b/src/assets/icons/questionsPage/addImage.tsx index adec08f1..630cb87e 100644 --- a/src/assets/icons/questionsPage/addImage.tsx +++ b/src/assets/icons/questionsPage/addImage.tsx @@ -1,30 +1,54 @@ import { Box } from "@mui/material"; +import { FC } from "react"; +interface Iprops { + onClick?: () => void; +} -// interface Props { -// color: string; -// } +const AddImage: FC = ({ onClick }) => { + return ( + + + + + + + + + + + ); +}; -export default function AddImage() { - - return ( - - - - - - - - - - - ); -} \ No newline at end of file +export default AddImage; diff --git a/src/index.tsx b/src/index.tsx index b8ac515c..40838698 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -18,6 +18,7 @@ import { Result } from "./pages/Result/Result"; import { Setting } from "./pages/Result/Setting"; import MyQuizzes from "./pages/createQuize/MyQuizzes"; import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull"; +import ImageCrop from "@ui_kit/Modal/ImageCrop"; const routeslink: { path: string; page: JSX.Element; header: boolean; sidebar: boolean }[] = [ { path: "/", page: , header: false, sidebar: false }, @@ -38,6 +39,7 @@ ReactDOM.render( } /> ))} } /> + } /> diff --git a/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx b/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx index f7143075..c9bbb6a0 100644 --- a/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx +++ b/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx @@ -1,4 +1,4 @@ -import {Box, Link, Typography, useTheme} from "@mui/material"; +import { Box, Link, Typography, useTheme } from "@mui/material"; import AddImage from "../../../assets/icons/questionsPage/addImage"; import EnterIcon from "../../../assets/icons/questionsPage/enterIcon"; import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict"; @@ -6,57 +6,58 @@ import SwitchOptionsAndPict from "./switchOptionsAndPict"; import React from "react"; interface Props { - totalIndex: number + totalIndex: number; } -export default function OptionsAndPicture({totalIndex}: Props) { - const theme = useTheme(); - const [switchState, setSwitchState] = React.useState('setting'); - const SSHC = (data: string) => { - setSwitchState(data) - } -return ( +export default function OptionsAndPicture({ totalIndex }: Props) { + const theme = useTheme(); + const [switchState, setSwitchState] = React.useState("setting"); + const SSHC = (data: string) => { + setSwitchState(data); + }; + return ( <> - - - - - Добавьте ответ - - - - - { - // console.info("I'm a button."); - // }} - > - Добавьте ответ - - или нажмите Enter - - + + + + + Добавьте ответ + - - + + { + // console.info("I'm a button."); + // }} + > + Добавьте ответ + + + или нажмите Enter + + + + + + -) -} \ No newline at end of file + ); +} diff --git a/src/pages/Questions/PageOptions/PageOptions.tsx b/src/pages/Questions/PageOptions/PageOptions.tsx index 1edb9417..492ca0f2 100644 --- a/src/pages/Questions/PageOptions/PageOptions.tsx +++ b/src/pages/Questions/PageOptions/PageOptions.tsx @@ -8,7 +8,7 @@ import SwitchPageOptions from "./switchPageOptions"; type Props = { disableInput?: boolean; - totalIndex: number + totalIndex: number; }; export default function PageOptions({ disableInput, totalIndex }: Props) { @@ -59,7 +59,7 @@ export default function PageOptions({ disableInput, totalIndex }: Props) { - + ); diff --git a/src/pages/Result/DescriptionForm/ImageAndVideoButtons.tsx b/src/pages/Result/DescriptionForm/ImageAndVideoButtons.tsx index 2c2f9a03..4512c9fa 100644 --- a/src/pages/Result/DescriptionForm/ImageAndVideoButtons.tsx +++ b/src/pages/Result/DescriptionForm/ImageAndVideoButtons.tsx @@ -2,12 +2,18 @@ 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"; export default function ImageAndVideoButtons() { const theme = useTheme(); + + const [opened, setOpened] = useState(false); + return ( - + setOpened(true)} /> + setOpened(false)} /> void; - sx?: SxProps; - header: string; + children?: React.ReactNode; + isExpanded?: boolean; + onClick?: () => void; + sx?: SxProps; + header: string; } -export default function AccordMy ({ children, isExpanded = false, onClick, sx, header }: Props) { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - const upSm = useMediaQuery(theme.breakpoints.up("sm")); - return ( - + + + + {header} + + + - - - {header} - - - - - - - - - - - {isExpanded && ( - - {children} - - - ) - } + + + + - ) -} \ No newline at end of file + {isExpanded && ( + + {children} + + )} + + + ); +} diff --git a/src/ui_kit/Modal/CroppingModal.tsx b/src/ui_kit/Modal/CroppingModal.tsx new file mode 100644 index 00000000..ee82a999 --- /dev/null +++ b/src/ui_kit/Modal/CroppingModal.tsx @@ -0,0 +1,276 @@ +import React, { FC, 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 } from "@mui/material"; + +import quiz from "../../assets/quiz-template-6.png"; +import { ResetIcon } from "@icons/ResetIcon"; + +interface Iprops { + opened: boolean; + onClose: () => void; +} + +const style = { + position: "absolute" as "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: "620px", + bgcolor: "background.paper", + boxShadow: 24, + padding: "20px", + borderRadius: "8px", +}; + +const styleSlider = { + width: "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`, + }, + }, +}; + +export const CroppingModal: FC = ({ opened, onClose }) => { + 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); + + 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; + + const scaleX = imageSize < 580 ? img.naturalWidth / (580 * (imageSize / 200)) : img.naturalWidth / 580; + const scaleY = img.naturalHeight / 320; + + 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 new file mode 100644 index 00000000..5c0c75e0 --- /dev/null +++ b/src/ui_kit/Modal/ImageCrop.tsx @@ -0,0 +1,15 @@ +import { Box, Button } from "@mui/material"; +import { FC, useState } from "react"; +import { CroppingModal } from "./CroppingModal"; + +const ImageCrop: FC = () => { + const [opened, setOpened] = useState(false); + return ( + + setOpened(false)} /> + + + ); +}; + +export default ImageCrop; diff --git a/yarn.lock b/yarn.lock index c0705d05..cd8d5038 100755 --- a/yarn.lock +++ b/yarn.lock @@ -5126,6 +5126,11 @@ html-minifier-terser@^6.0.2: relateurl "^0.2.7" terser "^5.10.0" +html-to-image@^1.11.11: + version "1.11.11" + resolved "https://registry.yarnpkg.com/html-to-image/-/html-to-image-1.11.11.tgz#c0f8a34dc9e4b97b93ff7ea286eb8562642ebbea" + integrity sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA== + html-webpack-plugin@^5.5.0: version "5.5.0" resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz" @@ -6648,6 +6653,11 @@ normalize-url@^6.0.1: resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== +normalize-wheel@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45" + integrity sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA== + notistack@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz" @@ -7769,11 +7779,26 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-easy-crop@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/react-easy-crop/-/react-easy-crop-5.0.0.tgz#8945dccf4d9f578e7d8d06ed71229e93f46d4a43" + integrity sha512-ppYg3E0jxpDW+HdgLa65lCykZSsGMuusBuKD3HeTMs/Aod4xiWyAH5jZn5iHlllLUV2c0PPT6FznvdNeLhO2wA== + dependencies: + normalize-wheel "^1.0.1" + tslib "2.0.1" + react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== +react-image-crop@^10.1.5: + version "10.1.5" + resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-10.1.5.tgz#60f9d81405b01b6925629cae235e8406616fc0e5" + integrity sha512-BL8Rd/UHCE4O5GcYQiWDKVh5JOJb0Ic/Gde2W171v5nY7RyQzFLM1cxIzlYfESLO/lNgBVhDHuEV9RHnqALMkA== + dependencies: + clsx "^1.2.1" + react-image-file-resizer@^0.4.8: version "0.4.8" resolved "https://registry.npmjs.org/react-image-file-resizer/-/react-image-file-resizer-0.4.8.tgz" @@ -8918,6 +8943,11 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" + integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== + tslib@^1.8.1: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"