import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'; import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, clearBadges, updateCategory, deleteCategory, partyStart, partyStop, subscribeEvents, getSelectedChannels, setSelectedChannel, } from './api'; import type { VoiceChannelInfo, Sound, Category } from './types'; import { getCookie, setCookie } from './cookies'; /* ── Category Color Palette ── */ const CAT_PALETTE = [ '#3b82f6', '#f59e0b', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316', '#06b6d4', '#ef4444', '#a855f7', '#84cc16', '#d946ef', '#0ea5e9', '#f43f5e', '#10b981', ]; const THEMES = [ { id: 'midnight', label: 'Midnight' }, { id: 'daylight', label: 'Daylight' }, { id: 'neon', label: 'Neon' }, { id: 'vapor', label: 'Vapor' }, { id: 'matrix', label: 'Matrix' }, ]; type Tab = 'all' | 'favorites' | 'recent'; export default function App() { /* ── State ── */ const [sounds, setSounds] = useState([]); const [total, setTotal] = useState(0); const [folders, setFolders] = useState>([]); const [categories, setCategories] = useState([]); const [activeTab, setActiveTab] = useState('all'); const [activeFolder, setActiveFolder] = useState(''); const [query, setQuery] = useState(''); const [channels, setChannels] = useState([]); const [selected, setSelected] = useState(''); const selectedRef = useRef(''); const [volume, setVolume] = useState(1); const [favs, setFavs] = useState>({}); const [theme, setTheme] = useState(() => localStorage.getItem('jb-theme') || 'midnight'); const [isAdmin, setIsAdmin] = useState(false); const [showAdmin, setShowAdmin] = useState(false); const [chaosMode, setChaosMode] = useState(false); const [partyActiveGuilds, setPartyActiveGuilds] = useState([]); const chaosModeRef = useRef(false); const [lastPlayed, setLastPlayed] = useState(''); const [notification, setNotification] = useState<{ msg: string; type: 'info' | 'error' } | null>(null); /* ── Refs ── */ useEffect(() => { chaosModeRef.current = chaosMode; }, [chaosMode]); useEffect(() => { selectedRef.current = selected; }, [selected]); /* ── Helpers ── */ const notify = useCallback((msg: string, type: 'info' | 'error' = 'info') => { setNotification({ msg, type }); setTimeout(() => setNotification(null), 3000); }, []); const guildId = selected ? selected.split(':')[0] : ''; const channelId = selected ? selected.split(':')[1] : ''; /* ── Init ── */ useEffect(() => { (async () => { try { const [ch, selMap] = await Promise.all([fetchChannels(), getSelectedChannels()]); setChannels(ch); if (ch.length) { const g = ch[0].guildId; const serverCid = selMap[g]; const match = serverCid && ch.find(x => x.guildId === g && x.channelId === serverCid); setSelected(match ? `${g}:${serverCid}` : `${ch[0].guildId}:${ch[0].channelId}`); } } catch (e: any) { notify(e?.message || 'Channel-Fehler', 'error'); } try { setIsAdmin(await adminStatus()); } catch { } try { const c = await fetchCategories(); setCategories(c.categories || []); } catch { } })(); }, []); /* ── Theme ── */ useEffect(() => { document.body.setAttribute('data-theme', theme); localStorage.setItem('jb-theme', theme); }, [theme]); /* ── SSE ── */ useEffect(() => { const unsub = subscribeEvents((msg) => { if (msg?.type === 'party') { setPartyActiveGuilds(prev => { const s = new Set(prev); if (msg.active) s.add(msg.guildId); else s.delete(msg.guildId); return Array.from(s); }); } else if (msg?.type === 'snapshot') { setPartyActiveGuilds(Array.isArray(msg.party) ? msg.party : []); try { const sel = msg?.selected || {}; const g = selectedRef.current?.split(':')[0]; if (g && sel[g]) setSelected(`${g}:${sel[g]}`); } catch { } try { const vols = msg?.volumes || {}; const g = selectedRef.current?.split(':')[0]; if (g && typeof vols[g] === 'number') setVolume(vols[g]); } catch { } } else if (msg?.type === 'channel') { const g = selectedRef.current?.split(':')[0]; if (msg.guildId === g) setSelected(`${msg.guildId}:${msg.channelId}`); } else if (msg?.type === 'volume') { const g = selectedRef.current?.split(':')[0]; if (msg.guildId === g && typeof msg.volume === 'number') setVolume(msg.volume); } }); return () => { try { unsub(); } catch { } }; }, []); useEffect(() => { setChaosMode(guildId ? partyActiveGuilds.includes(guildId) : false); }, [selected, partyActiveGuilds]); /* ── Data Fetch ── */ useEffect(() => { (async () => { try { let folderParam = '__all__'; if (activeTab === 'recent') folderParam = '__recent__'; else if (activeFolder) folderParam = activeFolder; const s = await fetchSounds(query, folderParam, undefined, false); setSounds(s.items); setTotal(s.total); setFolders(s.folders); } catch (e: any) { notify(e?.message || 'Sounds-Fehler', 'error'); } })(); }, [activeTab, activeFolder, query]); /* ── Favs persistence ── */ useEffect(() => { const c = getCookie('favs'); if (c) try { setFavs(JSON.parse(c)); } catch { } }, []); useEffect(() => { try { setCookie('favs', JSON.stringify(favs)); } catch { } }, [favs]); /* ── Volume sync ── */ useEffect(() => { if (selected) { (async () => { try { const v = await getVolume(guildId); setVolume(v); } catch { } })(); } }, [selected]); /* ── Actions ── */ async function handlePlay(s: Sound) { if (!selected) return notify('Bitte einen Voice-Channel auswählen', 'error'); try { await playSound(s.name, guildId, channelId, volume, s.relativePath); setLastPlayed(s.name); setTimeout(() => setLastPlayed(''), 4000); } catch (e: any) { notify(e?.message || 'Play fehlgeschlagen', 'error'); } } async function handleStop() { if (!selected) return; try { await fetch(`/api/stop?guildId=${encodeURIComponent(guildId)}`, { method: 'POST' }); } catch { } } async function handleRandom() { if (!sounds.length || !selected) return; const rnd = sounds[Math.floor(Math.random() * sounds.length)]; handlePlay(rnd); } async function toggleParty() { if (chaosMode) { await handleStop(); try { await partyStop(guildId); } catch { } } else { if (!selected) return notify('Bitte einen Channel auswählen', 'error'); try { await partyStart(guildId, channelId); } catch { } } } /* ── Computed ── */ const displaySounds = useMemo(() => { if (activeTab === 'favorites') { return sounds.filter(s => favs[s.relativePath ?? s.fileName]); } return sounds; }, [sounds, activeTab, favs]); const favCount = useMemo(() => Object.values(favs).filter(Boolean).length, [favs]); const visibleFolders = useMemo(() => folders.filter(f => !['__all__', '__recent__', '__top3__'].includes(f.key)), [folders]); const folderColorMap = useMemo(() => { const m: Record = {}; visibleFolders.forEach((f, i) => { m[f.key] = CAT_PALETTE[i % CAT_PALETTE.length]; }); return m; }, [visibleFolders]); /* ── Admin State ── */ const [adminPwd, setAdminPwd] = useState(''); async function handleAdminLogin() { try { const ok = await adminLogin(adminPwd); if (ok) { setIsAdmin(true); setAdminPwd(''); notify('Admin eingeloggt'); } else notify('Falsches Passwort', 'error'); } catch { notify('Login fehlgeschlagen', 'error'); } } async function handleAdminLogout() { try { await adminLogout(); setIsAdmin(false); notify('Ausgeloggt'); } catch { } } /* ── Render ── */ return (
{/* ════════ Header ════════ */}
JUKEBOX
search setQuery(e.target.value)} /> {query && ( )}
{total} Sounds
{/* ════════ Tab Bar ════════ */} {/* ════════ Category Filter ════════ */} {activeTab === 'all' && visibleFolders.length > 0 && (
{visibleFolders.map(f => { const color = folderColorMap[f.key] || '#888'; const isActive = activeFolder === f.key; return ( ); })}
)} {/* ════════ Sound Grid ════════ */}
{displaySounds.length === 0 ? (
{activeTab === 'favorites' ? 'star_border' : 'music_off'}

{activeTab === 'favorites' ? 'Noch keine Favorites — klick den Stern!' : query ? `Kein Sound für "${query}" gefunden` : 'Keine Sounds vorhanden'}

) : (
{displaySounds.map((s, idx) => { const key = s.relativePath ?? s.fileName; const isFav = !!favs[key]; const color = s.folder ? folderColorMap[s.folder] || '#555' : '#555'; const isNew = s.badges?.includes('new'); const isTop = s.badges?.includes('top'); const isPlaying = lastPlayed === s.name; return ( ); })}
)}
{/* ════════ Control Bar ════════ */}
headset_mic
{lastPlayed && (
play_arrow {lastPlayed}
)}
{ const newVol = volume > 0 ? 0 : 0.5; setVolume(newVol); if (guildId) setVolumeLive(guildId, newVol).catch(() => {}); }} > {volume === 0 ? 'volume_off' : volume < 0.5 ? 'volume_down' : 'volume_up'} { const v = parseFloat(e.target.value); setVolume(v); if (guildId) try { await setVolumeLive(guildId, v); } catch { } }} style={{ '--fill': `${Math.round(volume * 100)}%` } as React.CSSProperties} /> {Math.round(volume * 100)}%
{/* ════════ Notification Toast ════════ */} {notification && (
{notification.type === 'error' ? 'error_outline' : 'check_circle'} {notification.msg}
)} {/* ════════ Admin Panel Overlay ════════ */} {showAdmin && (
{ if (e.target === e.currentTarget) setShowAdmin(false); }}>

Admin

{!isAdmin ? (
setAdminPwd(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdminLogin()} placeholder="Admin-Passwort..." />
) : (

Eingeloggt als Admin

)}
)}
); }