"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)} /> ); }