"use client"; import { Loader2, Package, Search, Zap } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { createCatalogItem } from "@/actions/catalog-items"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { useServerAction } from "@/hooks/use-server-action"; import { getHabboCdnIconUrl, getLocalIconUrl } from "@/lib/furni/classname"; interface FurniResult { id: number; itemName: string; publicName: string; type: string; spriteId: number; } interface QuickAddFurniProps { pageId: number; furniRevisionMap: Record; } export function QuickAddFurni({ pageId, furniRevisionMap, }: QuickAddFurniProps) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [debounced, setDebounced] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const { run, isPending } = useServerAction(); const inputRef = useRef(null); // Keyboard shortcut: Ctrl+K / Cmd+K useEffect(() => { function onKeyDown(e: KeyboardEvent) { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { const tag = (e.target as HTMLElement)?.tagName; if (tag === "INPUT" || tag === "TEXTAREA") return; e.preventDefault(); setOpen(true); } } document.addEventListener("keydown", onKeyDown); return () => document.removeEventListener("keydown", onKeyDown); }, []); // Debounce search useEffect(() => { const t = setTimeout(() => setDebounced(query.trim()), 300); return () => clearTimeout(t); }, [query]); // Focus input when opened useEffect(() => { if (open) setTimeout(() => inputRef.current?.focus(), 50); }, [open]); // Fetch results useEffect(() => { if (!open || debounced.length < 2) { setResults([]); return; } setLoading(true); const ac = new AbortController(); fetch( `/api/admin/furni/search?q=${encodeURIComponent(debounced)}&limit=30`, { signal: ac.signal, }, ) .then((r) => r.json()) .then((data) => setResults(data.results ?? [])) .catch(() => {}) .finally(() => setLoading(false)); return () => ac.abort(); }, [debounced, open]); const handleAdd = useCallback( (base: FurniResult) => { run( () => createCatalogItem({ pageId, itemIds: String(base.id), catalogName: base.publicName || base.itemName, costCredits: 0, costPoints: 0, pointsType: 0, amount: 1, limitedSells: 0, limitedStack: 0, orderNumber: 1, offerId: -1, songId: 0, haveOffer: "1", clubOnly: "0", extradata: "", }), { successMessage: `Added "${base.publicName || base.itemName}" to page.`, errorMessage: "Failed to add item.", onSuccess: () => { toast.success(`Added #${base.id}`); }, }, ); }, [pageId, run], ); return ( Quick Add Furni
setQuery(e.target.value)} className="pl-9" /> {loading && ( )}
{debounced.length < 2 ? (

Type at least 2 characters to search

) : results.length === 0 && !loading ? (

No furni found for "{debounced}"

) : (
{results.map((r) => { const furni = furniRevisionMap[r.id]; return ( ); })}
)}
); } function FurniThumb({ classname, revision, }: { classname: string; revision: number; }) { if (!classname) { return (
); } const primary = getLocalIconUrl(classname); function handleError(e: React.SyntheticEvent) { const img = e.target as HTMLImageElement; if (!img.dataset.fallback && revision > 0) { img.dataset.fallback = "cdn"; img.src = getHabboCdnIconUrl(classname, revision); } else { img.style.opacity = "0"; } } return (
{/* eslint-disable-next-line @next/next/no-img-element */} {classname}
); }