From c88926aba07acc95522e264527cd1a056edc65f0 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Fri, 31 Jul 2026 21:54:31 +0200 Subject: [PATCH] Modul-Seiten: alles zu einer Funktion an einem Ort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher war eine Funktion über vier Bereiche verteilt: Schalter unter Module, Kanal unter Support, Texte unter Texte, Werte unter Werte. Wer die Begrüßung ändern wollte, brauchte drei Bereiche. Jetzt öffnet ein Klick auf die Karte die Seite dieser Funktion — mit Schalter, ihren Kanälen und Feldern, ihren Texten, ihren Werten und dem Weg zum passenden Werkzeug. Für „Willkommen" also Kanal, Karte, Farben, Vorschau und beide Texte untereinander. Bewusst keine 35 Einträge in der Seitenleiste: das Modul-Raster ist die Navigation, wie bei MEE6. Die Seitenleiste bleibt so lang wie vorher. Die Seite wird aus den Registern gebaut, nicht von Hand: modules.js kennt die Felder (neu: `fields` für alles jenseits der Pflichtkanäle, `tool` für den Verweis auf einen Editor), templates.js hatte die Zuordnung schon, tuning.js hat sie bekommen. Eine neue Funktion bekommt ihre Seite dadurch geschenkt — es gibt keinen Ort, an dem man sie vergessen könnte. Was ein eigenes Werkzeug hat — Rollen-Menüs, Bewerbungen, Composer, Server-Monitor —, bleibt in seinem Bereich; die Modul-Seite verlinkt nur dorthin. Dort ist mehr Platz, und die Editoren lassen sich nicht sinnvoll aus Registern erzeugen. Texte und Werte bleiben zusätzlich als flache Übersicht: wer alle Texte am Stück durchgehen will, soll dafür nicht 27 Seiten öffnen müssen. Beim Umbau ist mir ein Skript in die Quelldatei gelaufen — der Schreibvorgang scheiterte an einem Emoji und hinterließ modules.js leer. Wiederhergestellt aus dem letzten Commit; die Skripte schreiben jetzt erst in eine Zwischendatei und benennen danach um. Co-Authored-By: Claude Fable 5 --- README.md | 11 + frontend/src/components/ModulePage.jsx | 342 +++++++++++++++++++++++++ frontend/src/pages/Settings.jsx | 65 +++-- frontend/src/style.css | 49 ++++ src/modules.js | 90 +++++++ src/tuning.js | 44 ++-- src/web/api.js | 15 +- 7 files changed, 575 insertions(+), 41 deletions(-) create mode 100644 frontend/src/components/ModulePage.jsx diff --git a/README.md b/README.md index 10d335f..e2bf816 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,17 @@ Share Tech Mono, selbst gehostet). ### Module +Jede Funktion hat **ihre eigene Seite**: ein Klick auf ihre Karte zeigt Schalter, +Kanäle und Rollen, ihre Texte, ihre Werte und den Weg zum passenden Werkzeug — +statt alles über vier Bereiche verteilt. Für „Willkommen" also Kanal, Karte, +Farben, Vorschau und Begrüßungstext an einem Ort. Die Adresse merkt sich die +Seite (`/settings#module/welcome`). + +Gebaut wird sie aus den Registern: [`src/modules.js`](src/modules.js) kennt die +Felder, [`src/templates.js`](src/templates.js) die Texte, [`src/tuning.js`](src/tuning.js) +die Werte — jeweils über `module` verknüpft. Eine neue Funktion bekommt ihre +Seite dadurch geschenkt. + Alle **35 Funktionen** lassen sich im Tab *Module* einzeln ein- und ausschalten — gruppiert nach Inhalte, Community, Moderation und Server. Jede Karte zeigt, ob das Modul einsatzbereit ist oder noch etwas fehlt (z. B. ein Kanal), und verlinkt direkt diff --git a/frontend/src/components/ModulePage.jsx b/frontend/src/components/ModulePage.jsx new file mode 100644 index 0000000..de9bc9a --- /dev/null +++ b/frontend/src/components/ModulePage.jsx @@ -0,0 +1,342 @@ +// Die Seite einer einzelnen Funktion: Schalter, ihre Kanäle und Felder, ihre +// Texte, ihre Werte — alles an einem Ort statt über vier Bereiche verteilt. +// +// Vorbild ist die Plugin-Seite von MEE6. Die Felder werden aus den Registern +// gerendert (modules.js, templates.js, tuning.js), nicht von Hand gebaut — +// eine neue Funktion bekommt ihre Seite dadurch geschenkt. +import { useEffect, useState } from 'react'; +import { apiGet, apiPut, apiPost } from '../api.js'; +import { moduleIcon } from '../module-icons.jsx'; +import { IconArrowLeft, IconWarning, IconCheck, IconRefresh, IconChevronDown } from '../icons.jsx'; + +/** Ein Feld aus dem Register — Art entscheidet über die Darstellung */ +function Feld({ feld, value, onChange, PickSelect, channelItems, roleItems }) { + const gemeinsam = { id: `feld-${feld.setting}` }; + + if (feld.art === 'kanal' || feld.art === 'rolle') { + return ( + + ); + } + if (feld.art === 'schalter') { + return ( + + ); + } + if (feld.art === 'farbe') { + return ( + + ); + } + return ( + onChange(e.target.value)} + /> + ); +} + +/** + * @param {{ + * id: string, onBack: () => void, onToggle: (id, on) => void, + * form: object, setForm: (f: object) => void, + * PickSelect: Function, channelItems: Array, roleItems: Array, + * welcomePreviewUrl: string, flash: (msg: string) => void, + * goTab: (tab: string) => void, + * }} props + */ +export default function ModulePage(props) { + const { id, onBack, onToggle, form, setForm, PickSelect, channelItems, roleItems, + welcomePreviewUrl, flash, goTab } = props; + + const [detail, setDetail] = useState(null); + const [fehler, setFehler] = useState(false); + const [offenerText, setOffenerText] = useState(null); + const [textEntwurf, setTextEntwurf] = useState({}); + const [werteEntwurf, setWerteEntwurf] = useState({}); + + const laden = () => { + setFehler(false); + apiGet(`/api/modules/${id}`).then(setDetail).catch(() => setFehler(true)); + }; + useEffect(laden, [id]); + + if (fehler) return

