"use client"; import { useCallback, useEffect, useState } from "react"; import Cropper, { type Area } from "react-easy-crop"; import { getCroppedImageFile } from "@/lib/cropImage"; import styles from "./ImageCropDialog.module.css"; const MIN_ZOOM = 0.2; const MAX_ZOOM = 3; const ZOOM_STEP = 0.05; type Props = { open: boolean; imageSrc: string | null; title: string; aspect: number; /** Prefer PNG for logos with transparency */ outputType?: "image/jpeg" | "image/png"; fileName?: string; onCancel: () => void; onComplete: (file: File, previewUrl: string) => void; }; export default function ImageCropDialog({ open, imageSrc, title, aspect, outputType = "image/jpeg", fileName = "image", onCancel, onComplete, }: Props) { const [crop, setCrop] = useState({ x: 0, y: 0 }); const [zoom, setZoom] = useState(1); const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const transparentOutput = outputType === "image/png"; useEffect(() => { if (!open) return; setCrop({ x: 0, y: 0 }); setZoom(1); setCroppedAreaPixels(null); setBusy(false); setError(""); }, [open, imageSrc]); const onCropComplete = useCallback((_: Area, pixels: Area) => { setCroppedAreaPixels(pixels); }, []); function clampZoom(value: number) { return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Number(value.toFixed(2)))); } if (!open || !imageSrc) return null; async function apply() { if (!croppedAreaPixels || !imageSrc) return; setBusy(true); setError(""); try { const file = await getCroppedImageFile( imageSrc, croppedAreaPixels, fileName, outputType, ); const previewUrl = URL.createObjectURL(file); onComplete(file, previewUrl); setCrop({ x: 0, y: 0 }); setZoom(1); } catch (err) { setError(err instanceof Error ? err.message : "Could not crop image."); } finally { setBusy(false); } } return (
Drag to move · zoom out to add padding around the image
{error}
) : null}