From dadd75d4521371a63d164d2292cc0f04d696ba1c Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Fri, 31 Jul 2026 16:06:27 +0200 Subject: [PATCH] Texte: 27 Bot-Nachrichten im Panel bearbeitbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher standen Begrüßung, Level-Ansage, Ticket-Texte und alle Bestätigungs-Nachrichten fest im Code. Wer den Ton ändern wollte, musste die Quelldateien anfassen und neu deployen — für alle außer mir also gar nicht. src/templates.js hält jetzt alle nach außen gehenden Texte an einer Stelle: Standardtext, Platzhalter mit Erklärung und die Zuordnung zum Modul. Der neue Setup-Tab „Texte" zeigt sie als Karten mit Textfeld; die Bausteine darunter fügen sich per Klick an der Cursor-Position ein. Der Standard bleibt im Code, die Datenbank speichert nur echte Abweichungen. Ein leeres Feld heißt deshalb „wieder Standard" — und angepasste Vorlagen sind an einer Marke erkennbar. Vorlagen zu ausgeschalteten Modulen sind als solche gekennzeichnet, damit niemand einen Text feilt, den gerade nichts sendet. Unbekannte Platzhalter bleiben stehen statt still zu verschwinden, damit ein Tippfehler in der Nachricht sichtbar wird. Die Route heißt /api/texts, nicht /api/templates — letzteres sind seit je die gespeicherten Composer-Nachrichten. Co-Authored-By: Claude Fable 5 --- README.md | 10 + frontend/src/pages/Settings.jsx | 125 +++++++++++- frontend/src/style.css | 45 +++++ src/bot/app-forms.js | 17 +- src/bot/birthdays.js | 10 +- src/bot/client.js | 16 +- src/bot/commands/bug.js | 12 +- src/bot/commands/playtester-setup.js | 11 +- src/bot/commands/ticket-setup.js | 9 +- src/bot/commands/wunsch.js | 7 +- src/bot/extras.js | 15 +- src/bot/giveaways.js | 9 +- src/bot/levels.js | 9 +- src/bot/mod-tools.js | 25 +-- src/bot/tickets.js | 3 +- src/templates.js | 273 +++++++++++++++++++++++++++ src/web/api.js | 24 +++ 17 files changed, 560 insertions(+), 60 deletions(-) create mode 100644 src/templates.js diff --git a/README.md b/README.md index 7325492..1971b6b 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,16 @@ Definiert sind sie zentral in [`src/modules.js`](src/modules.js). Funktionen mit einem bereits vorhandenen Schalter (Level-System, Backups …) nutzen weiterhin dieselbe Einstellung — es gibt also keine zweite Wahrheit. +### Texte + +Was der Bot nach außen schreibt — Begrüßungen, Bestätigungen, Direktnachrichten — +steht als Vorlage im Tab *Texte*. Platzhalter wie `{user}` oder `{count}` setzt der +Bot beim Senden ein; ein Klick auf den Baustein fügt ihn an der Cursor-Position ein. +Ein leeres Feld stellt den Standardtext wieder her. + +Die Vorlagen stehen in [`src/templates.js`](src/templates.js). Der Standard lebt im +Code, die Datenbank enthält nur echte Abweichungen. + ### Setup-Seite (`/settings`) Aufgeteilt in **Tabs mit Sidebar-Navigation** (Status · Brand · Feeds · Community · Rollen · Support · Bewerbungen · Composer · Server · System · API · Team) — diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index b76e104..0de4a03 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -5,7 +5,7 @@ import { IconStatus, IconBrand, IconFeeds, IconCommunity, IconRoles, IconSupport, IconApplications, IconComposer, IconServer, IconSystem, IconApi, IconTeam, IconLock, IconBot, IconUpload, IconChevronDown, IconKey, IconPages, IconX, - IconHome as IconLayers, IconWarning, IconCheck, + IconHome as IconLayers, IconWarning, IconCheck, IconMessage, IconRefresh, } from '../icons.jsx'; import { Markdown } from '../markdown.jsx'; @@ -19,6 +19,7 @@ const API_SCOPES = [ const TABS = [ { id: 'status', Icon: IconStatus, label: 'Status' }, { id: 'module', Icon: IconLayers, label: 'Module' }, + { id: 'texte', Icon: IconMessage, label: 'Texte' }, { id: 'brand', Icon: IconBrand, label: 'Brand' }, { id: 'feeds', Icon: IconFeeds, label: 'Feeds' }, { id: 'community', Icon: IconCommunity, label: 'Community' }, @@ -37,6 +38,7 @@ const TABS = [ const TAB_SCOPES = { status: 'any', module: 'settings', + texte: 'settings', brand: null, feeds: 'settings', community: 'community', @@ -175,6 +177,10 @@ export default function Settings({ me }) { // Module (An/Aus für alle Funktionen) const [modules, setModules] = useState([]); const [moduleGroups, setModuleGroups] = useState([]); + // Text-Vorlagen (was der Bot nach außen schreibt) + const [templates, setTemplates] = useState([]); + const [templateGroups, setTemplateGroups] = useState([]); + const [tplDraft, setTplDraft] = useState({}); // id → bearbeiteter Text // Eigene Seiten (Regeln, Über uns, …) const emptyPage = { slug: '', title: '', content: '', published: false, in_menu: true, sort: 0, isNew: true }; const [pages, setPages] = useState([]); @@ -236,6 +242,10 @@ export default function Settings({ me }) { setModules(d.modules); setModuleGroups(d.groups); }).catch(() => {}); + apiGet('/api/texts').then((d) => { + setTemplates(d.templates); + setTemplateGroups(d.groups); + }).catch(() => {}); }, [me.admin, me.scopes?.length]); useEffect(() => { @@ -281,6 +291,34 @@ export default function Settings({ me }) { } } + /** Vorlage speichern. Leerer Text stellt den Standard wieder her. */ + async function saveTemplate(id, text) { + try { + const res = await apiPut(`/api/texts/${encodeURIComponent(id)}`, { text }); + setTemplates(res.templates); + setTplDraft(({ [id]: _drop, ...rest }) => rest); + flash(text.trim() ? '✓ Text gespeichert' : '✓ Auf den Standard zurückgesetzt'); + } catch { + flash('✗ Speichern fehlgeschlagen'); + } + } + + /** Platzhalter an der Cursor-Position einfügen */ + function insertPlaceholder(tpl, key) { + const field = document.getElementById(`tpl-${tpl.id}`); + const current = tplDraft[tpl.id] ?? tpl.text; + const at = field?.selectionStart ?? current.length; + const next = `${current.slice(0, at)}{${key}}${current.slice(field?.selectionEnd ?? at)}`; + setTplDraft((old) => ({ ...old, [tpl.id]: next })); + // Cursor hinter den eingefügten Platzhalter setzen + requestAnimationFrame(() => { + if (!field) return; + field.focus(); + const pos = at + key.length + 2; + field.setSelectionRange(pos, pos); + }); + } + /** Titel → Adress-Kürzel (Umlaute mit auflösen) */ function slugify(value) { return String(value).toLowerCase() @@ -1756,6 +1794,90 @@ export default function Settings({ me }) { ); + const tabTexte = ( +
+

// Texte

+

+ Alles, was der Bot nach außen schreibt — Begrüßungen, Bestätigungen, + Direktnachrichten. Die Bausteine in geschweiften Klammern setzt der Bot + beim Senden ein; klick einen an, um ihn an der Cursor-Position einzufügen. + Ein leeres Feld bedeutet: wieder der Standardtext. +

+ + {templateGroups.map((group) => { + const items = templates.filter((t) => t.group === group.id); + if (items.length === 0) return null; + return ( +
+

+ {group.label} +

+
+ {items.map((t) => { + const draft = tplDraft[t.id]; + const value = draft ?? t.text; + const dirty = draft != null && draft !== t.text; + const off = t.module && modules.some((m) => m.id === t.module && !m.enabled); + return ( +
+
+ {t.name} + {t.customized && angepasst} + {off && Modul aus} +
+ +