From 23f62fde3d75e13fc63954bc041b5ea63e74f4cf Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 12 Apr 2026 20:03:34 +0200 Subject: [PATCH] feat(29-03): create ImageCropEditor component Zoom+pan editor using react-easy-crop with zoom slider, save/cancel buttons, and dominant color background. Returns crop coordinates for persistence. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client/components/ImageCropEditor.tsx | 135 ++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/client/components/ImageCropEditor.tsx diff --git a/src/client/components/ImageCropEditor.tsx b/src/client/components/ImageCropEditor.tsx new file mode 100644 index 0000000..fbd866b --- /dev/null +++ b/src/client/components/ImageCropEditor.tsx @@ -0,0 +1,135 @@ +import { useCallback, useState } from "react"; +import Cropper from "react-easy-crop"; +import type { Area, Point } from "react-easy-crop"; + +interface CropResult { + zoom: number; + x: number; + y: number; +} + +interface ImageCropEditorProps { + imageUrl: string; + dominantColor?: string | null; + initialZoom?: number; + initialX?: number; + initialY?: number; + aspect?: number; + onSave: (result: CropResult) => void; + onCancel: () => void; +} + +export function ImageCropEditor({ + imageUrl, + dominantColor, + initialZoom = 1, + initialX = 0, + initialY = 0, + aspect = 4 / 3, + onSave, + onCancel, +}: ImageCropEditorProps) { + const [crop, setCrop] = useState({ x: initialX, y: initialY }); + const [zoom, setZoom] = useState(initialZoom); + + const onCropComplete = useCallback( + (_croppedArea: Area, _croppedAreaPixels: Area) => { + // Crop/zoom state is tracked via setCrop/setZoom, not this callback + }, + [], + ); + + function handleSave() { + onSave({ + zoom, + x: crop.x, + y: crop.y, + }); + } + + return ( +
+ {/* Crop area */} +
+ +
+ + {/* Zoom slider */} +
+ + + + + + + setZoom(Number(e.target.value))} + className="flex-1 h-1.5 bg-gray-200 rounded-full appearance-none cursor-pointer accent-gray-900" + /> + + + + + +
+ + {/* Action buttons */} +
+ + +
+
+ ); +}