gaming-hub/web/src/UserSettings.tsx

255 lines
9.2 KiB
TypeScript
Raw Normal View History

import { useState, useEffect, useCallback } from 'react';
interface UserInfo {
discordId: string;
username: string;
avatar: string | null;
globalName: string | null;
}
interface SoundOption {
name: string;
fileName: string;
folder: string;
relativePath: string;
}
interface UserSettingsProps {
user: UserInfo;
onClose: () => void;
onLogout: () => void;
}
export default function UserSettings({ user, onClose, onLogout }: UserSettingsProps) {
const [entranceSound, setEntranceSound] = useState<string | null>(null);
const [exitSound, setExitSound] = useState<string | null>(null);
const [availableSounds, setAvailableSounds] = useState<SoundOption[]>([]);
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState<'entrance' | 'exit' | null>(null);
const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
const [activeSection, setActiveSection] = useState<'entrance' | 'exit'>('entrance');
// Fetch current sounds + available sounds
useEffect(() => {
Promise.all([
fetch('/api/soundboard/user/sounds', { credentials: 'include' }).then(r => r.json()),
fetch('/api/soundboard/user/available-sounds').then(r => r.json()),
])
.then(([userSounds, sounds]) => {
setEntranceSound(userSounds.entrance ?? null);
setExitSound(userSounds.exit ?? null);
setAvailableSounds(sounds);
setLoading(false);
})
.catch(() => {
setMessage({ text: 'Fehler beim Laden der Einstellungen', type: 'error' });
setLoading(false);
});
}, []);
// Close on Escape
useEffect(() => {
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [onClose]);
const showMessage = useCallback((text: string, type: 'success' | 'error') => {
setMessage({ text, type });
setTimeout(() => setMessage(null), 3000);
}, []);
async function setSound(type: 'entrance' | 'exit', fileName: string) {
setSaving(type);
try {
const resp = await fetch(`/api/soundboard/user/${type}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileName }),
credentials: 'include',
});
if (resp.ok) {
const data = await resp.json();
if (type === 'entrance') setEntranceSound(data.entrance);
else setExitSound(data.exit);
showMessage(`${type === 'entrance' ? 'Entrance' : 'Exit'}-Sound gesetzt!`, 'success');
} else {
const err = await resp.json().catch(() => ({ error: 'Unbekannter Fehler' }));
showMessage(err.error || 'Fehler', 'error');
}
} catch {
showMessage('Verbindungsfehler', 'error');
}
setSaving(null);
}
async function removeSound(type: 'entrance' | 'exit') {
setSaving(type);
try {
const resp = await fetch(`/api/soundboard/user/${type}`, {
method: 'DELETE',
credentials: 'include',
});
if (resp.ok) {
if (type === 'entrance') setEntranceSound(null);
else setExitSound(null);
showMessage(`${type === 'entrance' ? 'Entrance' : 'Exit'}-Sound entfernt`, 'success');
}
} catch {
showMessage('Verbindungsfehler', 'error');
}
setSaving(null);
}
// Group sounds by folder
const folders = new Map<string, SoundOption[]>();
const q = search.toLowerCase();
for (const s of availableSounds) {
if (q && !s.name.toLowerCase().includes(q) && !s.fileName.toLowerCase().includes(q)) continue;
const key = s.folder || 'Allgemein';
if (!folders.has(key)) folders.set(key, []);
folders.get(key)!.push(s);
}
// Sort folders alphabetically, "Allgemein" first
const sortedFolders = [...folders.entries()].sort(([a], [b]) => {
if (a === 'Allgemein') return -1;
if (b === 'Allgemein') return 1;
return a.localeCompare(b);
});
const currentSound = activeSection === 'entrance' ? entranceSound : exitSound;
return (
<div className="hub-usettings-overlay" onClick={onClose}>
<div className="hub-usettings-panel" onClick={e => e.stopPropagation()}>
{/* Header */}
<div className="hub-usettings-header">
<div className="hub-usettings-user">
{user.avatar ? (
<img src={user.avatar} alt="" className="hub-usettings-avatar" />
) : (
<div className="hub-usettings-avatar-placeholder">{user.username[0]?.toUpperCase()}</div>
)}
<div className="hub-usettings-user-info">
<span className="hub-usettings-username">{user.globalName || user.username}</span>
<span className="hub-usettings-discriminator">@{user.username}</span>
</div>
</div>
<div className="hub-usettings-header-actions">
<button className="hub-usettings-logout" onClick={onLogout} title="Abmelden">
{'\uD83D\uDEAA'}
</button>
<button className="hub-usettings-close" onClick={onClose}>
{'\u2715'}
</button>
</div>
</div>
{/* Message toast */}
{message && (
<div className={`hub-usettings-toast ${message.type}`}>
{message.type === 'success' ? '\u2705' : '\u274C'} {message.text}
</div>
)}
{loading ? (
<div className="hub-usettings-loading">
<span className="hub-update-spinner" /> Lade Einstellungen...
</div>
) : (
<div className="hub-usettings-content">
{/* Section tabs */}
<div className="hub-usettings-tabs">
<button
className={`hub-usettings-tab ${activeSection === 'entrance' ? 'active' : ''}`}
onClick={() => setActiveSection('entrance')}
>
{'\uD83D\uDC4B'} Entrance-Sound
</button>
<button
className={`hub-usettings-tab ${activeSection === 'exit' ? 'active' : ''}`}
onClick={() => setActiveSection('exit')}
>
{'\uD83D\uDC4E'} Exit-Sound
</button>
</div>
{/* Current sound display */}
<div className="hub-usettings-current">
<span className="hub-usettings-current-label">
Aktuell: {' '}
</span>
{currentSound ? (
<span className="hub-usettings-current-value">
{'\uD83C\uDFB5'} {currentSound}
<button
className="hub-usettings-remove-btn"
onClick={() => removeSound(activeSection)}
disabled={saving === activeSection}
title="Entfernen"
>
{'\u2715'}
</button>
</span>
) : (
<span className="hub-usettings-current-none">Kein Sound gesetzt</span>
)}
</div>
{/* Search */}
<div className="hub-usettings-search-wrap">
<input
type="text"
className="hub-usettings-search"
placeholder="Sounds durchsuchen..."
value={search}
onChange={e => setSearch(e.target.value)}
/>
{search && (
<button className="hub-usettings-search-clear" onClick={() => setSearch('')}>
{'\u2715'}
</button>
)}
</div>
{/* Sound list */}
<div className="hub-usettings-sounds">
{sortedFolders.length === 0 ? (
<div className="hub-usettings-empty">
{search ? 'Keine Treffer' : 'Keine Sounds verfügbar'}
</div>
) : (
sortedFolders.map(([folder, sounds]) => (
<div key={folder} className="hub-usettings-folder">
<div className="hub-usettings-folder-name">{'\uD83D\uDCC1'} {folder}</div>
<div className="hub-usettings-folder-sounds">
{sounds.map(s => {
const isSelected = currentSound === s.relativePath || currentSound === s.fileName;
return (
<button
key={s.relativePath}
className={`hub-usettings-sound-btn ${isSelected ? 'selected' : ''}`}
onClick={() => setSound(activeSection, s.fileName)}
disabled={saving === activeSection}
title={s.relativePath}
>
<span className="hub-usettings-sound-icon">
{isSelected ? '\u2705' : '\uD83C\uDFB5'}
</span>
<span className="hub-usettings-sound-name">{s.name}</span>
</button>
);
})}
</div>
</div>
))
)}
</div>
</div>
)}
</div>
</div>
);
}