// src/app/[locale]/admin/import/effects/import-effects-client.tsx "use client"; import { Check, Download, Loader2, Music, Package, Search, Sparkles, Trash2, X, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { ImportErrorBanner } from "@/components/admin/import-error-banner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { adminFetch } from "@/lib/admin-fetch"; import { runSseImport } from "@/lib/sse-client"; import { cn } from "@/lib/utils"; interface EffectItem { id: string; lib: string; type: string; revision: number; present: boolean; nitroExists: boolean; } type Filter = "all" | "fx" | "dance"; export function ImportEffectsClient() { const [searchTerm, setSearchTerm] = useState(""); const [effects, setEffects] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [filter, setFilter] = useState("all"); const [selected, setSelected] = useState>(new Set()); const [batchProgress, setBatchProgress] = useState<{ done: number; total: number; } | null>(null); const [busyLib, setBusyLib] = useState(null); const batchAbortRef = useRef(null); const fetchEffects = useCallback(async (search: string) => { setLoading(true); setError(""); try { const params = new URLSearchParams(); if (search) params.set("search", search); const res = await adminFetch(`/api/admin/import/effects?${params}`); const data = await res.json(); if (!res.ok) { setError(data.error || "Failed to load"); return; } setEffects(data.effects || []); setSelected(new Set()); } catch { setError("Network error"); } finally { setLoading(false); } }, []); useEffect(() => { fetchEffects(""); }, [fetchEffects]); const visible = useMemo( () => effects.filter((e) => filter === "all" || e.type === filter), [effects, filter], ); const importable = useMemo( () => visible.filter((e) => !e.present || !e.nitroExists), [visible], ); const importedCount = useMemo( () => visible.filter((e) => e.present && e.nitroExists).length, [visible], ); function toggle(lib: string) { setSelected((prev) => { const n = new Set(prev); if (n.has(lib)) n.delete(lib); else n.add(lib); return n; }); } function markDone(lib: string) { setEffects((prev) => prev.map((e) => e.lib === lib ? { ...e, present: true, nitroExists: true } : e, ), ); } async function importViaSse(items: EffectItem[]) { batchAbortRef.current?.abort(); const abort = new AbortController(); batchAbortRef.current = abort; setBatchProgress({ done: 0, total: items.length }); let done = 0; try { await runSseImport( "/api/admin/import/effects/batch", { items, concurrency: 3 }, (lib) => { done++; markDone(lib); setBatchProgress({ done, total: items.length }); }, (succeeded, failed) => { setSelected(new Set()); if (succeeded > 0) toast.success(`${succeeded} effect(s) imported`); if (failed > 0) toast.error(`${failed} failed`); }, abort.signal, () => { done++; setBatchProgress({ done, total: items.length }); }, ); } catch (err) { if ((err as Error)?.name === "AbortError") { toast.info("Import cancelled"); } else { throw err; } } finally { if (batchAbortRef.current === abort) batchAbortRef.current = null; setBatchProgress(null); } } async function importOne(e: EffectItem) { setBusyLib(e.lib); try { await importViaSse([e]); } finally { setBusyLib(null); } } async function importSelected() { const items = visible.filter( (e) => selected.has(e.lib) && (!e.present || !e.nitroExists), ); if (items.length === 0) return; await importViaSse(items); } async function remove(e: EffectItem) { setBusyLib(e.lib); try { const res = await adminFetch( `/api/admin/import/effects?lib=${encodeURIComponent(e.lib)}`, { method: "DELETE", }, ); const data = await res.json(); if (res.ok) { toast.success(`${e.lib} removed`); setEffects((prev) => prev.map((x) => x.lib === e.lib ? { ...x, present: false, nitroExists: false } : x, ), ); } else { toast.error(data.error || "Delete failed"); } } finally { setBusyLib(null); } } return (
{/* Stats */} {effects.length > 0 && (
{visible.length} effects
{importedCount} imported
{importable.length} available
)} {/* Search + filter */}
setSearchTerm(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") fetchEffects(searchTerm); if (e.key === "Escape") { setSearchTerm(""); fetchEffects(""); } }} className="pl-9 pr-8" /> {searchTerm && ( )}
{(["all", "fx", "dance"] as Filter[]).map((f) => ( ))}
{/* Bulk actions */} {importable.length > 0 && (
{selected.size > 0 && ( )} {batchProgress && ( <> {batchProgress.done}/{batchProgress.total} )}
)} {/* Error */} {error && } {/* Empty */} {!loading && visible.length === 0 && !error && (

No effects found

)} {/* Grid */} {visible.length > 0 && (
{visible.map((e) => { const imported = e.present && e.nitroExists; const isSelected = selected.has(e.lib); return (
{!imported && ( )}
{e.type === "dance" ? ( ) : ( )} {e.type}
{e.lib}
id {e.id}
{imported ? (
Imported
) : ( )}
); })}
)}
); }