Diese Funktion konnte nicht geladen werden.

; + if (!detail) return

Lade …

; + + const Icon = moduleIcon(detail.id); + + async function textSpeichern(textId, wert) { + try { + const res = await apiPut(`/api/texts/${encodeURIComponent(textId)}`, { text: wert }); + setDetail((d) => ({ ...d, texte: res.templates.filter((t) => t.module === d.id) })); + setTextEntwurf(({ [textId]: _weg, ...rest }) => rest); + flash(wert.trim() ? '✓ Text gespeichert' : '✓ Auf den Standard zurückgesetzt'); + } catch { + flash('✗ Speichern fehlgeschlagen'); + } + } + + async function werteSpeichern() { + const geaendert = Object.fromEntries( + Object.entries(werteEntwurf).filter(([wid, v]) => { + const jetzt = detail.werte.find((w) => w.id === wid); + return jetzt && String(v) !== String(jetzt.value); + }) + ); + if (Object.keys(geaendert).length === 0) return; + try { + const res = await apiPut('/api/tuning', geaendert); + setDetail((d) => ({ ...d, werte: res.values.filter((w) => w.module === d.id) })); + setWerteEntwurf({}); + flash('✓ Werte gespeichert'); + } catch (error) { + flash(`✗ ${error.body?.error ?? 'Speichern fehlgeschlagen'}`); + } + } + + async function anlegen(setting) { + try { + const res = await apiPost('/api/setup/create', { setting }); + setForm(res.settings); + laden(); + flash(res.wiederverwendet ? `✓ „${res.name}" verknüpft` : `✓ „${res.name}" angelegt`); + } catch (error) { + flash(`✗ ${error.body?.error ?? 'Anlegen fehlgeschlagen'}`); + } + } + + const werteOffen = detail.werte.some( + (w) => werteEntwurf[w.id] !== undefined && String(werteEntwurf[w.id]) !== String(w.value) + ); + + return ( +
+ + +
+ +
+

{detail.name}

+

{detail.desc}

+
+ +
+ + {detail.enabled && detail.missing.length > 0 && ( +

+ Fehlt noch: {detail.missing.map((m) => m.label).join(', ')} +

+ )} + {detail.enabled && detail.missing.length === 0 && ( +

+ Einsatzbereit +

+ )} + + {/* Kanäle, Rollen und alles Weitere */} + {detail.felder.length > 0 && ( +
+

Einstellungen

+ {detail.felder.filter((f) => f.art !== 'farbe').map((f) => ( +
+ {f.art !== 'schalter' && ( + + )} +
+ setForm({ ...form, [f.setting]: v })} + PickSelect={PickSelect} + channelItems={channelItems} + roleItems={roleItems} + /> + {/* Fehlt eine Voraussetzung, kann der Bot sie anlegen */} + {f.pflicht && detail.missing.some((m) => m.setting === f.setting) + && detail.missing.find((m) => m.setting === f.setting)?.create && ( + + )} +
+
+ ))} + + {detail.felder.some((f) => f.art === 'farbe') && ( +
+ {detail.felder.filter((f) => f.art === 'farbe').map((f) => ( + setForm({ ...form, [f.setting]: v })} + PickSelect={PickSelect} + channelItems={channelItems} + roleItems={roleItems} + /> + ))} +
+ )} +
+ )} + + {/* Vorschau, wo es eine gibt */} + {detail.vorschau === 'welcome' && form.welcome_card_enabled && ( +
+

Vorschau

+ Vorschau der Willkommens-Karte +
+ )} + + {/* Texte dieser Funktion */} + {detail.texte.length > 0 && ( +
+

Texte

+
+ {detail.texte.map((t) => { + const entwurf = textEntwurf[t.id]; + const wert = entwurf ?? t.text; + const geaendert = entwurf != null && entwurf !== t.text; + const offen = offenerText === t.id; + return ( +
+ + {offen && ( +
+