Nightly: Admin Emoji-Picker für Custom-Badges; Kategorien UI: Umbenennen/Löschen + Anlegen; Badges im UI dargestellt

This commit is contained in:
vibe-bot 2025-08-09 17:30:21 +02:00
parent 8795657f69
commit 9e5ba70711
3 changed files with 81 additions and 16 deletions

View file

@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, assignBadges } from './api'; import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, assignBadges, updateCategory, deleteCategory } from './api';
import type { VoiceChannelInfo, Sound, Category } from './types'; import type { VoiceChannelInfo, Sound, Category } from './types';
import { getCookie, setCookie } from './cookies'; import { getCookie, setCookie } from './cookies';
@ -26,6 +26,16 @@ export default function App() {
const [selectedSet, setSelectedSet] = useState<Record<string, boolean>>({}); const [selectedSet, setSelectedSet] = useState<Record<string, boolean>>({});
const [assignCategoryId, setAssignCategoryId] = useState<string>(''); const [assignCategoryId, setAssignCategoryId] = useState<string>('');
const [newCategoryName, setNewCategoryName] = useState<string>(''); const [newCategoryName, setNewCategoryName] = useState<string>('');
const [editingCategoryId, setEditingCategoryId] = useState<string>('');
const [editingCategoryName, setEditingCategoryName] = useState<string>('');
const [showEmojiPicker, setShowEmojiPicker] = useState<boolean>(false);
const emojiPickerRef = useRef<HTMLDivElement|null>(null);
const EMOJIS = useMemo(()=>{
// einfache, breite Auswahl gängiger Emojis; kann später erweitert/extern geladen werden
const groups = [
'😀😁😂🤣😅😊🙂😉😍😘😜🤪🤗🤔🤩🥳😎😴🤤','😇🥰🥺😡🤬😱😭🙈🙉🙊💀👻🤖🎃','👍👎👏🙌🙏🤝💪🔥✨💥🎉🎊','❤️🧡💛💚💙💜🖤🤍🤎💖💘💝','⭐🌟🌈☀️🌙⚡❄️☔🌊🍀','🎵🎶🎧🎤🎸🥁🎹🎺🎻','🍕🍔🍟🌭🌮🍣🍺🍻🍷🥂','🐶🐱🐼🐸🦄🐧🐢🦖🐙','🚀🛸✈️🚁🚗🏎️🚓🚒','🏆🥇🥈🥉🎯🎮🎲🧩']
return groups.join('').split('');
}, []);
const [showBroccoli, setShowBroccoli] = useState<boolean>(false); const [showBroccoli, setShowBroccoli] = useState<boolean>(false);
const selectedCount = useMemo(() => Object.values(selectedSet).filter(Boolean).length, [selectedSet]); const selectedCount = useMemo(() => Object.values(selectedSet).filter(Boolean).length, [selectedSet]);
const [clock, setClock] = useState<string>(() => new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Europe/Berlin' }).format(new Date())); const [clock, setClock] = useState<string>(() => new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Europe/Berlin' }).format(new Date()));
@ -439,26 +449,35 @@ export default function App() {
}} }}
>Zu Kategorie</button> >Zu Kategorie</button>
{/* Custom Badge setzen */} {/* Custom Badge Picker */}
<button <div style={{ position:'relative' }}>
className="bg-gray-700 hover:bg-gray-600 text-white font-bold py-3 px-6 rounded-lg transition duration-300" <button
onClick={async ()=>{ className="bg-gray-700 hover:bg-gray-600 text-white font-bold py-3 px-6 rounded-lg transition duration-300"
try{ onClick={()=> setShowEmojiPicker(v=>!v)}
const files = Object.entries(selectedSet).filter(([,v])=>v).map(([k])=>k); >Custom Emoji</button>
// Beispiel: Herz-Emoji als Badge; später UI-Eingabe möglich {showEmojiPicker && (
await assignBadges(files, ['❤'], []); <div ref={emojiPickerRef as any} className="emoji-picker" style={{ position:'absolute', top:'110%', right:0, zIndex: 99999 }}>
setInfo('Badge gesetzt'); setError(null); {EMOJIS.map((e, i)=> (
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder, activeCategoryId || undefined); <button key={i} onClick={async ()=>{
setSounds(resp.items); setTotal(resp.total); setFolders(resp.folders); try{
}catch(e:any){ setError(e?.message||'Badge-Update fehlgeschlagen'); setInfo(null); } const files = Object.entries(selectedSet).filter(([,v])=>v).map(([k])=>k);
}} await assignBadges(files, [e], []);
>Badge </button> setShowEmojiPicker(false);
setInfo('Badge gesetzt'); setError(null);
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder, activeCategoryId || undefined);
setSounds(resp.items); setTotal(resp.total); setFolders(resp.folders);
}catch(err:any){ setError(err?.message||'Badge-Update fehlgeschlagen'); setInfo(null); }
}}>{e}</button>
))}
</div>
)}
</div>
</> </>
)} )}
<div className="flex-1" /> <div className="flex-1" />
{/* Kategorie anlegen */} {/* Kategorien: anlegen/umbenennen/löschen */}
<input className="input-field" placeholder="Neue Kategorie" value={newCategoryName} onChange={(e)=>setNewCategoryName(e.target.value)} style={{maxWidth:200}} /> <input className="input-field" placeholder="Neue Kategorie" value={newCategoryName} onChange={(e)=>setNewCategoryName(e.target.value)} style={{maxWidth:200}} />
<button className="bg-gray-700 hover:bg-gray-600 text-white font-bold py-3 px-6 rounded-lg transition duration-300" onClick={async()=>{ <button className="bg-gray-700 hover:bg-gray-600 text-white font-bold py-3 px-6 rounded-lg transition duration-300" onClick={async()=>{
try{ try{
@ -470,6 +489,29 @@ export default function App() {
}catch(e:any){ setError(e?.message||'Anlegen fehlgeschlagen'); setInfo(null); } }catch(e:any){ setError(e?.message||'Anlegen fehlgeschlagen'); setInfo(null); }
}}>Anlegen</button> }}>Anlegen</button>
<select className="input-field" value={editingCategoryId} onChange={(e)=>{ setEditingCategoryId(e.target.value); const c = categories.find(x=>x.id===e.target.value); setEditingCategoryName(c?.name||''); }} style={{maxWidth:200}}>
<option value="">Kategorie wählen</option>
{categories.map(c=> <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
<input className="input-field" placeholder="Neuer Name" value={editingCategoryName} onChange={(e)=>setEditingCategoryName(e.target.value)} style={{maxWidth:200}} />
<button className="bg-gray-700 hover:bg-gray-600 text-white font-bold py-3 px-6 rounded-lg transition duration-300" onClick={async()=>{
try{
if(!editingCategoryId){ setError('Bitte Kategorie wählen'); return; }
await updateCategory(editingCategoryId, { name: editingCategoryName.trim() });
const cats = await fetchCategories(); setCategories(cats.categories || []);
setInfo('Kategorie umbenannt'); setError(null);
}catch(e:any){ setError(e?.message||'Umbenennen fehlgeschlagen'); setInfo(null); }
}}>Umbenennen</button>
<button className="bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300" onClick={async()=>{
try{
if(!editingCategoryId){ setError('Bitte Kategorie wählen'); return; }
await deleteCategory(editingCategoryId);
setEditingCategoryId(''); setEditingCategoryName('');
const cats = await fetchCategories(); setCategories(cats.categories || []);
setInfo('Kategorie gelöscht'); setError(null);
}catch(e:any){ setError(e?.message||'Löschen fehlgeschlagen'); setInfo(null); }
}}>Löschen</button>
<button className="bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300" onClick={async ()=>{ try{ await adminLogout(); setIsAdmin(false); clearSelection(); } catch{} }}>Logout</button> <button className="bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300" onClick={async ()=>{ try{ await adminLogout(); setIsAdmin(false); clearSelection(); } catch{} }}>Logout</button>
</div> </div>
)} )}

View file

@ -28,6 +28,23 @@ export async function createCategory(name: string, color?: string) {
return res.json(); 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[] = []) { export async function assignCategories(files: string[], add: string[], remove: string[] = []) {
const res = await fetch(`${API_BASE}/categories/assign`, { const res = await fetch(`${API_BASE}/categories/assign`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',

View file

@ -440,6 +440,12 @@ header p {
max-height: 280px; overflow-y: auto; max-height: 280px; overflow-y: auto;
z-index: 20000; z-index: 20000;
} }
.emoji-picker {
display: grid; grid-template-columns: repeat(10, 2rem); gap: .25rem; padding: .5rem;
max-height: 260px; overflow: auto; background: #0f1530; border:1px solid rgba(255,255,255,.28); border-radius: 12px;
}
.emoji-picker button { background: transparent; border: 0; font-size: 1.25rem; cursor: pointer; }
.emoji-picker button:hover { filter: brightness(1.2); }
.select-item { .select-item {
width: 100%; text-align: left; padding: 10px 12px; color: #e7e7ee; width: 100%; text-align: left; padding: 10px 12px; color: #e7e7ee;
background: transparent; border: 0; background: transparent; border: 0;