'use client' import { Loader2, Package, 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 IconPickerProps { value: number onChange: (iconImage: number) => void disabled?: boolean /** Optional trigger override. If not provided, a default button is rendered. */ children?: React.ReactNode } const PAGE_SIZE = 120 export function IconPicker({ value, onChange, disabled, children }: IconPickerProps) { const [open, setOpen] = useState(false) const [search, setSearch] = useState('') const [debounced, setDebounced] = useState('') const [icons, setIcons] = 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 fetchIcons = useCallback( async (nextOffset: number, replace: boolean) => { setLoading(true) try { const params = new URLSearchParams({ limit: String(PAGE_SIZE), offset: String(nextOffset), }) if (debounced) params.set('search', debounced) const res = await fetch(`/api/admin/catalog/icons?${params}`) if (!res.ok) throw new Error('Failed to load icons') const data = await res.json() const fetched: number[] = data.icons ?? [] setTotal(data.total ?? 0) setHasMore(nextOffset + fetched.length < (data.total ?? 0)) setIcons((prev) => (replace ? fetched : [...prev, ...fetched])) setOffset(nextOffset + fetched.length) } catch (err) { console.error('[IconPicker]', err) } finally { setLoading(false) } }, [debounced], ) // Reset and fetch on open / search change // biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code useEffect(() => { if (!open) return setIcons([]) setOffset(0) setHasMore(true) fetchIcons(0, true) if (scrollRef.current) scrollRef.current.scrollTop = 0 }, [open, debounced, fetchIcons]) const handleScroll = useCallback(() => { const el = scrollRef.current if (!el || loading || !hasMore) return if (el.scrollTop + el.clientHeight >= el.scrollHeight - 200) { fetchIcons(offset, false) } }, [loading, hasMore, offset, fetchIcons]) const handleSelect = (id: number) => { onChange(id) setOpen(false) } const handleClear = () => { onChange(0) setOpen(false) } return ( {children ?? ( )} Select Catalog Icon
setSearch(e.target.value)} className="pl-9 pr-9" autoFocus /> {search && ( )}
{total} icons
{icons.length === 0 && !loading && (

No icons found

)}
{icons.map((id) => ( ))}
{loading && (
)}
) } // ── Small preview component (inlined to avoid circular imports) ─────── function IconPreview({ iconImage, size = 24 }: { iconImage: number; size?: number }) { const [error, setError] = useState(false) // biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code useEffect(() => setError(false), [iconImage]) if (error || iconImage <= 0) { return ( ) } return ( setError(true)} /> ) }