import { useRef, useState } from "react"; import { apiUpload } from "../lib/api"; import { GearImage, imageContainerBg } from "./GearImage"; import { ImageCropEditor } from "./ImageCropEditor"; interface ImageUploadProps { value: string | null; imageUrl?: string | null; dominantColor?: string | null; onChange: (filename: string | null) => void; onCropChange?: (crop: { zoom: number; x: number; y: number }) => void; } const MAX_SIZE_BYTES = 5 * 1024 * 1024; // 5MB const ACCEPTED_TYPES = ["image/jpeg", "image/png", "image/webp"]; export function ImageUpload({ value: _value, imageUrl, dominantColor, onChange, onCropChange, }: ImageUploadProps) { const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const [localPreview, setLocalPreview] = useState(null); const [showCropEditor, setShowCropEditor] = useState(false); const inputRef = useRef(null); async function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setError(null); if (!ACCEPTED_TYPES.includes(file.type)) { setError("Please select a JPG, PNG, or WebP image."); return; } if (file.size > MAX_SIZE_BYTES) { setError("Image must be under 5MB."); return; } // Create local preview for immediate display const previewUrl = URL.createObjectURL(file); setLocalPreview(previewUrl); setUploading(true); try { const result = await apiUpload<{ filename: string }>("/api/images", file); onChange(result.filename); if (onCropChange) { setShowCropEditor(true); } } catch { setError("Upload failed. Please try again."); setLocalPreview(null); } finally { setUploading(false); // Reset input so the same file can be re-selected if (inputRef.current) inputRef.current.value = ""; } } function handleRemove(e: React.MouseEvent) { e.stopPropagation(); setLocalPreview(null); onChange(null); } // Determine the display URL: local preview takes priority (just-uploaded), // then presigned URL from API, then nothing const displayUrl = localPreview || imageUrl || null; return (
{/* Crop editor overlay */} {showCropEditor && displayUrl && onCropChange && (
{ onCropChange(result); setShowCropEditor(false); }} onCancel={() => setShowCropEditor(false)} />
)} {/* Hero image area */} {!showCropEditor && (
inputRef.current?.click()} className="relative w-full aspect-[4/3] rounded-xl overflow-hidden cursor-pointer group" style={{ backgroundColor: displayUrl ? imageContainerBg(dominantColor) : undefined, }} > {displayUrl ? ( <> {/* Remove button */} ) : (
{/* ImagePlus icon */} Click to add photo
)} {/* Upload spinner overlay */} {uploading && (
)}
)} {error &&

{error}

}
); }