import React, { useEffect, useMemo, useRef, useState } from 'react'; import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename } from './api'; import type { VoiceChannelInfo, Sound } from './types'; import { getCookie, setCookie } from './cookies'; export default function App() { const [sounds, setSounds] = useState([]); const [total, setTotal] = useState(0); const [folders, setFolders] = useState>([]); const [activeFolder, setActiveFolder] = useState('__all__'); const [channels, setChannels] = useState([]); const [query, setQuery] = useState(''); const [selected, setSelected] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [volume, setVolume] = useState(1); const [favs, setFavs] = useState>({}); const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'dark'); const [isAdmin, setIsAdmin] = useState(false); const [adminPwd, setAdminPwd] = useState(''); const [selectedSet, setSelectedSet] = useState>({}); const selectedCount = useMemo(() => Object.values(selectedSet).filter(Boolean).length, [selectedSet]); const [clock, setClock] = useState(() => new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Europe/Berlin' }).format(new Date())); useEffect(() => { (async () => { try { const c = await fetchChannels(); setChannels(c); const stored = localStorage.getItem('selectedChannel'); if (stored && c.find(x => `${x.guildId}:${x.channelId}` === stored)) { setSelected(stored); } else if (c[0]) { setSelected(`${c[0].guildId}:${c[0].channelId}`); } } catch (e: any) { setError(e?.message || 'Fehler beim Laden der Channels'); } try { setIsAdmin(await adminStatus()); } catch {} })(); }, []); // Uhrzeit (Berlin) aktualisieren useEffect(() => { const fmt = new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Europe/Berlin' }); const update = () => setClock(fmt.format(new Date())); const id = setInterval(update, 1000); update(); return () => clearInterval(id); }, []); useEffect(() => { (async () => { try { const folderParam = activeFolder === '__favs__' ? '__all__' : activeFolder; const s = await fetchSounds(query, folderParam); setSounds(s.items); setTotal(s.total); setFolders(s.folders); } catch (e: any) { setError(e?.message || 'Fehler beim Laden der Sounds'); } })(); }, [activeFolder, query]); // Favoriten aus Cookie laden useEffect(() => { const c = getCookie('favs'); if (c) { try { setFavs(JSON.parse(c)); } catch {} } }, []); // Favoriten persistieren useEffect(() => { try { setCookie('favs', JSON.stringify(favs)); } catch {} }, [favs]); // Theme anwenden/persistieren useEffect(() => { document.body.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); }, [theme]); useEffect(() => { (async () => { if (selected) { localStorage.setItem('selectedChannel', selected); // gespeicherte Lautstärke vom Server laden try { const [guildId] = selected.split(':'); const v = await getVolume(guildId); setVolume(v); } catch {} } })(); }, [selected]); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return sounds; return sounds.filter((s) => s.name.toLowerCase().includes(q)); }, [sounds, query]); const favCount = useMemo(() => Object.values(favs).filter(Boolean).length, [favs]); async function handlePlay(name: string, rel?: string) { setError(null); if (!selected) return setError('Bitte einen Voice-Channel auswählen'); const [guildId, channelId] = selected.split(':'); try { setLoading(true); await playSound(name, guildId, channelId, volume, rel); } catch (e: any) { setError(e?.message || 'Play fehlgeschlagen'); } finally { setLoading(false); } } return (

Einmal mit Soundboard -Profis

{clock}
Geladene Sounds: {total}
{isAdmin && (
Admin-Modus
)}
setQuery(e.target.value)} placeholder="Nach Sounds suchen..." aria-label="Suche" />
{ const v = parseFloat(e.target.value); setVolume(v); if (selected) { const [guildId] = selected.split(':'); try { await setVolumeLive(guildId, v); } catch {} } }} aria-label="Lautstärke" />
{!isAdmin && (
setAdminPwd(e.target.value)} placeholder="Admin Passwort" />
)}
{/* Admin Toolbar */} {isAdmin && (
{selectedCount === 1 && ( { const from = Object.keys(selectedSet).find((k) => selectedSet[k]); if (!from) return; try { await adminRename(from, newName); } catch (e: any) { alert(e?.message || 'Umbenennen fehlgeschlagen'); return; } const folderParam = activeFolder === '__favs__' ? '__all__' : activeFolder; const s = await fetchSounds(query, folderParam); setSounds(s.items); setTotal(s.total); setFolders(s.folders); setSelectedSet({}); }} /> )}
)} {folders.length > 0 && ( )} {error &&
{error}
}
{(activeFolder === '__favs__' ? filtered.filter((s) => !!favs[s.relativePath ?? s.fileName]) : filtered).map((s) => { const key = `${s.relativePath ?? s.fileName}`; const isFav = !!favs[key]; return (
{isAdmin && ( { try { e.stopPropagation(); } catch {} }} onChange={(e) => { try { setSelectedSet((prev) => ({ ...prev, [key]: e.target.checked })); } catch (err) { console.error('Checkbox change error:', err); } }} /> )}
); })} {filtered.length === 0 &&
Keine Sounds gefunden.
}
{/* footer counter entfällt, da oben sichtbar */}
); } type SelectProps = { channels: VoiceChannelInfo[]; value: string; onChange: (v: string) => void; }; function CustomSelect({ channels, value, onChange }: SelectProps) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { const close = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; window.addEventListener('click', close); return () => window.removeEventListener('click', close); }, []); const current = channels.find(c => `${c.guildId}:${c.channelId}` === value); return (
{open && (
{channels.map((c) => { const v = `${c.guildId}:${c.channelId}`; const active = v === value; return ( ); })}
)}
); } // Einfache ErrorBoundary, damit die Seite nicht blank wird und Fehler sichtbar sind class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { error?: Error }>{ constructor(props: { children: React.ReactNode }) { super(props); this.state = { error: undefined }; } static getDerivedStateFromError(error: Error) { return { error }; } componentDidCatch(error: Error, info: any) { console.error('UI-ErrorBoundary:', error, info); } render() { if (this.state.error) { return (

Es ist ein Fehler aufgetreten

{String(this.state.error.message || this.state.error)}
); } return this.props.children as any; } } // Inline-Komponente für Umbenennen (nur bei genau 1 Selektion sichtbar) type RenameInlineProps = { onSubmit: (newName: string) => void | Promise }; function RenameInline({ onSubmit }: RenameInlineProps) { const [val, setVal] = useState(''); async function submit() { const n = val.trim(); if (!n) return; await onSubmit(n); setVal(''); } return (
setVal(e.target.value)} placeholder="Neuer Name" onKeyDown={(e) => { if (e.key === 'Enter') void submit(); }} />
); }