"use client"; import { ImageIcon, Loader2, Search, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; interface CatalogImagePickerProps { value: string; onChange: (name: string) => void; /** Filter: 'header' | 'teaser' | 'all' */ type?: "header" | "teaser" | "all"; disabled?: boolean; label?: string; } const PAGE_SIZE = 80; export function CatalogImagePicker({ value, onChange, type = "all", disabled, label, }: CatalogImagePickerProps) { const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const [debounced, setDebounced] = useState(""); const [images, setImages] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [offset, setOffset] = useState(0); const [hasMore, setHasMore] = useState(true); const scrollRef = useRef(null); // Debounce search useEffect(() => { const t = setTimeout(() => setDebounced(search.trim()), 250); return () => clearTimeout(t); }, [search]); const fetchImages = useCallback( async (nextOffset: number, replace: boolean) => { setLoading(true); try { const params = new URLSearchParams({ limit: String(PAGE_SIZE), offset: String(nextOffset), type, }); if (debounced) params.set("search", debounced); const res = await fetch(`/api/admin/catalog/images?${params}`); if (!res.ok) throw new Error(); const data = await res.json(); const fetched: string[] = data.images ?? []; setTotal(data.total ?? 0); setHasMore(nextOffset + fetched.length < (data.total ?? 0)); setImages((prev) => (replace ? fetched : [...prev, ...fetched])); setOffset(nextOffset + fetched.length); } catch { // ignore } finally { setLoading(false); } }, [debounced, type], ); // Reset and fetch on open / search change // biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code useEffect(() => { if (!open) return; setImages([]); setOffset(0); setHasMore(true); fetchImages(0, true); if (scrollRef.current) scrollRef.current.scrollTop = 0; }, [open, debounced, fetchImages]); const handleScroll = useCallback(() => { const el = scrollRef.current; if (!el || loading || !hasMore) return; if (el.scrollTop + el.clientHeight >= el.scrollHeight - 300) { fetchImages(offset, false); } }, [loading, hasMore, offset, fetchImages]); const handleSelect = (name: string) => { onChange(name); setOpen(false); }; const handleClear = () => { onChange(""); setOpen(false); }; return ( {type === "header" ? "Select Headline Image" : type === "teaser" ? "Select Teaser Image" : "Select Catalog Image"}
setSearch(e.target.value)} className="pl-9 pr-9" autoFocus /> {search && ( )}
{total} images
{images.length === 0 && !loading && (

No images found

)}
{images.map((name) => ( ))}
{loading && (
)}
); } // ── Thumbnail with gif/png fallback ───────────────────────────── function CatalogImageThumb({ name, size = 60, }: { name: string; size?: number; }) { const [error, setError] = useState(0); // Reset error state when name changes // biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code useEffect(() => setError(0), [name]); if (!name || error >= 2) { return (
); } // Try .png first, fallback to .gif const src = error === 0 ? `/swf/c_images/catalogue/${name}.png` : `/swf/c_images/catalogue/${name}.gif`; return ( {name} setError((e) => e + 1)} /> ); }