diff --git a/docs/konzept-zwei-seiten.md b/docs/konzept-zwei-seiten.md index 89e2001..7d31643 100644 --- a/docs/konzept-zwei-seiten.md +++ b/docs/konzept-zwei-seiten.md @@ -40,7 +40,7 @@ Konfigurieren. | Route | Inhalt | |---|---| | `/` | Landing: „Das kann der Bot" — Hero, Feature-Kacheln, Einladen-Button | -| `/features` | Alle Funktionen ausführlich, gruppiert (Community, Moderation, Feeds, Server) | +| `/features` | Alle Funktionen ausführlich, gruppiert — die Liste kommt aus `/api/features` und damit aus dem Modul-Register, kann also nicht veralten | | `/commands` | Slash-Command-Referenz, aus dem Code erzeugt | | `/docs` | Kurzanleitungen: API v1, SSO, Webhooks, Design-System | | `/dashboard` | die heutige Config-Seite (Login + Berechtigung nötig) | diff --git a/frontend/src/BotApp.jsx b/frontend/src/BotApp.jsx index a55b613..ac255b1 100644 --- a/frontend/src/BotApp.jsx +++ b/frontend/src/BotApp.jsx @@ -1,9 +1,10 @@ // Rahmen der Bot-Produktseite — eigene Navigation, orange Leitfarbe. -// Das Dashboard ist dieselbe Setup-Seite wie bisher, nur unter /dashboard. +// Das Dashboard ist dieselbe Config-Seite wie bisher, nur unter /dashboard. import { useEffect, useState } from 'react'; import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom'; import { apiGet } from './api.js'; import BotLanding from './pages/BotLanding.jsx'; +import BotFeatures from './pages/BotFeatures.jsx'; import BotCommands from './pages/BotCommands.jsx'; import Settings from './pages/Settings.jsx'; import Impressum from './pages/Impressum.jsx'; @@ -82,7 +83,7 @@ export default function BotApp() {
} /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/BotFeatures.jsx b/frontend/src/pages/BotFeatures.jsx new file mode 100644 index 0000000..a85adc8 --- /dev/null +++ b/frontend/src/pages/BotFeatures.jsx @@ -0,0 +1,147 @@ +// Funktionsliste der Produktseite. Die Einträge kommen aus /api/features und +// damit aus demselben Register wie die Module im Panel — die Seite kann also +// nicht veralten, wenn eine Funktion dazukommt. +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { apiGet } from '../api.js'; +import { + IconDevlog, IconCommits, IconChangelog, IconActivity, IconMessage, IconPlay, + IconLevel, IconStar, IconGallery, IconThumbsUp, IconGiveaway, IconBirthday, + IconEvents, IconGame, IconKey, IconRoles, IconApplications, IconShield, + IconArchive, IconMail, IconSupport, IconMagic, IconTeam, IconRefresh, + IconWarning, IconServer, IconMonitor, IconVoice, IconZap, IconClock, + IconSend, IconTag, IconDatabase, IconPackage, IconLock, + IconApi, IconPages, IconBrand, IconBot, IconArrowRight, IconIdea, +} from '../icons.jsx'; + +// Ein Symbol je Funktion — bewusst hier und nicht im Register, damit der +// Bot-Code nichts über das Frontend wissen muss. +const FEATURE_ICONS = { + devlogs: IconDevlog, commit_feed: IconCommits, releases: IconChangelog, + weekly_recap: IconActivity, devlog_threads: IconMessage, social: IconPlay, + levels: IconLevel, starboard: IconStar, gallery: IconGallery, + voting: IconThumbsUp, giveaways: IconGiveaway, birthdays: IconBirthday, + events: IconEvents, playtester: IconGame, alpha_keys: IconKey, + role_menus: IconRoles, applications: IconApplications, + moderation: IconShield, modlog: IconArchive, modmail: IconMail, + tickets: IconSupport, welcome: IconMagic, autorole: IconTeam, + sticky_roles: IconRefresh, bug_reports: IconWarning, + server_monitor: IconServer, watchdog: IconMonitor, temp_voice: IconVoice, + triggers: IconZap, reminders: IconClock, scheduled_posts: IconSend, + tags: IconTag, backups: IconDatabase, repo_backups: IconPackage, + member_gate: IconLock, +}; + +// Was kein Discord-Modul ist, sondern zum Drumherum gehört +const EXTRAS = [ + [IconBrand, 'Webinterface im eigenen Look', + 'Jede Funktion wird im Browser eingestellt — keine Konfigurationsdatei, kein Neustart. ' + + 'Farben, Bot-Avatar und Banner gehören dazu, und andere Dienste können denselben Look mitbenutzen.'], + [IconMessage, 'Texte selbst schreiben', + 'Begrüßung, Level-Ansage, Ticket-Texte — alles, was der Bot nach außen schreibt, ' + + 'lässt sich im Panel ändern, mit Platzhaltern für Name, Zahl und Link.'], + [IconApi, 'API & Single Sign-On', + 'Eigene Skripte posten über die API v1 mit Bearer-Schlüsseln und Bereichs-Rechten. ' + + 'Andere Dienste nutzen den Discord-Login mit und bekommen die Rollen gleich mitgeliefert.'], + [IconPages, 'Eigene Seiten', + 'Regeln, Über uns, FAQ — in Markdown geschrieben, mit einem Klick veröffentlicht ' + + 'und automatisch im Menü.'], + [IconTeam, 'Rechte fürs Team', + 'Team-Mitglieder bekommen gezielten Zugriff auf einzelne Bereiche. ' + + 'Wer was geändert hat, steht im Protokoll.'], + [IconServer, 'Läuft überall', + 'Node und SQLite in einem Container, ein Volume für die Daten. Nächtliche Sicherungen ' + + 'inklusive — von der Datenbank und allen Repositories.'], +]; + +export default function BotFeatures() { + const [data, setData] = useState(null); + + useEffect(() => { + apiGet('/api/features').then(setData).catch(() => setData({ groups: [], features: [] })); + window.scrollTo(0, 0); + }, []); + + const total = data ? data.features.length + EXTRAS.length : null; + + return ( + <> +
+
FEATURES
+
+ +
+ {!data &&

Lade …

} + + {data?.groups.map((group) => { + const items = data.features.filter((f) => f.group === group.id); + if (items.length === 0) return null; + return ( +
+

// {group.label}

+
+ {items.map((f) => { + const Icon = FEATURE_ICONS[f.id] ?? IconIdea; + return ( +
+ +
+ {f.name} +

{f.desc}

+
+
+ ); + })} +
+
+ ); + })} + + {data && ( +
+

// Drumherum

+
+ {EXTRAS.map(([Icon, name, desc]) => ( +
+ +
+ {name} +

{desc}

+
+
+ ))} +
+
+ )} + + {data && ( +
+

+ Gebaut, weil drei Bots mit je eigenem Abo für eine Community + weder übersichtlich noch günstig sind. Der Quelltext liegt offen, + der Betrieb kostet einen Container. +

+
+ + Befehle ansehen + + + Zur Übersicht + +
+
+ )} +
+ + ); +} diff --git a/frontend/src/pages/BotLanding.jsx b/frontend/src/pages/BotLanding.jsx index c578eaa..226b597 100644 --- a/frontend/src/pages/BotLanding.jsx +++ b/frontend/src/pages/BotLanding.jsx @@ -23,11 +23,14 @@ const HIGHLIGHTS = [ export default function BotLanding() { const [status, setStatus] = useState(null); const [hub, setHub] = useState(null); + const [featureCount, setFeatureCount] = useState(null); useEffect(() => { // Nur verlinken, wenn der Hub wirklich woanders liegt apiGet('/api/legal').then((d) => setHub(d.split ? d.hubUrl : null)).catch(() => {}); apiGet('/api/serverstats').then(setStatus).catch(() => {}); + // Zahl kommt aus dem Modul-Register, damit sie nicht veraltet + apiGet('/api/features').then((d) => setFeatureCount(d.features.length)).catch(() => {}); window.scrollTo(0, 0); }, []); @@ -59,6 +62,12 @@ export default function BotLanding() {
+ {featureCount && ( +
+ {featureCount} + Funktionen, einzeln schaltbar +
+ )}
16 Slash-Befehle diff --git a/frontend/src/style.css b/frontend/src/style.css index eec5cc8..0c1cc0b 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1642,6 +1642,42 @@ button.with-icon, a.with-icon { justify-content: center; } .site-bot .particle { background: #ff4d00; } .bot-hero { min-height: 460px; } +/* Funktionsliste der Produktseite */ +.feature-group { margin-bottom: 2.6rem; } +.feature-grid { + display: grid; gap: .8rem; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + margin-top: 1.1rem; +} +.feature-card { + display: flex; gap: .9rem; align-items: flex-start; + padding: 1.1rem 1.2rem; + transition: border-color .2s, transform .2s; +} +.feature-card:hover { border-color: var(--neon); transform: translateY(-2px); } +.feature-icon { + flex: none; display: inline-flex; + color: var(--neon); margin-top: .15rem; +} +.feature-name { + display: block; + font-family: var(--display); font-size: 1.1rem; + letter-spacing: .04em; color: var(--text); + margin-bottom: .3rem; +} +.feature-desc { + color: var(--muted); font-weight: 300; + font-size: .92rem; line-height: 1.5; margin: 0; +} +.feature-outro { + border-top: 1px solid var(--border); + padding-top: 1.8rem; margin-top: .5rem; +} +.feature-outro p { + color: var(--muted); font-weight: 300; + line-height: 1.7; max-width: 46rem; margin: 0 0 1.4rem; +} + /* Befehlsliste */ .cmd-list { display: flex; flex-direction: column; gap: .5rem; } .cmd-row { diff --git a/src/web/api.js b/src/web/api.js index 3110378..c836bb5 100644 --- a/src/web/api.js +++ b/src/web/api.js @@ -20,7 +20,7 @@ import { savePage, getPage, listPages, listPublishedPages, deletePage, } from '../db.js'; import crypto from 'node:crypto'; -import { moduleStates, setModuleEnabled, MODULE_GROUPS } from '../modules.js'; +import { moduleStates, setModuleEnabled, MODULE_GROUPS, MODULES } from '../modules.js'; import { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.js'; import { lastResults, GAME_ICONS } from '../bot/server-monitor.js'; import { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.js'; @@ -185,6 +185,14 @@ export function registerApiRoutes(app, client) { return { ok: true, modules: moduleStates() }; }); + // Funktionsliste für die Produktseite — öffentlich und ohne Zustand: + // was der Bot kann, nicht was hier gerade eingeschaltet ist. Quelle ist + // dasselbe Register wie die Module, damit die Seite nicht veraltet. + app.get('/api/features', async () => ({ + groups: MODULE_GROUPS, + features: MODULES.map((m) => ({ id: m.id, name: m.name, desc: m.desc, group: m.group })), + })); + // --- Text-Vorlagen: was der Bot nach außen schreibt --- // (nicht /api/templates — das sind die gespeicherten Composer-Nachrichten)