From ddd7610d6c12a1be2c187a2775c757671b74aeb7 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Sat, 1 Aug 2026 09:49:27 +0200 Subject: [PATCH] Playtester bekommen einen eigenen Bereich, Module ziehen um MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Playtester und Alpha-Keys standen als zwei Kaesten mitten im Community-Bereich zwischen Geburtstagen und Starboard. Die Liste war reine Anzeige: Namen und Datum, sonst nichts. Wer einem Einzelnen einen Schluessel geben wollte, konnte nur "an alle verteilen" druecken, und wer jemanden wieder rausnehmen wollte, musste an die Datenbank. Jetzt ist es ein eigener Bereich mit drei Zahlen oben und einer Liste, in der neben jedem Namen sein Schluessel steht. Daneben, je nach Zustand, "Schluessel geben" oder "Zurueckziehen", und ein ✕ zum Entfernen — die Rolle geht dabei mit, denn nur aus der Liste zu streichen und die Rolle stehen zu lassen waere eine Halbwahrheit. Der Vorrat ist jetzt sichtbar, nicht nur seine Groesse: einen vertippten Schluessel findet man sonst nie wieder. Nachlegen und "wer bekommt einen?" laufen ueber Dialoge, damit der Bereich nicht zumuellt — dafuer gibt es jetzt eine kleine Dialog-Komponente, gebaut wie die Lightbox (Portal, Escape, Hintergrund festhalten). Zurueckziehen legt den Schluessel zurueck in den Vorrat. Die Person hat ihn per DM natuerlich weiterhin — das ist eine Buchhaltung, keine Sperre. Loeschen geht absichtlich nur bei freien Schluesseln. ## Module dorthin, wo man sie sucht Ein paar Module standen im falschen Bereich, teils seit Langem: - Willkommens-Karte: Support → Brand. Sie ist Aussehen, kein Support-Fall, und im Brand-Bereich steht der Rest vom Auftritt. Der Block ist woertlich umgezogen, nur in eine eigene Konstante — so kann sich beim Umzug nichts am Inhalt geaendert haben. - Twitch & YouTube: System → Feeds. Es ist ein Feed. - Events: System → Community. - Auto-Rolle und Rollen merken: Ihre Felder standen laengst im Rollen-Bereich, nur das Register sagte "Community" — dadurch sprang "Einrichten" woanders hin als die Einstellung liegt. - Playtester und Alpha-Keys: Community → eigener Bereich. Damit ist System kein Sammelbecken mehr, sondern nur noch Technik: Waechter, Sprachkanaele, Erinnerungen, Sicherungen, Member-Gate. Geprueft: Routen (alle vier neuen bewacht), SQL gegen das Schema, und im Browser beide Dialoge (Escape schliesst, Hintergrund wird wieder freigegeben, Hinzufuegen bleibt bei leerem Feld aus), die Liste in allen drei Zustaenden, und die Suche — "willkommen" fuehrt jetzt nach Brand, "twitch" zu Feeds, "sticky" zu Rollen. Co-Authored-By: Claude Opus 5 --- README.md | 10 +- frontend/src/components/Dialog.jsx | 38 +++ frontend/src/pages/Settings.jsx | 501 +++++++++++++++++++---------- frontend/src/setting-index.js | 26 +- frontend/src/style.css | 50 +++ src/db.js | 8 + src/modules.js | 18 +- src/web/api.js | 116 +++++-- 8 files changed, 559 insertions(+), 208 deletions(-) create mode 100644 frontend/src/components/Dialog.jsx diff --git a/README.md b/README.md index 99c72e2..9b52866 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,8 @@ auf nichts mehr: keine Posts, keine Hintergrund-Prüfungen. | 🎉 **Giveaways** | `/giveaway` (Admin): Teilnahme-Button, automatische Ziehung; am Gewinner-Post 🔁 Neu auslosen + 👥 Teilnehmerliste | | 🎂 **Geburtstage** | `/geburtstag` zum Eintragen; morgens Gratulation im Kanal + Tages-Rolle | | 📅 **Events** | Neue Discord-Events werden angekündigt + öffentliche Events-Seite | -| 🧪 **Playtester-Programm** | `/playtester-setup` postet den Bewerbungs-Button; Rolle + Liste im Panel | -| 🎟️ **Alpha-Keys** | Key-Pool im Panel; Ein-Klick-Verteilung an alle Playtester per DM | +| 🧪 **Playtester-Programm** | `/playtester-setup` postet den Bewerbungs-Button; eigener Panel-Bereich mit Liste, Rolle und dem Schlüssel-Vorrat — Playtester lassen sich dort auch wieder entfernen (die Rolle geht mit) | +| 🎟️ **Alpha-Keys** | Schlüssel-Vorrat im Panel: nachlegen, gezielt an eine Person schicken, an alle Wartenden verteilen, zurückziehen oder löschen. Zustellung per Direktnachricht — kommt sie nicht an, bleibt der Schlüssel frei | | 🎭 **Rollen-Menüs** | Menüs im Webinterface bauen (Emoji, Label, Rolle, optional exklusiv), Bot postet Button-Embeds; Posts jederzeit editierbar | | 📋 **Bewerbungen** | Formulare (bis 5 Fragen) als Discord-Modal, Review mit ✅/❌ im Staff-Kanal, Rolle + DM bei Annahme — z. B. FiveM-Whitelist | | 🪪 **Verknüpfte Rollen** | Discords *Linked Roles*: Level, Playtester-Status und Dabei-seit werden als geprüfte Kennzahlen an Discord gemeldet — Rollen vergibt dann Discord selbst | @@ -169,14 +169,14 @@ selbst gehostet). ## Config-Seite (`/settings`) -**16 Bereiche**, in der Seitenleiste nach Themen gruppiert: +**17 Bereiche**, in der Seitenleiste nach Themen gruppiert: | Gruppe | Bereiche | |---|---| | Überblick | Status · Module · Texte · Werte | -| Auftritt | Brand · Seiten | +| Auftritt | Brand (samt Willkommens-Karte) · Seiten | | Inhalte | Feeds · Composer | -| Community | Community · Rollen · Support · Bewerbungen | +| Community | Community · Rollen · Support · Playtester · Bewerbungen | | Technik | Server · System | | Zugang | API · Team | diff --git a/frontend/src/components/Dialog.jsx b/frontend/src/components/Dialog.jsx new file mode 100644 index 0000000..34e7df3 --- /dev/null +++ b/frontend/src/components/Dialog.jsx @@ -0,0 +1,38 @@ +// Kleiner Dialog fürs Panel — für Dinge, die den Bereich sonst zumüllen: +// Schlüssel nachlegen, jemanden auswählen, etwas bestätigen. +// +// Rendert per Portal direkt in , damit Kopfzeile und Scanlines nicht +// durchscheinen — dieselbe Bauart wie die Lightbox. +import { useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { IconX } from '../icons.jsx'; + +export default function Dialog({ title, onClose, children, footer, weit = false }) { + useEffect(() => { + const onKey = (e) => { if (e.key === 'Escape') onClose(); }; + document.addEventListener('keydown', onKey); + // Hintergrund festhalten, sonst scrollt die Seite unter dem Dialog weg + document.body.style.overflow = 'hidden'; + return () => { + document.removeEventListener('keydown', onKey); + document.body.style.overflow = ''; + }; + }, [onClose]); + + return createPortal( +
+ {/* Klicks im Kasten dürfen nicht bis zum Hintergrund durchfallen */} +
e.stopPropagation()}> +
+

