jukebox-vibe/web/src/App.tsx

203 lines
6.7 KiB
TypeScript
Raw Normal View History

2025-08-07 23:24:56 +02:00
import React, { useEffect, useMemo, useState } from 'react';
import { fetchChannels, fetchSounds, playSound, setVolumeLive } from './api';
2025-08-07 23:24:56 +02:00
import type { VoiceChannelInfo, Sound } from './types';
import { getCookie, setCookie } from './cookies';
2025-08-07 23:24:56 +02:00
export default function App() {
const [sounds, setSounds] = useState<Sound[]>([]);
const [total, setTotal] = useState<number>(0);
const [folders, setFolders] = useState<Array<{ key: string; name: string; count: number }>>([]);
const [activeFolder, setActiveFolder] = useState<string>('__all__');
2025-08-07 23:24:56 +02:00
const [channels, setChannels] = useState<VoiceChannelInfo[]>([]);
const [query, setQuery] = useState('');
const [selected, setSelected] = useState<string>('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [volume, setVolume] = useState<number>(1);
const [favs, setFavs] = useState<Record<string, boolean>>({});
2025-08-07 23:24:56 +02:00
useEffect(() => {
(async () => {
try {
const c = await fetchChannels();
2025-08-07 23:24:56 +02:00
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}`);
}
2025-08-07 23:24:56 +02:00
} catch (e: any) {
setError(e?.message || 'Fehler beim Laden der Channels');
2025-08-07 23:24:56 +02:00
}
})();
}, []);
2025-08-07 23:24:56 +02:00
useEffect(() => {
(async () => {
2025-08-07 23:24:56 +02:00
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]);
2025-08-07 23:24:56 +02:00
// 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]);
useEffect(() => {
if (selected) localStorage.setItem('selectedChannel', selected);
}, [selected]);
2025-08-07 23:24:56 +02:00
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return sounds;
return sounds.filter((s) => s.name.toLowerCase().includes(q));
}, [sounds, query]);
2025-08-07 23:24:56 +02:00
const favCount = useMemo(() => Object.values(favs).filter(Boolean).length, [favs]);
async function handlePlay(name: string, rel?: string) {
2025-08-07 23:24:56 +02:00
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);
2025-08-07 23:24:56 +02:00
} catch (e: any) {
setError(e?.message || 'Play fehlgeschlagen');
} finally {
setLoading(false);
}
}
return (
<div className="container">
<header>
<h1>Discord Soundboard</h1>
<p>Schicke dem Bot per privater Nachricht eine .mp3 neue Sounds erscheinen automatisch.</p>
<div className="badge">Geladene Sounds: {total}</div>
2025-08-07 23:24:56 +02:00
</header>
<section className="controls glass">
<div className="control search">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Nach Sounds suchen..."
aria-label="Suche"
/>
</div>
<div className="control select">
<select value={selected} onChange={(e) => setSelected(e.target.value)} aria-label="Voice-Channel">
{channels.map((c) => (
<option key={`${c.guildId}:${c.channelId}`} value={`${c.guildId}:${c.channelId}`}>
{c.guildName} {c.channelName}
</option>
))}
</select>
</div>
<div className="control volume">
<label>🔊 {Math.round(volume * 100)}%</label>
<input
type="range"
min={0}
max={1}
step={0.01}
value={volume}
onChange={async (e) => {
const v = parseFloat(e.target.value);
setVolume(v);
if (selected) {
const [guildId] = selected.split(':');
try { await setVolumeLive(guildId, v); } catch {}
}
}}
aria-label="Lautstärke"
/>
</div>
2025-08-07 23:24:56 +02:00
</section>
{folders.length > 0 && (
<nav className="tabs glass">
{/* Favoriten Tab */}
<button
key="__favs__"
className={`tab ${activeFolder === '__favs__' ? 'active' : ''}`}
type="button"
onClick={() => setActiveFolder('__favs__')}
>
Favoriten ({favCount})
</button>
{folders.map((f) => (
<button
key={f.key}
className={`tab ${activeFolder === f.key ? 'active' : ''}`}
type="button"
onClick={async () => {
setActiveFolder(f.key);
const resp = await fetchSounds(undefined, f.key);
setSounds(resp.items);
setTotal(resp.total);
setFolders(resp.folders);
}}
>
{f.name} ({f.count})
</button>
))}
</nav>
)}
2025-08-07 23:24:56 +02:00
{error && <div className="error">{error}</div>}
<section className="grid">
{(activeFolder === '__favs__' ? filtered.filter((s) => !!favs[s.relativePath ?? s.fileName]) : filtered).map((s) => {
const key = `${s.relativePath ?? s.fileName}`;
const isFav = !!favs[key];
return (
<div key={`${s.fileName}-${s.name}`} className="sound-wrap">
<button className="sound" type="button" onClick={() => handlePlay(s.name, s.relativePath)} disabled={loading}>
{s.name}
</button>
<button
className={`fav ${isFav ? 'active' : ''}`}
aria-label={isFav ? 'Favorit entfernen' : 'Als Favorit speichern'}
title={isFav ? 'Favorit entfernen' : 'Als Favorit speichern'}
onClick={() => setFavs((prev) => ({ ...prev, [key]: !prev[key] }))}
>
</button>
</div>
);
})}
2025-08-07 23:24:56 +02:00
{filtered.length === 0 && <div className="hint">Keine Sounds gefunden.</div>}
</section>
{/* footer counter entfällt, da oben sichtbar */}
2025-08-07 23:24:56 +02:00
</div>
);
}
function handlePlayWithPathFactory(play: (name: string, rel?: string) => Promise<void>) {
return (s: Sound & { relativePath?: string }) => play(s.name, s.relativePath);
}
2025-08-07 23:24:56 +02:00