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