2025-08-08 13:14:27 +02:00
|
|
|
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
2025-08-08 14:23:18 +02:00
|
|
|
|
import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename } from './api';
|
2025-08-07 23:24:56 +02:00
|
|
|
|
import type { VoiceChannelInfo, Sound } from './types';
|
2025-08-08 03:21:01 +02:00
|
|
|
|
import { getCookie, setCookie } from './cookies';
|
2025-08-07 23:24:56 +02:00
|
|
|
|
|
|
|
|
|
|
export default function App() {
|
|
|
|
|
|
const [sounds, setSounds] = useState<Sound[]>([]);
|
2025-08-08 01:40:49 +02:00
|
|
|
|
const [total, setTotal] = useState<number>(0);
|
2025-08-08 01:56:30 +02:00
|
|
|
|
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);
|
2025-08-08 01:23:52 +02:00
|
|
|
|
const [volume, setVolume] = useState<number>(1);
|
2025-08-08 03:21:01 +02:00
|
|
|
|
const [favs, setFavs] = useState<Record<string, boolean>>({});
|
2025-08-08 13:17:29 +02:00
|
|
|
|
const [theme, setTheme] = useState<string>(() => localStorage.getItem('theme') || 'dark');
|
2025-08-08 14:23:18 +02:00
|
|
|
|
const [isAdmin, setIsAdmin] = useState<boolean>(false);
|
|
|
|
|
|
const [adminPwd, setAdminPwd] = useState<string>('');
|
|
|
|
|
|
const [selectedSet, setSelectedSet] = useState<Record<string, boolean>>({});
|
|
|
|
|
|
const selectedCount = useMemo(() => Object.values(selectedSet).filter(Boolean).length, [selectedSet]);
|
2025-08-08 14:41:05 +02:00
|
|
|
|
const [clock, setClock] = useState<string>(() => new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Europe/Berlin' }).format(new Date()));
|
2025-08-07 23:24:56 +02:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
try {
|
2025-08-08 03:17:38 +02:00
|
|
|
|
const c = await fetchChannels();
|
2025-08-07 23:24:56 +02:00
|
|
|
|
setChannels(c);
|
2025-08-08 02:37:53 +02:00
|
|
|
|
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) {
|
2025-08-08 03:17:38 +02:00
|
|
|
|
setError(e?.message || 'Fehler beim Laden der Channels');
|
2025-08-07 23:24:56 +02:00
|
|
|
|
}
|
2025-08-08 14:23:18 +02:00
|
|
|
|
try { setIsAdmin(await adminStatus()); } catch {}
|
2025-08-07 23:24:56 +02:00
|
|
|
|
})();
|
2025-08-08 03:17:38 +02:00
|
|
|
|
}, []);
|
2025-08-07 23:24:56 +02:00
|
|
|
|
|
2025-08-08 14:41:05 +02:00
|
|
|
|
// 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);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-08-08 03:17:38 +02:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
(async () => {
|
2025-08-07 23:24:56 +02:00
|
|
|
|
try {
|
2025-08-08 03:37:54 +02:00
|
|
|
|
const folderParam = activeFolder === '__favs__' ? '__all__' : activeFolder;
|
|
|
|
|
|
const s = await fetchSounds(query, folderParam);
|
2025-08-08 01:40:49 +02:00
|
|
|
|
setSounds(s.items);
|
|
|
|
|
|
setTotal(s.total);
|
2025-08-08 01:56:30 +02:00
|
|
|
|
setFolders(s.folders);
|
2025-08-08 03:17:38 +02:00
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
setError(e?.message || 'Fehler beim Laden der Sounds');
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
}, [activeFolder, query]);
|
2025-08-07 23:24:56 +02:00
|
|
|
|
|
2025-08-08 03:21:01 +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]);
|
|
|
|
|
|
|
2025-08-08 13:17:29 +02:00
|
|
|
|
// Theme anwenden/persistieren
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
document.body.setAttribute('data-theme', theme);
|
|
|
|
|
|
localStorage.setItem('theme', theme);
|
|
|
|
|
|
}, [theme]);
|
|
|
|
|
|
|
2025-08-08 02:37:53 +02:00
|
|
|
|
useEffect(() => {
|
2025-08-08 13:46:27 +02:00
|
|
|
|
(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 {}
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
2025-08-08 02:37:53 +02:00
|
|
|
|
}, [selected]);
|
|
|
|
|
|
|
2025-08-07 23:24:56 +02:00
|
|
|
|
const filtered = useMemo(() => {
|
|
|
|
|
|
const q = query.trim().toLowerCase();
|
2025-08-08 03:07:35 +02:00
|
|
|
|
if (!q) return sounds;
|
|
|
|
|
|
return sounds.filter((s) => s.name.toLowerCase().includes(q));
|
|
|
|
|
|
}, [sounds, query]);
|
2025-08-07 23:24:56 +02:00
|
|
|
|
|
2025-08-08 03:37:54 +02:00
|
|
|
|
const favCount = useMemo(() => Object.values(favs).filter(Boolean).length, [favs]);
|
|
|
|
|
|
|
2025-08-08 01:56:30 +02:00
|
|
|
|
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);
|
2025-08-08 01:56:30 +02:00
|
|
|
|
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 (
|
2025-08-08 14:55:12 +02:00
|
|
|
|
<ErrorBoundary>
|
2025-08-07 23:24:56 +02:00
|
|
|
|
<div className="container">
|
|
|
|
|
|
<header>
|
2025-08-08 14:47:18 +02:00
|
|
|
|
<div className="header-row">
|
|
|
|
|
|
<h1>Einmal mit Soundboard -Profis</h1>
|
|
|
|
|
|
<div className="clock">{clock}</div>
|
|
|
|
|
|
</div>
|
2025-08-08 02:44:08 +02:00
|
|
|
|
<div className="badge">Geladene Sounds: {total}</div>
|
2025-08-08 14:23:18 +02:00
|
|
|
|
{isAdmin && (
|
|
|
|
|
|
<div className="badge">Admin-Modus</div>
|
|
|
|
|
|
)}
|
2025-08-07 23:24:56 +02:00
|
|
|
|
</header>
|
|
|
|
|
|
|
2025-08-08 10:40:13 +02:00
|
|
|
|
<section className="controls glass">
|
2025-08-08 01:23:52 +02:00
|
|
|
|
<div className="control search">
|
|
|
|
|
|
<input
|
|
|
|
|
|
value={query}
|
|
|
|
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
|
|
|
|
placeholder="Nach Sounds suchen..."
|
|
|
|
|
|
aria-label="Suche"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-08-08 13:14:27 +02:00
|
|
|
|
<CustomSelect
|
|
|
|
|
|
channels={channels}
|
|
|
|
|
|
value={selected}
|
|
|
|
|
|
onChange={setSelected}
|
|
|
|
|
|
/>
|
2025-08-08 01:23:52 +02:00
|
|
|
|
<div className="control volume">
|
|
|
|
|
|
<label>🔊 {Math.round(volume * 100)}%</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="range"
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
max={1}
|
|
|
|
|
|
step={0.01}
|
|
|
|
|
|
value={volume}
|
2025-08-08 01:51:36 +02:00
|
|
|
|
onChange={async (e) => {
|
|
|
|
|
|
const v = parseFloat(e.target.value);
|
|
|
|
|
|
setVolume(v);
|
|
|
|
|
|
if (selected) {
|
|
|
|
|
|
const [guildId] = selected.split(':');
|
|
|
|
|
|
try { await setVolumeLive(guildId, v); } catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
2025-08-08 01:23:52 +02:00
|
|
|
|
aria-label="Lautstärke"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-08-08 13:17:29 +02:00
|
|
|
|
<div className="control theme">
|
|
|
|
|
|
<select value={theme} onChange={(e) => setTheme(e.target.value)} aria-label="Theme">
|
|
|
|
|
|
<option value="dark">Dark</option>
|
|
|
|
|
|
<option value="light">Light</option>
|
|
|
|
|
|
<option value="rainbow">Rainbow Chaos</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
2025-08-08 14:23:18 +02:00
|
|
|
|
{!isAdmin && (
|
|
|
|
|
|
<div className="control" style={{ display: 'flex', gap: 8 }}>
|
|
|
|
|
|
<input type="password" value={adminPwd} onChange={(e) => setAdminPwd(e.target.value)} placeholder="Admin Passwort" />
|
|
|
|
|
|
<button type="button" className="tab" onClick={async () => {
|
|
|
|
|
|
const ok = await adminLogin(adminPwd);
|
|
|
|
|
|
if (ok) { setIsAdmin(true); setAdminPwd(''); }
|
|
|
|
|
|
else alert('Login fehlgeschlagen');
|
|
|
|
|
|
}}>Login</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-08-07 23:24:56 +02:00
|
|
|
|
</section>
|
|
|
|
|
|
|
2025-08-08 14:23:18 +02:00
|
|
|
|
{/* Admin Toolbar */}
|
|
|
|
|
|
{isAdmin && (
|
|
|
|
|
|
<section className="controls glass" style={{ marginTop: -8 }}>
|
|
|
|
|
|
<div className="control" style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
|
|
|
|
<button type="button" className="tab" onClick={async () => {
|
|
|
|
|
|
const toDelete = Object.entries(selectedSet).filter(([, v]) => v).map(([k]) => k);
|
|
|
|
|
|
if (toDelete.length === 0) return;
|
|
|
|
|
|
if (!confirm(`Wirklich ${toDelete.length} Datei(en) löschen?`)) return;
|
|
|
|
|
|
try { await adminDelete(toDelete); } catch (e: any) { alert(e?.message || 'Löschen fehlgeschlagen'); }
|
|
|
|
|
|
// refresh
|
|
|
|
|
|
const folderParam = activeFolder === '__favs__' ? '__all__' : activeFolder;
|
|
|
|
|
|
const s = await fetchSounds(query, folderParam);
|
|
|
|
|
|
setSounds(s.items);
|
|
|
|
|
|
setTotal(s.total);
|
|
|
|
|
|
setFolders(s.folders);
|
|
|
|
|
|
setSelectedSet({});
|
|
|
|
|
|
}}>🗑️ Löschen</button>
|
|
|
|
|
|
{selectedCount === 1 && (
|
|
|
|
|
|
<RenameInline onSubmit={async (newName) => {
|
|
|
|
|
|
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({});
|
|
|
|
|
|
}} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
<button type="button" className="tab" onClick={async () => { await adminLogout(); setIsAdmin(false); }}>Logout</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-08-08 01:56:30 +02:00
|
|
|
|
{folders.length > 0 && (
|
2025-08-08 10:40:13 +02:00
|
|
|
|
<nav className="tabs glass">
|
2025-08-08 03:31:28 +02:00
|
|
|
|
{/* Favoriten Tab */}
|
|
|
|
|
|
<button
|
|
|
|
|
|
key="__favs__"
|
|
|
|
|
|
className={`tab ${activeFolder === '__favs__' ? 'active' : ''}`}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setActiveFolder('__favs__')}
|
|
|
|
|
|
>
|
2025-08-08 03:37:54 +02:00
|
|
|
|
Favoriten ({favCount})
|
2025-08-08 03:31:28 +02:00
|
|
|
|
</button>
|
2025-08-08 14:05:44 +02:00
|
|
|
|
{/* Neueste 10 */}
|
|
|
|
|
|
<button
|
|
|
|
|
|
key="__recent__"
|
|
|
|
|
|
className={`tab ${activeFolder === '__recent__' ? 'active' : ''}`}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
|
setActiveFolder('__recent__');
|
|
|
|
|
|
const resp = await fetchSounds(undefined, '__recent__');
|
|
|
|
|
|
setSounds(resp.items);
|
|
|
|
|
|
setTotal(resp.total);
|
|
|
|
|
|
setFolders(resp.folders);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Neu
|
|
|
|
|
|
</button>
|
2025-08-08 01:56:30 +02:00
|
|
|
|
{folders.map((f) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={f.key}
|
|
|
|
|
|
className={`tab ${activeFolder === f.key ? 'active' : ''}`}
|
2025-08-08 02:37:53 +02:00
|
|
|
|
type="button"
|
2025-08-08 01:56:30 +02:00
|
|
|
|
onClick={async () => {
|
|
|
|
|
|
setActiveFolder(f.key);
|
2025-08-08 03:07:35 +02:00
|
|
|
|
const resp = await fetchSounds(undefined, f.key);
|
2025-08-08 01:56:30 +02:00
|
|
|
|
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">
|
2025-08-08 03:31:28 +02:00
|
|
|
|
{(activeFolder === '__favs__' ? filtered.filter((s) => !!favs[s.relativePath ?? s.fileName]) : filtered).map((s) => {
|
2025-08-08 03:21:01 +02:00
|
|
|
|
const key = `${s.relativePath ?? s.fileName}`;
|
|
|
|
|
|
const isFav = !!favs[key];
|
|
|
|
|
|
return (
|
2025-08-08 14:46:07 +02:00
|
|
|
|
<div key={`${s.fileName}-${s.name}`} className="sound-wrap row">
|
2025-08-08 14:23:18 +02:00
|
|
|
|
{isAdmin && (
|
|
|
|
|
|
<input
|
2025-08-08 14:46:07 +02:00
|
|
|
|
className="row-check"
|
2025-08-08 14:23:18 +02:00
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={!!selectedSet[key]}
|
2025-08-08 14:55:12 +02:00
|
|
|
|
onClick={(e) => { try { e.stopPropagation(); } catch {} }}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setSelectedSet((prev) => ({ ...prev, [key]: e.target.checked }));
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Checkbox change error:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
2025-08-08 14:23:18 +02:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-08-08 14:46:07 +02:00
|
|
|
|
<button className="sound" type="button" onClick={(e) => { e.stopPropagation(); handlePlay(s.name, s.relativePath); }} disabled={loading}>
|
2025-08-08 14:05:44 +02:00
|
|
|
|
{s.isRecent ? '🆕 ' : ''}{s.name}
|
2025-08-08 03:21:01 +02:00
|
|
|
|
</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>
|
2025-08-08 02:44:08 +02:00
|
|
|
|
{/* footer counter entfällt, da oben sichtbar */}
|
2025-08-07 23:24:56 +02:00
|
|
|
|
</div>
|
2025-08-08 14:55:12 +02:00
|
|
|
|
</ErrorBoundary>
|
2025-08-07 23:24:56 +02:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 13:14:27 +02:00
|
|
|
|
type SelectProps = {
|
|
|
|
|
|
channels: VoiceChannelInfo[];
|
|
|
|
|
|
value: string;
|
|
|
|
|
|
onChange: (v: string) => void;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function CustomSelect({ channels, value, onChange }: SelectProps) {
|
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
|
const ref = useRef<HTMLDivElement | null>(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 (
|
|
|
|
|
|
<div className="control select custom-select" ref={ref}>
|
|
|
|
|
|
<button type="button" className="select-trigger" onClick={() => setOpen(v => !v)}>
|
|
|
|
|
|
{current ? `${current.guildName} – ${current.channelName}` : 'Channel wählen'}
|
|
|
|
|
|
<span className="chev">▾</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{open && (
|
|
|
|
|
|
<div className="select-menu">
|
|
|
|
|
|
{channels.map((c) => {
|
|
|
|
|
|
const v = `${c.guildId}:${c.channelId}`;
|
|
|
|
|
|
const active = v === value;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
key={v}
|
|
|
|
|
|
className={`select-item ${active ? 'active' : ''}`}
|
|
|
|
|
|
onClick={() => { onChange(v); setOpen(false); }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{c.guildName} – {c.channelName}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2025-08-08 01:56:30 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 14:55:12 +02:00
|
|
|
|
// 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 (
|
|
|
|
|
|
<div style={{ padding: 20 }}>
|
|
|
|
|
|
<h2>Es ist ein Fehler aufgetreten</h2>
|
|
|
|
|
|
<pre style={{ whiteSpace: 'pre-wrap' }}>{String(this.state.error.message || this.state.error)}</pre>
|
|
|
|
|
|
<button type="button" onClick={() => this.setState({ error: undefined })}>Zurück</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.props.children as any;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 15:01:53 +02:00
|
|
|
|
// Inline-Komponente für Umbenennen (nur bei genau 1 Selektion sichtbar)
|
|
|
|
|
|
type RenameInlineProps = { onSubmit: (newName: string) => void | Promise<void> };
|
|
|
|
|
|
function RenameInline({ onSubmit }: RenameInlineProps) {
|
|
|
|
|
|
const [val, setVal] = useState('');
|
|
|
|
|
|
async function submit() {
|
|
|
|
|
|
const n = val.trim();
|
|
|
|
|
|
if (!n) return;
|
|
|
|
|
|
await onSubmit(n);
|
|
|
|
|
|
setVal('');
|
|
|
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
|
|
|
|
<input
|
|
|
|
|
|
value={val}
|
|
|
|
|
|
onChange={(e) => setVal(e.target.value)}
|
|
|
|
|
|
placeholder="Neuer Name"
|
|
|
|
|
|
onKeyDown={(e) => { if (e.key === 'Enter') void submit(); }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button type="button" className="tab" onClick={() => void submit()}>Umbenennen</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-07 23:24:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|