// 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
) : ( )}
) })}
)}
) }