From a42a4126ecbc5947e70039bd3e96937f53a56312 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Sun, 19 Apr 2026 16:43:46 +0100 Subject: [PATCH] fix: image crop broken (#37) --- .../components/submit-form/image-editor.tsx | 228 +++++++++--------- frontend/src/main.tsx | 1 + 2 files changed, 115 insertions(+), 114 deletions(-) diff --git a/frontend/src/components/submit-form/image-editor.tsx b/frontend/src/components/submit-form/image-editor.tsx index 2934e2e..758267f 100644 --- a/frontend/src/components/submit-form/image-editor.tsx +++ b/frontend/src/components/submit-form/image-editor.tsx @@ -1,114 +1,114 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import ReactCrop, { type Crop } from "react-image-crop"; -import { Icon } from "@iconify/react"; - -interface Props { - isOpen: boolean; - setIsOpen: React.Dispatch>; - image: string | undefined; - setImage: (value: string | undefined) => void; -} - -export default function ImageEditorPortrait({ isOpen, setIsOpen, image, setImage }: Props) { - const [isVisible, setIsVisible] = useState(false); - const [crop, setCrop] = useState(); - - const imageRef = useRef(null); - const canvasRef = useRef(null); - - const applyCrop = useCallback(() => { - if (!imageRef.current || !canvasRef.current || !crop) return; - - const image = imageRef.current; - const canvas = canvasRef.current; - - if (!crop.width || !crop.height || image.naturalWidth === 0 || image.naturalHeight === 0) return; - - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - const scaleX = image.naturalWidth / image.width; - const scaleY = image.naturalHeight / image.height; - - canvas.width = crop.width * scaleX; - canvas.height = crop.height * scaleY; - - ctx.drawImage(image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, crop.width * scaleX, crop.height * scaleY); - - setImage(canvas.toDataURL()); - setCrop(undefined); - }, [crop, setImage]); - - const rotate = () => { - if (!imageRef.current || !canvasRef.current) return; - - const image = imageRef.current; - const canvas = canvasRef.current; - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - canvas.width = image.naturalHeight; - canvas.height = image.naturalWidth; - - ctx.translate(canvas.width / 2, canvas.height / 2); - ctx.rotate(Math.PI / 2); - ctx.drawImage(image, -image.naturalWidth / 2, -image.naturalHeight / 2); - - setImage(canvas.toDataURL()); - }; - - const close = () => { - setIsVisible(false); - setTimeout(() => { - setIsOpen(false); - }, 300); - }; - - useEffect(() => { - if (isOpen) { - // slight delay to trigger animation - setTimeout(() => setIsVisible(true), 10); - } - }, [isOpen]); - - return ( -
-
- -
-
-

Edit Image

- -
- -
- setCrop(c)} className="rounded-2xl border-2 border-amber-500 overflow-hidden max-h-96"> - - - -
- -
- - - -
-
-
- ); -} +import { useCallback, useEffect, useRef, useState } from "react"; +import ReactCrop, { type Crop } from "react-image-crop"; +import { Icon } from "@iconify/react"; + +interface Props { + isOpen: boolean; + setIsOpen: React.Dispatch>; + image: string | undefined; + setImage: (value: string | undefined) => void; +} + +export default function ImageEditorPortrait({ isOpen, setIsOpen, image, setImage }: Props) { + const [isVisible, setIsVisible] = useState(false); + const [crop, setCrop] = useState(); + + const imageRef = useRef(null); + const canvasRef = useRef(null); + + const applyCrop = useCallback(() => { + if (!imageRef.current || !canvasRef.current || !crop) return; + + const image = imageRef.current; + const canvas = canvasRef.current; + + if (!crop.width || !crop.height || image.naturalWidth === 0 || image.naturalHeight === 0) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const scaleX = image.naturalWidth / image.width; + const scaleY = image.naturalHeight / image.height; + + canvas.width = crop.width * scaleX; + canvas.height = crop.height * scaleY; + + ctx.drawImage(image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, crop.width * scaleX, crop.height * scaleY); + + setImage(canvas.toDataURL()); + setCrop(undefined); + }, [crop, setImage]); + + const rotate = () => { + if (!imageRef.current || !canvasRef.current) return; + + const image = imageRef.current; + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + canvas.width = image.naturalHeight; + canvas.height = image.naturalWidth; + + ctx.translate(canvas.width / 2, canvas.height / 2); + ctx.rotate(Math.PI / 2); + ctx.drawImage(image, -image.naturalWidth / 2, -image.naturalHeight / 2); + + setImage(canvas.toDataURL()); + }; + + const close = () => { + setIsVisible(false); + setTimeout(() => { + setIsOpen(false); + }, 300); + }; + + useEffect(() => { + if (isOpen) { + // slight delay to trigger animation + setTimeout(() => setIsVisible(true), 10); + } + }, [isOpen]); + + return ( +
+
+ +
+
+

Edit Image

+ +
+ +
+ setCrop(c)} className="rounded-2xl border-2 border-amber-500 overflow-hidden max-h-96"> + + + +
+ +
+ + + +
+
+
+ ); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 9c7ef55..68b15dc 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,6 +2,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter, Route, Routes } from "react-router"; import "./index.css"; +import "react-image-crop/dist/ReactCrop.css"; import "@fontsource-variable/lexend/wght.css"; import PrivacyPage from "./pages/privacy.tsx";