'use client' import { useEffect, useState, useRef } from 'react' import Image from 'next/image' import type { Library, LibraryType } from '@/types' const TYPE_ICONS: Record = { games: '๐ŸŽฎ', mixed: '๐Ÿ—‚๏ธ', movies: '๐ŸŽฌ', tv: '๐Ÿ“บ', } const TYPE_LABELS: Record = { games: 'Games', mixed: 'Mixed Media', movies: 'Movies', tv: 'TV Shows', } // โ”€โ”€โ”€ Main Page โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ export default function ManagePage() { const [libraries, setLibraries] = useState([]) const [loading, setLoading] = useState(true) const refresh = () => { fetch('/api/libraries') .then((r) => r.json()) .then((data: Library[]) => { setLibraries(data) setLoading(false) }) .catch(() => setLoading(false)) } useEffect(() => { refresh() }, []) return (

Manage Libraries

Add or remove media library folders.

{loading ? ( ) : libraries.length === 0 ? (

No libraries configured yet.

) : (
{libraries.map((lib) => ( ))}
)}
) } // โ”€โ”€โ”€ Section wrapper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function Section({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
) } // โ”€โ”€โ”€ Library Row โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function LibraryRow({ library, onRemoved, onUpdated, }: { library: Library onRemoved: () => void onUpdated: () => void }) { const [confirming, setConfirming] = useState(false) const [removing, setRemoving] = useState(false) const [uploadingCover, setUploadingCover] = useState(false) const cancelRef = useRef | null>(null) const fileInputRef = useRef(null) const handleRemoveClick = () => { if (!confirming) { setConfirming(true) cancelRef.current = setTimeout(() => setConfirming(false), 4000) return } if (cancelRef.current) clearTimeout(cancelRef.current) setRemoving(true) fetch(`/api/libraries/${encodeURIComponent(library.id)}`, { method: 'DELETE' }) .then(() => onRemoved()) .catch(() => setRemoving(false)) } const handleCancel = () => { if (cancelRef.current) clearTimeout(cancelRef.current) setConfirming(false) } const handleCoverChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return setUploadingCover(true) const form = new FormData() form.append('cover', file) await fetch(`/api/library-cover/${encodeURIComponent(library.id)}`, { method: 'POST', body: form, }) setUploadingCover(false) // Reset input so the same file can be re-selected e.target.value = '' onUpdated() } const handleRemoveCover = async () => { await fetch(`/api/library-cover/${encodeURIComponent(library.id)}`, { method: 'DELETE' }) onUpdated() } return (
{/* Cover thumbnail */} {/* Info */}
{library.name} {TYPE_LABELS[library.type] ?? library.type}

{library.path}

{/* Actions */}
{library.coverExt && ( )} {confirming && ( )}
) } // โ”€โ”€โ”€ Add Library Form โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function AddLibraryForm({ onAdded }: { onAdded: () => void }) { const [name, setName] = useState('') const [libPath, setLibPath] = useState('') const [type, setType] = useState('games') const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError(null) setSubmitting(true) try { const res = await fetch('/api/libraries', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, path: libPath, type }), }) const data = await res.json() if (!res.ok) { setError(data.error ?? 'Something went wrong.') setSubmitting(false) return } // Success โ€” fire scan for the new library (fire-and-forget) void fetch(`/api/scan/${encodeURIComponent((data as { id: string }).id)}`, { method: 'POST' }) // Reset form setName('') setLibPath('') setType('games') setSubmitting(false) onAdded() } catch { setError('Network error. Please try again.') setSubmitting(false) } } return (
setName(e.target.value)} placeholder="e.g. Games" required className="w-full rounded-lg px-3 py-2 text-sm outline-none focus:ring-2" style={{ backgroundColor: 'var(--background)', border: '1px solid var(--border)', color: 'var(--text-primary)', }} onFocus={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)')} onBlur={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--border)')} />
setLibPath(e.target.value)} placeholder="e.g. /mnt/nas/Games or ./data/Games" required className="w-full rounded-lg px-3 py-2 text-sm font-mono outline-none focus:ring-2" style={{ backgroundColor: 'var(--background)', border: '1px solid var(--border)', color: 'var(--text-primary)', }} onFocus={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)')} onBlur={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--border)')} /> {error && (

{error}

)}
) } // โ”€โ”€โ”€ Field wrapper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function Field({ label, children }: { label: string; children: React.ReactNode }) { return (
{children}
) } // โ”€โ”€โ”€ Loading skeleton โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function LoadingRows() { return (
{[70, 50, 85].map((w) => (
))}
) }