diff --git a/web/src/App.tsx b/web/src/App.tsx index 6476596..9829d0b 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; 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 { getCookie, setCookie } from './cookies'; @@ -26,6 +26,16 @@ export default function App() { const [selectedSet, setSelectedSet] = useState>({}); const [assignCategoryId, setAssignCategoryId] = useState(''); const [newCategoryName, setNewCategoryName] = useState(''); + const [editingCategoryId, setEditingCategoryId] = useState(''); + const [editingCategoryName, setEditingCategoryName] = useState(''); + const [showEmojiPicker, setShowEmojiPicker] = useState(false); + const emojiPickerRef = useRef(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(false); const selectedCount = useMemo(() => Object.values(selectedSet).filter(Boolean).length, [selectedSet]); const [clock, setClock] = useState(() => 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 - {/* Custom Badge setzen */} - + {/* Custom Badge Picker */} +
+ + {showEmojiPicker && ( +
+ {EMOJIS.map((e, i)=> ( + + ))} +
+ )} +
)}
- {/* Kategorie anlegen */} + {/* Kategorien: anlegen/umbenennen/lรถschen */} setNewCategoryName(e.target.value)} style={{maxWidth:200}} /> + + setEditingCategoryName(e.target.value)} style={{maxWidth:200}} /> + + +
)} diff --git a/web/src/api.ts b/web/src/api.ts index e9968a7..02d20ca 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -28,6 +28,23 @@ export async function createCategory(name: string, color?: string) { 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', diff --git a/web/src/styles.css b/web/src/styles.css index f6d1262..47434fa 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -440,6 +440,12 @@ header p { max-height: 280px; overflow-y: auto; 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 { width: 100%; text-align: left; padding: 10px 12px; color: #e7e7ee; background: transparent; border: 0;