jukebox-vibe/web/src/api.ts
Bot 52c86240af Feat: Drag & Drop MP3/WAV Upload mit Progress-Tracking
Backend:
- multer reaktiviert (war auskommentiert) mit diskStorage + Collision-Handling
- /api/upload (POST, admin-protected): bis zu 20 Dateien gleichzeitig
- MP3/WAV-Filter (50MB Limit), sofortige Hintergrund-Normalisierung nach Upload

Frontend:
- Globale window dragenter/dragleave/drop Listener mit Counter gegen false-positives
- Drag-Overlay: Vollbild-Blur + animierter Drop-Zone (pulsierender Accent-Border, bouncing Icon)
- Upload-Queue: floating Card bottom-right mit Per-Datei Progressbar + Status-Icons
  (sync-Animation beim Hochladen, check_circle grün, error rot)
- Auto-Refresh der Soundliste + Analytics nach Upload
- Auto-Dismiss der Queue nach 3.5s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 22:15:07 +01:00

241 lines
8.9 KiB
TypeScript

import type { AnalyticsResponse, Sound, SoundsResponse, VoiceChannelInfo } from './types';
const API_BASE = import.meta.env.VITE_API_BASE_URL || '/api';
export async function fetchSounds(q?: string, folderKey?: string, categoryId?: string, fuzzy?: boolean): Promise<SoundsResponse> {
const url = new URL(`${API_BASE}/sounds`, window.location.origin);
if (q) url.searchParams.set('q', q);
if (folderKey !== undefined) url.searchParams.set('folder', folderKey);
if (categoryId) url.searchParams.set('categoryId', categoryId);
if (typeof fuzzy === 'boolean') url.searchParams.set('fuzzy', fuzzy ? '1' : '0');
const res = await fetch(url.toString());
if (!res.ok) throw new Error('Fehler beim Laden der Sounds');
return res.json();
}
export async function fetchAnalytics(): Promise<AnalyticsResponse> {
const res = await fetch(`${API_BASE}/analytics`);
if (!res.ok) throw new Error('Fehler beim Laden der Analytics');
return res.json();
}
// Kategorien
export async function fetchCategories() {
const res = await fetch(`${API_BASE}/categories`, { credentials: 'include' });
if (!res.ok) throw new Error('Fehler beim Laden der Kategorien');
return res.json();
}
export async function createCategory(name: string, color?: string) {
const res = await fetch(`${API_BASE}/categories`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
body: JSON.stringify({ name, color })
});
if (!res.ok) throw new Error('Kategorie anlegen fehlgeschlagen');
return res.json();
}
export async function updateCategory(id: string, payload: { name?: string; color?: string; sort?: number }) {
const res = await fetch(`${API_BASE}/categories/${encodeURIComponent(id)}`, {
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Kategorie aktualisieren fehlgeschlagen');
return res.json();
}
export async function deleteCategory(id: string) {
const res = await fetch(`${API_BASE}/categories/${encodeURIComponent(id)}`, {
method: 'DELETE', credentials: 'include'
});
if (!res.ok) throw new Error('Kategorie löschen fehlgeschlagen');
return res.json();
}
export async function assignCategories(files: string[], add: string[], remove: string[] = []) {
const res = await fetch(`${API_BASE}/categories/assign`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
body: JSON.stringify({ files, add, remove })
});
if (!res.ok) throw new Error('Zuordnung fehlgeschlagen');
return res.json();
}
export async function assignBadges(files: string[], add: string[], remove: string[] = []) {
const res = await fetch(`${API_BASE}/badges/assign`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
body: JSON.stringify({ files, add, remove })
});
if (!res.ok) throw new Error('Badges-Update fehlgeschlagen');
return res.json();
}
export async function clearBadges(files: string[]) {
const res = await fetch(`${API_BASE}/badges/clear`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
body: JSON.stringify({ files })
});
if (!res.ok) throw new Error('Badges löschen fehlgeschlagen');
return res.json();
}
export async function fetchChannels(): Promise<VoiceChannelInfo[]> {
const res = await fetch(`${API_BASE}/channels`);
if (!res.ok) throw new Error('Fehler beim Laden der Channels');
return res.json();
}
export async function getSelectedChannels(): Promise<Record<string, string>> {
const res = await fetch(`${API_BASE}/selected-channels`);
if (!res.ok) throw new Error('Fehler beim Laden der Channel-Auswahl');
const data = await res.json();
return data?.selected || {};
}
export async function setSelectedChannel(guildId: string, channelId: string): Promise<void> {
const res = await fetch(`${API_BASE}/selected-channel`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId, channelId })
});
if (!res.ok) throw new Error('Channel-Auswahl setzen fehlgeschlagen');
}
export async function playSound(soundName: string, guildId: string, channelId: string, volume: number, relativePath?: string): Promise<void> {
const res = await fetch(`${API_BASE}/play`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ soundName, guildId, channelId, volume, relativePath })
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data?.error || 'Play fehlgeschlagen');
}
}
export async function partyStart(guildId: string, channelId: string) {
const res = await fetch(`${API_BASE}/party/start`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId, channelId })
});
if (!res.ok) throw new Error('Partymode Start fehlgeschlagen');
}
export async function partyStop(guildId: string) {
const res = await fetch(`${API_BASE}/party/stop`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId })
});
if (!res.ok) throw new Error('Partymode Stop fehlgeschlagen');
}
export function subscribeEvents(onMessage: (data: any)=>void) {
const ev = new EventSource(`${API_BASE}/events`);
ev.onmessage = (e) => {
try { const data = JSON.parse(e.data); onMessage(data); } catch {}
};
return () => ev.close();
}
export async function setVolumeLive(guildId: string, volume: number): Promise<void> {
const res = await fetch(`${API_BASE}/volume`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId, volume })
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data?.error || 'Volume ändern fehlgeschlagen');
}
}
export async function getVolume(guildId: string): Promise<number> {
const url = new URL(`${API_BASE}/volume`, window.location.origin);
url.searchParams.set('guildId', guildId);
const res = await fetch(url.toString());
if (!res.ok) throw new Error('Fehler beim Laden der Lautstärke');
const data = await res.json();
return typeof data?.volume === 'number' ? data.volume : 1;
}
// Admin
export async function adminStatus(): Promise<boolean> {
const res = await fetch(`${API_BASE}/admin/status`, { credentials: 'include' });
if (!res.ok) return false;
const data = await res.json();
return !!data?.authenticated;
}
export async function adminLogin(password: string): Promise<boolean> {
const res = await fetch(`${API_BASE}/admin/login`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
body: JSON.stringify({ password })
});
return res.ok;
}
export async function adminLogout(): Promise<void> {
await fetch(`${API_BASE}/admin/logout`, { method: 'POST', credentials: 'include' });
}
export async function adminDelete(paths: string[]): Promise<void> {
const res = await fetch(`${API_BASE}/admin/sounds/delete`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
body: JSON.stringify({ paths })
});
if (!res.ok) throw new Error('Löschen fehlgeschlagen');
}
export async function adminRename(from: string, to: string): Promise<string> {
const res = await fetch(`${API_BASE}/admin/sounds/rename`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
body: JSON.stringify({ from, to })
});
if (!res.ok) throw new Error('Umbenennen fehlgeschlagen');
const data = await res.json();
return data?.to as string;
}
export async function playUrl(url: string, guildId: string, channelId: string, volume: number): Promise<void> {
const res = await fetch(`${API_BASE}/play-url`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, guildId, channelId, volume })
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data?.error || 'Play-URL fehlgeschlagen');
}
}
/** Lädt eine einzelne MP3/WAV-Datei hoch. Gibt den gespeicherten Dateinamen zurück. */
export function uploadFile(
file: File,
onProgress: (pct: number) => void,
): Promise<string> {
return new Promise((resolve, reject) => {
const form = new FormData();
form.append('files', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', `${API_BASE}/upload`);
xhr.upload.onprogress = e => {
if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100));
};
xhr.onload = () => {
if (xhr.status === 200) {
try {
const data = JSON.parse(xhr.responseText);
resolve(data.files?.[0]?.name ?? file.name);
} catch { resolve(file.name); }
} else {
try { reject(new Error(JSON.parse(xhr.responseText).error)); }
catch { reject(new Error(`HTTP ${xhr.status}`)); }
}
};
xhr.onerror = () => reject(new Error('Netzwerkfehler'));
xhr.send(form);
});
}