"use client"; import { useCallback, useRef, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Loader2, Upload, X, ImageIcon } from "lucide-react"; import type { UploadImageResponse } from "@/app/api/upload-image/route"; interface Props { /** Current image URL (controlled) */ value?: string; /** Called when a new image URL is ready (after upload) or cleared (empty string) */ onChange: (url: string) => void; /** Alt text value */ altValue?: string; /** Called when alt text changes */ onAltChange?: (alt: string) => void; /** Label shown above the dropzone */ label?: string; } const ACCEPTED = "image/jpeg,image/png,image/gif,image/webp,image/svg+xml,image/avif"; export function ImageUploader({ value, onChange, altValue = "", onAltChange, label = "Featured Image", }: Props) { const [uploading, setUploading] = useState(false); const [dragOver, setDragOver] = useState(false); const fileInputRef = useRef(null); const uploadFile = useCallback( async (file: File) => { setUploading(true); try { const fd = new FormData(); fd.append("file", file); const res = await fetch("/api/upload-image", { method: "POST", body: fd, }); const data = (await res.json()) as UploadImageResponse & { error?: string }; if (!res.ok || !data.success) { throw new Error(data.error || "Upload failed"); } onChange(data.url); toast.success("Image uploaded successfully!"); } catch (err) { toast.error(err instanceof Error ? err.message : "Upload failed"); } finally { setUploading(false); } }, [onChange] ); // ── Drag & Drop handlers ─────────────────────────────────────────────────── const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setDragOver(true); }; const handleDragLeave = () => setDragOver(false); const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setDragOver(false); const file = e.dataTransfer.files[0]; if (file) uploadFile(file); }; // ── File input change ────────────────────────────────────────────────────── const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { uploadFile(file); // Reset so the same file can be re-selected if removed e.target.value = ""; } }; const handleRemove = () => { onChange(""); onAltChange?.(""); }; return (
{value ? ( /* ── Preview ──────────────────────────────────────────────────────── */
{/* Plain — domain is dynamic (R2/custom), avoids next/image remotePatterns config */} {/* eslint-disable-next-line @next/next/no-img-element */} {altValue {/* Overlay — Replace / Remove actions */}
{uploading && (
)}
) : ( /* ── Dropzone ─────────────────────────────────────────────────────── */ )} {/* Hidden file input */} {/* Alt text field — always shown */}
onAltChange?.(e.target.value)} />
); }