From e3179e2e556008ffa464734e80b1184fd02728cd Mon Sep 17 00:00:00 2001 From: Mrlionbyte Date: Fri, 14 Mar 2025 09:00:52 +0530 Subject: [PATCH 1/4] feat: add image crop in profile --- frontend/package.json | 1 + .../user/editProfile/ImageModal.tsx | 52 +++++++++++++++ .../user/editProfile/PictureInput.tsx | 63 ++++++++++++++++--- .../components/user/editProfile/cropImage.ts | 47 ++++++++++++++ package-lock.json | 21 +++++++ 5 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/user/editProfile/ImageModal.tsx create mode 100644 frontend/src/components/user/editProfile/cropImage.ts diff --git a/frontend/package.json b/frontend/package.json index c97f3063..870f185d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,6 +35,7 @@ "next-themes": "^0.4.4", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-easy-crop": "^5.4.1", "react-hook-form": "^7.54.2", "react-icons": "^5.5.0", "react-markdown": "^10.0.1", diff --git a/frontend/src/components/user/editProfile/ImageModal.tsx b/frontend/src/components/user/editProfile/ImageModal.tsx new file mode 100644 index 00000000..1fce96d4 --- /dev/null +++ b/frontend/src/components/user/editProfile/ImageModal.tsx @@ -0,0 +1,52 @@ +import { FC } from 'react' +import Cropper from 'react-easy-crop'; + +interface ImageModalProps { + image: string | null; + crop: { x: number; y: number }; + setCrop: React.Dispatch>; + zoom: number; + setZoom: React.Dispatch>; + onCropComplete: (croppedArea: any, croppedAreaPixels: { width: number; height: number; x: number; y: number }) => void; + handleSave: () => void; + closeModal: () => void; + } + +const ImageModal:FC= ({ + image, + crop, + setCrop, + zoom, + setZoom, + onCropComplete, + handleSave, + closeModal + }) => { + return ( +
+
+ + +
+
+ + +
+
+ ) +} + +export default ImageModal; \ No newline at end of file diff --git a/frontend/src/components/user/editProfile/PictureInput.tsx b/frontend/src/components/user/editProfile/PictureInput.tsx index 6a184b5f..abf8478f 100644 --- a/frontend/src/components/user/editProfile/PictureInput.tsx +++ b/frontend/src/components/user/editProfile/PictureInput.tsx @@ -1,13 +1,23 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { X } from "lucide-react"; -import { FC, useState, useRef } from "react"; +import { FC, useState, useRef, useCallback } from "react"; +import ImageModal from "./ImageModal"; +import { getCroppedImg } from "./cropImage"; const PictureInput: FC = () => { - const [image, setImage] = useState( - "https://res.cloudinary.com/dwyxogyrk/image/upload/v1737433466/h0xf7zi0blmclfqrjeo7.png" - ); + const [image, setImage] = useState(null); + const [croppedImage, setCroppedImage] = useState(null); + const [imageModal, setImageModal] = useState(false); const fileInputRef = useRef(null); + const [crop, setCrop] = useState({ x: 0, y: 0 }); + const [zoom, setZoom] = useState(1); + const [croppedAreaPixels, setCroppedAreaPixels] = useState<{ + width: number; + height: number; + x: number; + y: number; + }| null>(null); // Handle file selection const handleFileChange = (event: React.ChangeEvent) => { @@ -16,8 +26,8 @@ const PictureInput: FC = () => { const reader = new FileReader(); reader.onload = () => setImage(reader.result as string); reader.readAsDataURL(file); + setImageModal(true); } - // Reset input value to allow selecting the same file again if (fileInputRef.current) { fileInputRef.current.value = ""; @@ -30,7 +40,27 @@ const PictureInput: FC = () => { }; // Remove selected image - const removeImage = () => setImage(null); + const removeImage = () => { + setImage(null); + setCroppedImage(null); + } + + const onCropComplete = useCallback((_val: any, + croppedAreaPixels: { width: number; height: number; x: number; y: number }) => { + setCroppedAreaPixels(croppedAreaPixels); + }, []); + + const handleSaveImage = async() => { + if (image && croppedAreaPixels){ + try{ + const croppedImage = await getCroppedImg(image, croppedAreaPixels); + setCroppedImage(croppedImage); + setImageModal(false); + } catch (e) { + console.error(e); + } + } + } return (
@@ -43,8 +73,9 @@ const PictureInput: FC = () => { )} - {image ? ( - Profile + {croppedImage ? ( + Profile ) : (
No Image @@ -65,7 +96,7 @@ const PictureInput: FC = () => { {/* Button to Trigger File Input */} @@ -78,8 +109,20 @@ const PictureInput: FC = () => { )}
+ {imageModal && ( + setImageModal(false)} + /> + )}
); }; -export default PictureInput; +export default PictureInput; \ No newline at end of file diff --git a/frontend/src/components/user/editProfile/cropImage.ts b/frontend/src/components/user/editProfile/cropImage.ts new file mode 100644 index 00000000..084c73f5 --- /dev/null +++ b/frontend/src/components/user/editProfile/cropImage.ts @@ -0,0 +1,47 @@ +export const getCroppedImg = async ( + imageSrc: String, + croppedAreaPixels: { width: number; height: number; x: number; y: number; } + ):Promise => { + const image = await createImage(imageSrc as string); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + if (!ctx) throw new Error("Could not get canvas context"); + + canvas.width = croppedAreaPixels.width; + canvas.height = croppedAreaPixels.height; + + ctx.drawImage( + image, + croppedAreaPixels.x, + croppedAreaPixels.y, + croppedAreaPixels.width, + croppedAreaPixels.height, + 0, + 0, + croppedAreaPixels.width, + croppedAreaPixels.height + ); + + return new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(URL.createObjectURL(blob)); + } else { + reject(new Error("Canvas toBlob failed")); + } + }, "image/jpeg"); + }); +}; + + +export function createImage(url: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = "anonymous"; + img.src = url; + + img.onload = () => resolve(img); + img.onerror = (error) => reject(error); + }); + } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 76c50aaf..1c41e3f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,6 +96,7 @@ "next-themes": "^0.4.4", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-easy-crop": "^5.4.1", "react-hook-form": "^7.54.2", "react-icons": "^5.5.0", "react-markdown": "^10.0.1", @@ -6763,6 +6764,12 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-wheel": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", + "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==", + "license": "BSD-3-Clause" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7141,6 +7148,20 @@ "react": "^19.0.0" } }, + "node_modules/react-easy-crop": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.4.1.tgz", + "integrity": "sha512-Djtsi7bWO75vkKYkVxNRrJWY69pXLahIAkUN0mmt9cXNnaq2tpG59ctSY6P7ipJgBc7COJDRMRuwb2lYwtACNQ==", + "license": "MIT", + "dependencies": { + "normalize-wheel": "^1.0.1", + "tslib": "^2.0.1" + }, + "peerDependencies": { + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + } + }, "node_modules/react-hook-form": { "version": "7.54.2", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", From e79b31559cd045f4c2eb2cc14a593e837ba6a890 Mon Sep 17 00:00:00 2001 From: Mrlionbyte Date: Fri, 14 Mar 2025 09:41:45 +0530 Subject: [PATCH 2/4] fix: correct type for onCropComplete callback --- frontend/src/components/user/editProfile/ImageModal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/user/editProfile/ImageModal.tsx b/frontend/src/components/user/editProfile/ImageModal.tsx index 1fce96d4..7158bf12 100644 --- a/frontend/src/components/user/editProfile/ImageModal.tsx +++ b/frontend/src/components/user/editProfile/ImageModal.tsx @@ -7,7 +7,8 @@ interface ImageModalProps { setCrop: React.Dispatch>; zoom: number; setZoom: React.Dispatch>; - onCropComplete: (croppedArea: any, croppedAreaPixels: { width: number; height: number; x: number; y: number }) => void; + onCropComplete: (croppedArea: { x: number; y: number; width: number; height: number }, + croppedAreaPixels: { width: number; height: number; x: number; y: number }) => void; handleSave: () => void; closeModal: () => void; } From 5cc00d2bde5a90bacc5cfabd787a51200be97aea Mon Sep 17 00:00:00 2001 From: Mrlionbyte Date: Fri, 14 Mar 2025 09:53:06 +0530 Subject: [PATCH 3/4] fix: correct type using Area/react-easy-crop --- .../src/components/user/editProfile/ImageModal.tsx | 6 +++--- .../src/components/user/editProfile/PictureInput.tsx | 12 ++++-------- .../src/components/user/editProfile/cropImage.ts | 4 +++- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/user/editProfile/ImageModal.tsx b/frontend/src/components/user/editProfile/ImageModal.tsx index 7158bf12..4ae4dd6f 100644 --- a/frontend/src/components/user/editProfile/ImageModal.tsx +++ b/frontend/src/components/user/editProfile/ImageModal.tsx @@ -1,5 +1,5 @@ import { FC } from 'react' -import Cropper from 'react-easy-crop'; +import Cropper, { Area } from 'react-easy-crop'; interface ImageModalProps { image: string | null; @@ -7,8 +7,8 @@ interface ImageModalProps { setCrop: React.Dispatch>; zoom: number; setZoom: React.Dispatch>; - onCropComplete: (croppedArea: { x: number; y: number; width: number; height: number }, - croppedAreaPixels: { width: number; height: number; x: number; y: number }) => void; + onCropComplete: (croppedArea: Area, + croppedAreaPixels: Area) => void; handleSave: () => void; closeModal: () => void; } diff --git a/frontend/src/components/user/editProfile/PictureInput.tsx b/frontend/src/components/user/editProfile/PictureInput.tsx index abf8478f..f259455b 100644 --- a/frontend/src/components/user/editProfile/PictureInput.tsx +++ b/frontend/src/components/user/editProfile/PictureInput.tsx @@ -4,6 +4,7 @@ import { X } from "lucide-react"; import { FC, useState, useRef, useCallback } from "react"; import ImageModal from "./ImageModal"; import { getCroppedImg } from "./cropImage"; +import { Area } from "react-easy-crop"; const PictureInput: FC = () => { const [image, setImage] = useState(null); @@ -12,12 +13,7 @@ const PictureInput: FC = () => { const fileInputRef = useRef(null); const [crop, setCrop] = useState({ x: 0, y: 0 }); const [zoom, setZoom] = useState(1); - const [croppedAreaPixels, setCroppedAreaPixels] = useState<{ - width: number; - height: number; - x: number; - y: number; - }| null>(null); + const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); // Handle file selection const handleFileChange = (event: React.ChangeEvent) => { @@ -45,8 +41,8 @@ const PictureInput: FC = () => { setCroppedImage(null); } - const onCropComplete = useCallback((_val: any, - croppedAreaPixels: { width: number; height: number; x: number; y: number }) => { + const onCropComplete = useCallback((_val: Area, + croppedAreaPixels: Area) => { setCroppedAreaPixels(croppedAreaPixels); }, []); diff --git a/frontend/src/components/user/editProfile/cropImage.ts b/frontend/src/components/user/editProfile/cropImage.ts index 084c73f5..92816b4d 100644 --- a/frontend/src/components/user/editProfile/cropImage.ts +++ b/frontend/src/components/user/editProfile/cropImage.ts @@ -1,6 +1,8 @@ +import { Area } from "react-easy-crop"; + export const getCroppedImg = async ( imageSrc: String, - croppedAreaPixels: { width: number; height: number; x: number; y: number; } + croppedAreaPixels: Area ):Promise => { const image = await createImage(imageSrc as string); const canvas = document.createElement("canvas"); From fbbfbd34dd09eb4566aac1412e9ab5e3ddc3b35e Mon Sep 17 00:00:00 2001 From: Mrlionbyte Date: Fri, 14 Mar 2025 10:17:44 +0530 Subject: [PATCH 4/4] fix: change String to string type --- frontend/src/components/user/editProfile/cropImage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/user/editProfile/cropImage.ts b/frontend/src/components/user/editProfile/cropImage.ts index 92816b4d..46b8c133 100644 --- a/frontend/src/components/user/editProfile/cropImage.ts +++ b/frontend/src/components/user/editProfile/cropImage.ts @@ -1,7 +1,7 @@ import { Area } from "react-easy-crop"; export const getCroppedImg = async ( - imageSrc: String, + imageSrc: string, croppedAreaPixels: Area ):Promise => { const image = await createImage(imageSrc as string);