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