{title}

+ +
+
{children}
+ {footer &&
{footer}
} +
+
, + document.body + ); +} diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 55a5726..a99ab19 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -6,11 +6,12 @@ import { IconApplications, IconComposer, IconServer, IconSystem, IconApi, IconTeam, IconLock, IconBot, IconUpload, IconChevronDown, IconKey, IconPages, IconX, IconHome as IconLayers, IconWarning, IconCheck, IconMessage, IconRefresh, - IconSearch, IconSystem as IconTune, IconStar, + IconSearch, IconSystem as IconTune, IconStar, IconGame, IconTrash, } from '../icons.jsx'; import { moduleIcon } from '../module-icons.jsx'; import ModulePage from '../components/ModulePage.jsx'; import UptimeTrack from '../components/UptimeTrack.jsx'; +import Dialog from '../components/Dialog.jsx'; import { findSettings, SETTING_INDEX } from '../setting-index.js'; import { Markdown } from '../markdown.jsx'; @@ -59,6 +60,8 @@ const TABS = [ find: 'rollen menü selbstbedienung buttons reaction roles' }, { id: 'support', Icon: IconSupport, label: 'Support', section: 'community', find: 'ticket modmail willkommen protokoll modlog moderation warnung' }, + { id: 'playtester', Icon: IconGame, label: 'Playtester', section: 'community', + find: 'tester alpha keys schlüssel vergeben pool testen einladen' }, { id: 'bewerbungen', Icon: IconApplications, label: 'Bewerbungen', section: 'community', find: 'formular fragen bewerbung prüfen annehmen ablehnen' }, @@ -88,6 +91,7 @@ const TAB_SCOPES = { community: 'community', rollen: 'rollen', support: 'settings', + playtester: 'community', bewerbungen: 'bewerbungen', composer: 'content', seiten: 'content', @@ -298,7 +302,6 @@ export default function Settings({ me }) { const [error, setError] = useState(null); const [feedback, setFeedback] = useState(null); // Community - const [playtesters, setPlaytesters] = useState([]); // Rollen-Menüs const emptyMenu = { id: null, title: '', description: '', channel_id: '', exclusive: false, @@ -312,6 +315,9 @@ export default function Settings({ me }) { const [twitchCreds, setTwitchCreds] = useState({ id: '', secret: '' }); // Alpha-Keys const [alphaKeys, setAlphaKeys] = useState(null); + // Offene Dialoge: Schlüssel nachlegen bzw. jemanden auswählen + const [keyDialog, setKeyDialog] = useState(false); + const [keyEmpfaenger, setKeyEmpfaenger] = useState(null); const [keyUpload, setKeyUpload] = useState(''); // Bewerbungs-Formulare const emptyForm = { @@ -459,7 +465,6 @@ export default function Settings({ me }) { }) .catch((e) => setError(e.status)); apiGet('/api/apikeys').then((d) => setKeys(d.keys)).catch(() => {}); - apiGet('/api/playtesters').then((d) => setPlaytesters(d.playtesters)).catch(() => {}); apiGet('/api/rolemenus').then((d) => setMenus(d.menus)).catch(() => {}); apiGet('/api/tags').then((d) => setTags(d.tags)).catch(() => {}); apiGet('/api/scheduled').then((d) => setScheduled(d.posts)).catch(() => {}); @@ -820,6 +825,48 @@ export default function Settings({ me }) { } } + /** Einen Schlüssel an eine ausgewählte Person schicken */ + async function keyGeben(userId, name) { + try { + const res = await apiPost('/api/alphakeys/assign', { user_id: userId }); + setAlphaKeys(res); + setKeyEmpfaenger(null); + flash(`✓ Schlüssel an ${name} verschickt`); + } catch (e) { + flash(`✗ ${e.body?.error ?? 'Vergeben fehlgeschlagen'}`); + } + } + + /** Vergebenen Schlüssel zurück in den Vorrat */ + async function keyZurueck(key) { + if (!window.confirm(`Schlüssel ${key} wieder freigeben? Die Person behält die Nachricht mit dem Schlüssel.`)) return; + try { + setAlphaKeys(await apiPost('/api/alphakeys/revoke', { key })); + } catch { + flash('✗ Zurückziehen fehlgeschlagen'); + } + } + + /** Freien Schlüssel löschen — z. B. einen vertippten */ + async function keyLoeschen(key) { + try { + setAlphaKeys(await apiDelete(`/api/alphakeys/${encodeURIComponent(key)}`)); + } catch (e) { + flash(`✗ ${e.body?.error ?? 'Löschen fehlgeschlagen'}`); + } + } + + async function playtesterEntfernen(userId, name) { + if (!window.confirm(`${name} aus dem Playtester-Programm nehmen? Die Rolle wird mit entfernt.`)) return; + try { + const res = await apiDelete(`/api/playtesters/${userId}`); + setAlphaKeys(await apiGet('/api/alphakeys')); + flash(res.rolleWeg ? `✓ ${name} entfernt` : `✓ ${name} entfernt — Rolle blieb (Rechte prüfen)`); + } catch { + flash('✗ Entfernen fehlgeschlagen'); + } + } + async function distributeKeys() { if (!window.confirm(`Alpha-Keys jetzt per DM an ${alphaKeys?.playtestersWithout?.length ?? 0} Playtester verteilen?`)) return; try { @@ -1400,6 +1447,106 @@ export default function Settings({ me }) { ); const statusDot = { online: '#23a55a', idle: '#f0b232', dnd: '#f23f43' }[form.bot_presence_status] ?? '#23a55a'; + // Willkommens-Karte: eigener Block, damit sie im Brand-Bereich stehen + // kann, wo das uebrige Aussehen gepflegt wird — inhaltlich unveraendert. + const willkommensKarte = ( + <> +
+

// Willkommen

+
+ + setForm({ ...form, welcome_channel_id: v })} + empty="— deaktiviert —" + /> +
+
+ +
+

// Willkommens-Karte

+

+ Das Bild, das neue Mitglieder im Willkommens-Kanal begrüßt. Die Vorschau + zeigt sofort, wie es aussieht — gespeichert wird erst mit dem Knopf unten. +

+ +
+ +
+ + {form.welcome_card_enabled && ( + <> + Vorschau der Willkommens-Karte + +
+ {[ + ['welcome_card_bg', 'Hintergrund'], + ['welcome_card_accent', 'Akzent 1'], + ['welcome_card_accent2', 'Akzent 2'], + ['welcome_card_text', 'Name'], + ].map(([key, label]) => ( + + ))} +
+ +
+ + setForm({ ...form, welcome_card_kicker: e.target.value })} + /> +
+
+ + setForm({ ...form, welcome_card_sub: e.target.value })} + /> +
+
+ + setForm({ ...form, welcome_card_image: e.target.value })} + /> +
+
+ +
+ + )} +
+ + ); + const tabBrand = ( <>
@@ -1482,6 +1629,7 @@ export default function Settings({ me }) {
+ {willkommensKarte} ); @@ -1500,6 +1648,32 @@ export default function Settings({ me }) { />
+ +
+

// Twitch & YouTube

+
+ + setForm({ ...form, social_announce_channel_id: v })} + empty="— deaktiviert —" + /> +
+
+
+ setForm({ ...form, youtube_channel_id: e.target.value })} /> + setForm({ ...form, twitch_channel: e.target.value })} /> +
+
+
+ +
+ setTwitchCreds({ ...twitchCreds, id: e.target.value })} /> + setTwitchCreds({ ...twitchCreds, secret: e.target.value })} /> +
+
+
@@ -1603,59 +1777,16 @@ export default function Settings({ me }) {
-

// Playtester

-
- +

// Events

+
+ setForm({ ...form, playtester_role_id: v })} + items={channelItems} + value={form.events_announce_channel_id ?? ''} + onChange={(v) => setForm({ ...form, events_announce_channel_id: v })} empty="— deaktiviert —" />
- {playtesters.length > 0 && ( -
- {playtesters.map((p) => ( -
- {p.username} - seit {p.applied_at.slice(0, 10)} -
- ))} -
- )} -
- -
-

// Alpha-Keys

-
-
{alphaKeys?.free ?? '—'}Frei
-
{alphaKeys?.assigned?.length ?? '—'}Vergeben
-
{alphaKeys?.playtestersWithout?.length ?? '—'}Warten
-
-
- -