Nightly: Admin Emoji-Picker für Custom-Badges; Kategorien UI: Umbenennen/Löschen + Anlegen; Badges im UI dargestellt
This commit is contained in:
parent
8795657f69
commit
9e5ba70711
3 changed files with 81 additions and 16 deletions
|
|
@ -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 */}
|
||||||
|
<div style={{ position:'relative' }}>
|
||||||
<button
|
<button
|
||||||
className="bg-gray-700 hover:bg-gray-600 text-white font-bold py-3 px-6 rounded-lg transition duration-300"
|
className="bg-gray-700 hover:bg-gray-600 text-white font-bold py-3 px-6 rounded-lg transition duration-300"
|
||||||
onClick={async ()=>{
|
onClick={()=> setShowEmojiPicker(v=>!v)}
|
||||||
|
>Custom Emoji</button>
|
||||||
|
{showEmojiPicker && (
|
||||||
|
<div ref={emojiPickerRef as any} className="emoji-picker" style={{ position:'absolute', top:'110%', right:0, zIndex: 99999 }}>
|
||||||
|
{EMOJIS.map((e, i)=> (
|
||||||
|
<button key={i} onClick={async ()=>{
|
||||||
try{
|
try{
|
||||||
const files = Object.entries(selectedSet).filter(([,v])=>v).map(([k])=>k);
|
const files = Object.entries(selectedSet).filter(([,v])=>v).map(([k])=>k);
|
||||||
// Beispiel: Herz-Emoji als Badge; später UI-Eingabe möglich
|
await assignBadges(files, [e], []);
|
||||||
await assignBadges(files, ['❤'], []);
|
setShowEmojiPicker(false);
|
||||||
setInfo('Badge gesetzt'); setError(null);
|
setInfo('Badge gesetzt'); setError(null);
|
||||||
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder, activeCategoryId || undefined);
|
const resp = await fetchSounds(query, activeFolder === '__favs__' ? '__all__' : activeFolder, activeCategoryId || undefined);
|
||||||
setSounds(resp.items); setTotal(resp.total); setFolders(resp.folders);
|
setSounds(resp.items); setTotal(resp.total); setFolders(resp.folders);
|
||||||
}catch(e:any){ setError(e?.message||'Badge-Update fehlgeschlagen'); setInfo(null); }
|
}catch(err:any){ setError(err?.message||'Badge-Update fehlgeschlagen'); setInfo(null); }
|
||||||
}}
|
}}>{e}</button>
|
||||||
>Badge ❤</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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue