"use client"; import { AlertCircle, BadgeCheck, Check, CheckCircle2, Download, Globe, Loader2, Package, Pencil, Search, X, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { adminFetch } from "@/lib/admin-fetch"; import { cn } from "@/lib/utils"; import { BadgeEditorDialog } from "./badge-editor-dialog"; interface BadgeItem { id: number; code: string; name: string; description: string; hotel: string; url_habbo: string; url_habboassets: string; alreadyImported: boolean; } const PAGE_SIZE = 100; export function ImportBadgesClient() { const [searchTerm, setSearchTerm] = useState(""); const [activeSearch, setActiveSearch] = useState(""); const [badges, setBadges] = useState([]); const [offset, setOffset] = useState(0); const [hasMore, setHasMore] = useState(false); const [loading, setLoading] = useState(false); const [importingCode, setImportingCode] = useState(null); const [error, setError] = useState(""); const [allHotels, setAllHotels] = useState(true); const [editingCode, setEditingCode] = useState(null); const [selected, setSelected] = useState>(new Set()); const [batchImporting, setBatchImporting] = useState(false); const searchRef = useRef(null); const fetchBadges = useCallback( async (search: string, off: number, all: boolean) => { setLoading(true); setError(""); try { const params = new URLSearchParams({ offset: String(off) }); if (search) params.set("search", search); if (all) params.set("allHotels", "1"); const res = await fetch(`/api/admin/import/badges?${params}`); const data = await res.json(); if (!res.ok) { setError(data.error || "Failed to load"); return; } const list = data.badges || []; setBadges(list); setHasMore(list.length >= PAGE_SIZE); setSelected(new Set()); } catch { setError("Network error"); } finally { setLoading(false); } }, [], ); // Auto-load on mount and when allHotels changes // biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code useEffect(() => { setOffset(0); fetchBadges(activeSearch, 0, allHotels); }, [fetchBadges, allHotels]); // "/" keyboard shortcut to focus search useEffect(() => { function onKeyDown(e: KeyboardEvent) { if (e.key === "/" && !e.metaKey && !e.ctrlKey && !e.altKey) { const tag = (e.target as HTMLElement)?.tagName; if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return; e.preventDefault(); searchRef.current?.focus(); } } document.addEventListener("keydown", onKeyDown); return () => document.removeEventListener("keydown", onKeyDown); }, []); function doSearch() { setActiveSearch(searchTerm); setOffset(0); fetchBadges(searchTerm, 0, allHotels); } function goPage(direction: "prev" | "next") { const newOffset = direction === "next" ? offset + PAGE_SIZE : Math.max(0, offset - PAGE_SIZE); setOffset(newOffset); fetchBadges(activeSearch, newOffset, allHotels); } async function postImportBadge(badge: BadgeItem) { const res = await adminFetch("/api/admin/import/badges", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: badge.code, name: badge.name, description: badge.description, }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { return { ok: false as const, error: (data as { error?: string }).error || `Failed to import ${badge.code}`, }; } return { ok: true as const }; } async function doImport(badge: BadgeItem) { setImportingCode(badge.code); try { const result = await postImportBadge(badge); if (result.ok) { toast.success(`${badge.code} imported`); setBadges((prev) => prev.map((b) => b.code === badge.code ? { ...b, alreadyImported: true } : b, ), ); setSelected((prev) => { const n = new Set(prev); n.delete(badge.code); return n; }); } else { toast.error(result.error); } } catch { toast.error(`Network error importing ${badge.code}`); } finally { setImportingCode(null); } } async function doBatchImport() { const toImport = badges.filter( (b) => selected.has(b.code) && !b.alreadyImported, ); if (toImport.length === 0) return; setBatchImporting(true); let ok = 0, fail = 0; for (const badge of toImport) { try { const result = await postImportBadge(badge); if (result.ok) { ok++; setBadges((prev) => prev.map((b) => b.code === badge.code ? { ...b, alreadyImported: true } : b, ), ); } else { fail++; } } catch { fail++; } } setBatchImporting(false); setSelected(new Set()); if (ok > 0) toast.success(`${ok} badge(s) imported`); if (fail > 0) toast.error(`${fail} failed`); } function toggleSelect(code: string) { setSelected((prev) => { const next = new Set(prev); if (next.has(code)) next.delete(code); else next.add(code); return next; }); } const importableBadges = useMemo( () => badges.filter((b) => !b.alreadyImported), [badges], ); function toggleSelectAll() { if (selected.size === importableBadges.length) { setSelected(new Set()); } else { setSelected(new Set(importableBadges.map((b) => b.code))); } } // Stats const importedCount = useMemo( () => badges.filter((b) => b.alreadyImported).length, [badges], ); const currentPage = Math.floor(offset / PAGE_SIZE) + 1; return (
{/* Stats bar */} {badges.length > 0 && (
{badges.length} results
{importedCount} imported
{importableBadges.length > 0 && (
{importableBadges.length} available
)}
)} {/* Search bar */}
setSearchTerm(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") doSearch(); if (e.key === "Escape") { setSearchTerm(""); searchRef.current?.blur(); } }} className="pl-9 pr-8" /> {searchTerm && ( )}

{allHotels ? "Showing all hotels — click for Italian only" : "Showing Italian only — click for all hotels"}

{/* Bulk actions */} {importableBadges.length > 0 && (
{selected.size > 0 && ( )}
)} {/* Error */} {error && (
{error}
)} {/* Loading skeleton */} {loading && badges.length === 0 && (
{Array.from({ length: 18 }, (_, i) => i).map((n) => (
))}
)} {/* Empty state */} {!loading && badges.length === 0 && !error && (

No badges found{activeSearch ? ` for "${activeSearch}"` : ""}

{!activeSearch && (

Try searching by badge name or code

)}
)} {/* Badge grid */} {badges.length > 0 && (
{badges.map((badge) => { const isSelected = selected.has(badge.code); return (
{/* Checkbox for non-imported */} {!badge.alreadyImported && ( )} {/* Badge image area */}
{badge.code} { const img = e.target as HTMLImageElement; if (!img.dataset.fallback) { img.dataset.fallback = "1"; img.src = badge.url_habboassets; } else { img.style.opacity = "0.3"; } }} />
{/* Info */}
{badge.code}
{badge.name && (
{badge.name}

{badge.name}

{badge.description && (

{badge.description}

)}
)} {allHotels && badge.hotel && ( {badge.hotel.toUpperCase()} )} {/* Action */}
{badge.alreadyImported ? (
Imported
) : ( )}
); })}
)} {/* Pagination */} {(offset > 0 || hasMore) && (
Page {currentPage}
)} {/* Badge Editor Dialog */} { if (!open) setEditingCode(null); }} />
); }