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
-
-
- -