// src/app/[locale]/admin/import/effects/import-effects-client.tsx "use client"; import { AlertCircle, Check, Download, Loader2, Music, Package, Search, Sparkles, Trash2, X, } from "lucide-react"; import { useCallback, useEffect, useMemo, 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 { 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 fetchEffects = useCallback(async (search: string) => { setLoading(true); setError(""); try { const params = new URLSearchParams(); if (search) params.set("search", search); const res = await fetch(`/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[]) { setBatchProgress({ done: 0, total: items.length }); const res = await fetch("/api/admin/import/effects/batch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items, concurrency: 3 }), }); if (!res.body) { toast.error("No response stream"); setBatchProgress(null); return; } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = ""; let done = 0; let succeeded = 0; let failed = 0; while (true) { const { value, done: streamDone } = await reader.read(); if (streamDone) break; buf += decoder.decode(value, { stream: true }); const parts = buf.split("\n\n"); buf = parts.pop() ?? ""; for (const part of parts) { if (!part.startsWith("data: ")) continue; const evt = JSON.parse(part.slice(6)); if (evt.type === "item_progress" && evt.status === "done") { done++; succeeded++; markDone(evt.classname); setBatchProgress({ done, total: items.length }); } else if (evt.type === "item_progress" && evt.status === "failed") { done++; failed++; setBatchProgress({ done, total: items.length }); } else if (evt.type === "batch_complete") { succeeded = evt.succeeded; failed = evt.failed; } } } setBatchProgress(null); setSelected(new Set()); if (succeeded > 0) toast.success(`${succeeded} effect(s) imported`); if (failed > 0) toast.error(`${failed} failed`); } 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 fetch(`/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 && (
{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
) : ( )}
); })}
)}
); }