'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, DialogTrigger, } 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)} /> ) }