diff --git a/nextjs/matcha/package.json b/nextjs/matcha/package.json
index dac7959..80fa505 100644
--- a/nextjs/matcha/package.json
+++ b/nextjs/matcha/package.json
@@ -17,7 +17,7 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-easy-crop": "^5.5.6",
- "react-image-crop": "^11.0.10"
+ "react-image-crop": "^11.0.10",
},
"devDependencies": {
"@eslint/eslintrc": "^3",
diff --git a/nextjs/matcha/src/components/common/ImageCropper.tsx b/nextjs/matcha/src/components/common/ImageCropper.tsx
index 1421f09..e3a4b11 100644
--- a/nextjs/matcha/src/components/common/ImageCropper.tsx
+++ b/nextjs/matcha/src/components/common/ImageCropper.tsx
@@ -1,7 +1,10 @@
+"use client";
+
import Cropper from "react-easy-crop";
import IconButton from "@/components/common/IconButton";
import Button from "@/components/common/Button";
import getImageUrl from '@/utils/getImageUrl';
+import type { Area } from '@/utils/cropImage';
const CROP_AREA_ASPECT = 9 / 16;
@@ -20,7 +23,7 @@ export default function ImageCropper({
profilePicture: File | null;
additionalPictures: (File | null)[];
currentCroppingIndex: number;
- onCropComplete: (croppedArea: any, croppedAreaPixels: any) => void;
+ onCropComplete: (croppedArea: Area, croppedAreaPixels: Area) => void;
submitImage: () => void;
zoom: number;
setZoom: (zoom: number) => void;
@@ -29,10 +32,20 @@ export default function ImageCropper({
crop: { x: number; y: number };
setCrop: (crop: { x: number; y: number }) => void;
}) {
+ // Get the current image based on index (0 = profile picture, 1+ = additional pictures)
+ const getCurrentImage = (): File | null => {
+ if (currentCroppingIndex === 0) {
+ return profilePicture;
+ }
+ return additionalPictures[currentCroppingIndex - 1] || null;
+ };
+
+ const imageUrl = getImageUrl(getCurrentImage()) || "none";
+
return (
setRotation((rotation + 90) % 180)}
+ onClick={() => setRotation((rotation - 90 + 360) % 360)}
size="small"
- aria-label="Rotate right"
+ aria-label="Rotate left"
>
- \-
+
setRotation((rotation + 90) % 180)}
+ onClick={() => setRotation((rotation + 90) % 360)}
size="small"
aria-label="Rotate right"
>
- -/
+
@@ -64,4 +81,4 @@ export default function ImageCropper({
);
-}
\ No newline at end of file
+}
diff --git a/nextjs/matcha/src/utils/cropImage.ts b/nextjs/matcha/src/utils/cropImage.ts
index d20f200..66bdd5e 100644
--- a/nextjs/matcha/src/utils/cropImage.ts
+++ b/nextjs/matcha/src/utils/cropImage.ts
@@ -1,104 +1,105 @@
-type Area = {
- x: number
- y: number
- width: number
- height: number
+/**
+ * Create a cropped image from a source image URL
+ */
+export const createImage = (url: string): Promise
=>
+ new Promise((resolve, reject) => {
+ const image = new Image();
+ image.addEventListener('load', () => resolve(image));
+ image.addEventListener('error', (error) => reject(error));
+ image.setAttribute('crossOrigin', 'anonymous');
+ image.src = url;
+ });
+
+export function getRadianAngle(degreeValue: number): number {
+ return (degreeValue * Math.PI) / 180;
}
-type Point = {
- x: number
- y: number
+export function rotateSize(width: number, height: number, rotation: number): { width: number; height: number } {
+ const rotRad = getRadianAngle(rotation);
+ return {
+ width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
+ height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
+ };
}
-export const createImage = (url: string) =>
- new Promise((resolve, reject) => {
- const image = new Image();
- image.addEventListener('load', () => resolve(image));
- image.addEventListener('error', (error) => reject(error));
- image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox
- image.src = url;
- });
-
-export function getRadianAngle(degreeValue: number) {
- return (degreeValue * Math.PI) / 180
+export interface Area {
+ width: number;
+ height: number;
+ x: number;
+ y: number;
}
-export function rotateSize(width: number, height: number, rotation: number) {
- const rotRad = getRadianAngle(rotation);
+export type ImageFormat = 'image/jpeg' | 'image/png' | 'image/webp';
- return {
- width:
- Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
- height:
- Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
- };
+export interface CropOptions {
+ format?: ImageFormat;
+ quality?: number;
}
+/**
+ * Returns a cropped image as a data URL
+ * @param imageSrc - Source image URL
+ * @param pixelCrop - Crop area in pixels
+ * @param rotation - Rotation in degrees (0-360)
+ * @param flip - Flip options
+ * @param options - Output format and quality options
+ */
export default async function getCroppedImg(
- imageSrc: string,
- pixelCrop: Area,
- rotation = 0,
- flip = { horizontal: false, vertical: false }
-) {
- const image = await createImage(imageSrc)
- const canvas = document.createElement('canvas')
- const ctx = canvas.getContext('2d')
-
- if (!ctx) {
- return null;
- }
-
- const rotRad = getRadianAngle(rotation);
- console.log('rotRad', rotRad);
-
- const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
- image.width as number,
- image.height as number,
- rotation
- );
-
- canvas.width = bBoxWidth;
- canvas.height = bBoxHeight;
-
- ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
- ctx.rotate(rotRad);;
- ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
- ctx.translate(-image.width / 2, -image.height / 2);
-
- ctx.drawImage(image, 0, 0);
-
- const croppedCanvas = document.createElement('canvas');
-
- const croppedCtx = croppedCanvas.getContext('2d');
-
- if (!croppedCtx) {
- return null;
- }
-
- croppedCanvas.width = pixelCrop.width;
- croppedCanvas.height = pixelCrop.height;
-
- croppedCtx.drawImage(
- canvas,
- pixelCrop.x,
- pixelCrop.y,
- pixelCrop.width,
- pixelCrop.height,
- 0,
- 0,
- pixelCrop.width,
- pixelCrop.height
- );
-
- // return croppedCanvas.toDataURL('image/jpeg');
-
- return new Promise((resolve, reject) => {
- croppedCanvas.toBlob((file: Blob | null) => {
- if (file) {
- resolve(URL.createObjectURL(file));
- } else {
- reject(new Error('Canvas is empty'));
- }
- }, 'image/jpeg');
- });
+ imageSrc: string,
+ pixelCrop: Area,
+ rotation = 0,
+ flip = { horizontal: false, vertical: false },
+ options: CropOptions = {}
+): Promise {
+ const { format = 'image/jpeg', quality = 0.9 } = options;
+
+ const image = await createImage(imageSrc);
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ if (!ctx) {
+ return null;
+ }
+
+ const rotRad = getRadianAngle(rotation);
+
+ const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
+ image.width,
+ image.height,
+ rotation
+ );
+
+ canvas.width = bBoxWidth;
+ canvas.height = bBoxHeight;
+
+ ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
+ ctx.rotate(rotRad);
+ ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
+ ctx.translate(-image.width / 2, -image.height / 2);
+
+ ctx.drawImage(image, 0, 0);
+
+ const croppedCanvas = document.createElement('canvas');
+ const croppedCtx = croppedCanvas.getContext('2d');
+
+ if (!croppedCtx) {
+ return null;
+ }
+
+ croppedCanvas.width = pixelCrop.width;
+ croppedCanvas.height = pixelCrop.height;
+
+ croppedCtx.drawImage(
+ canvas,
+ pixelCrop.x,
+ pixelCrop.y,
+ pixelCrop.width,
+ pixelCrop.height,
+ 0,
+ 0,
+ pixelCrop.width,
+ pixelCrop.height
+ );
+
+ return croppedCanvas.toDataURL(format, quality);
}
diff --git a/nextjs/matcha/src/utils/getImageUrl.ts b/nextjs/matcha/src/utils/getImageUrl.ts
index 2164bc6..d598760 100644
--- a/nextjs/matcha/src/utils/getImageUrl.ts
+++ b/nextjs/matcha/src/utils/getImageUrl.ts
@@ -1,3 +1,6 @@
+/**
+ * Convert a File object to a data URL for display
+ */
export default function getImageUrl(file: File | null): string | null {
return file ? URL.createObjectURL(file) : null;
-};
\ No newline at end of file
+}