Feature-Seite: alle Funktionen aus dem Modul-Register
/features zeigte bisher dieselbe Landing wie /, obwohl die Navigation und ein Knopf auf der Startseite eine echte Funktionsliste versprachen. Die Seite listet jetzt alle 35 Bot-Funktionen, nach denselben Gruppen sortiert wie im Panel, dazu sechs Punkte zum Drumherum (Webinterface, Texte, API/SSO, eigene Seiten, Team-Rechte, Betrieb). Die Einträge kommen über das neue öffentliche /api/features aus src/modules.js — derselben Quelle, aus der die Modul-Schalter kommen. Eine neue Funktion erscheint damit automatisch auf der Produktseite, statt dass jemand daran denken muss. Der Endpunkt gibt bewusst nur Name, Beschreibung und Gruppe heraus, nicht welche Module hier gerade laufen. Auch die Zahl auf der Startseite kommt von dort, damit nicht zwei Stellen dieselbe Zahl pflegen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ Konfigurieren.
|
|||||||
| Route | Inhalt |
|
| Route | Inhalt |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `/` | Landing: „Das kann der Bot" — Hero, Feature-Kacheln, Einladen-Button |
|
| `/` | 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 |
|
| `/commands` | Slash-Command-Referenz, aus dem Code erzeugt |
|
||||||
| `/docs` | Kurzanleitungen: API v1, SSO, Webhooks, Design-System |
|
| `/docs` | Kurzanleitungen: API v1, SSO, Webhooks, Design-System |
|
||||||
| `/dashboard` | die heutige Config-Seite (Login + Berechtigung nötig) |
|
| `/dashboard` | die heutige Config-Seite (Login + Berechtigung nötig) |
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// Rahmen der Bot-Produktseite — eigene Navigation, orange Leitfarbe.
|
// 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 { useEffect, useState } from 'react';
|
||||||
import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom';
|
import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom';
|
||||||
import { apiGet } from './api.js';
|
import { apiGet } from './api.js';
|
||||||
import BotLanding from './pages/BotLanding.jsx';
|
import BotLanding from './pages/BotLanding.jsx';
|
||||||
|
import BotFeatures from './pages/BotFeatures.jsx';
|
||||||
import BotCommands from './pages/BotCommands.jsx';
|
import BotCommands from './pages/BotCommands.jsx';
|
||||||
import Settings from './pages/Settings.jsx';
|
import Settings from './pages/Settings.jsx';
|
||||||
import Impressum from './pages/Impressum.jsx';
|
import Impressum from './pages/Impressum.jsx';
|
||||||
@@ -82,7 +83,7 @@ export default function BotApp() {
|
|||||||
<main className="main">
|
<main className="main">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<BotLanding />} />
|
<Route path="/" element={<BotLanding />} />
|
||||||
<Route path="/features" element={<BotLanding />} />
|
<Route path="/features" element={<BotFeatures />} />
|
||||||
<Route path="/commands" element={<BotCommands />} />
|
<Route path="/commands" element={<BotCommands />} />
|
||||||
<Route path="/dashboard" element={<Settings me={me} />} />
|
<Route path="/dashboard" element={<Settings me={me} />} />
|
||||||
<Route path="/impressum" element={<Impressum />} />
|
<Route path="/impressum" element={<Impressum />} />
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<section className="page-header">
|
||||||
|
<div className="page-bg-text">FEATURES</div>
|
||||||
|
<div className="page-header-grid" aria-hidden="true" />
|
||||||
|
<div className="page-header-content">
|
||||||
|
<div className="page-tag">// Alles drin, nichts hinter einer Paywall</div>
|
||||||
|
<h1 className="page-title">Funktionen</h1>
|
||||||
|
<p className="page-subtitle">
|
||||||
|
{total
|
||||||
|
? `${total} Funktionen — jede einzeln ein- und ausschaltbar, jede im Browser einstellbar.`
|
||||||
|
: 'Jede Funktion einzeln ein- und ausschaltbar, jede im Browser einstellbar.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="content">
|
||||||
|
{!data && <p className="notice">Lade …</p>}
|
||||||
|
|
||||||
|
{data?.groups.map((group) => {
|
||||||
|
const items = data.features.filter((f) => f.group === group.id);
|
||||||
|
if (items.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div className="feature-group" key={group.id}>
|
||||||
|
<h2 className="settings-title">// {group.label}</h2>
|
||||||
|
<div className="feature-grid">
|
||||||
|
{items.map((f) => {
|
||||||
|
const Icon = FEATURE_ICONS[f.id] ?? IconIdea;
|
||||||
|
return (
|
||||||
|
<div className="card feature-card" key={f.id}>
|
||||||
|
<span className="feature-icon"><Icon size={20} /></span>
|
||||||
|
<div>
|
||||||
|
<span className="feature-name">{f.name}</span>
|
||||||
|
<p className="feature-desc">{f.desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{data && (
|
||||||
|
<div className="feature-group">
|
||||||
|
<h2 className="settings-title">// Drumherum</h2>
|
||||||
|
<div className="feature-grid">
|
||||||
|
{EXTRAS.map(([Icon, name, desc]) => (
|
||||||
|
<div className="card feature-card" key={name}>
|
||||||
|
<span className="feature-icon"><Icon size={20} /></span>
|
||||||
|
<div>
|
||||||
|
<span className="feature-name">{name}</span>
|
||||||
|
<p className="feature-desc">{desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data && (
|
||||||
|
<div className="feature-outro">
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div className="home-cta">
|
||||||
|
<Link className="btn btn-save with-icon" to="/commands">
|
||||||
|
<IconBot /> Befehle ansehen
|
||||||
|
</Link>
|
||||||
|
<Link className="btn btn-ghost with-icon" to="/">
|
||||||
|
Zur Übersicht <IconArrowRight size={15} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,11 +23,14 @@ const HIGHLIGHTS = [
|
|||||||
export default function BotLanding() {
|
export default function BotLanding() {
|
||||||
const [status, setStatus] = useState(null);
|
const [status, setStatus] = useState(null);
|
||||||
const [hub, setHub] = useState(null);
|
const [hub, setHub] = useState(null);
|
||||||
|
const [featureCount, setFeatureCount] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Nur verlinken, wenn der Hub wirklich woanders liegt
|
// Nur verlinken, wenn der Hub wirklich woanders liegt
|
||||||
apiGet('/api/legal').then((d) => setHub(d.split ? d.hubUrl : null)).catch(() => {});
|
apiGet('/api/legal').then((d) => setHub(d.split ? d.hubUrl : null)).catch(() => {});
|
||||||
apiGet('/api/serverstats').then(setStatus).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);
|
window.scrollTo(0, 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -59,6 +62,12 @@ export default function BotLanding() {
|
|||||||
|
|
||||||
<section className="content">
|
<section className="content">
|
||||||
<div className="srv-summary">
|
<div className="srv-summary">
|
||||||
|
{featureCount && (
|
||||||
|
<div className="srv-sum-item">
|
||||||
|
<span className="srv-sum-n">{featureCount}</span>
|
||||||
|
<span className="srv-sum-label">Funktionen, einzeln schaltbar</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="srv-sum-item">
|
<div className="srv-sum-item">
|
||||||
<span className="srv-sum-n">16</span>
|
<span className="srv-sum-n">16</span>
|
||||||
<span className="srv-sum-label">Slash-Befehle</span>
|
<span className="srv-sum-label">Slash-Befehle</span>
|
||||||
|
|||||||
@@ -1642,6 +1642,42 @@ button.with-icon, a.with-icon { justify-content: center; }
|
|||||||
.site-bot .particle { background: #ff4d00; }
|
.site-bot .particle { background: #ff4d00; }
|
||||||
.bot-hero { min-height: 460px; }
|
.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 */
|
/* Befehlsliste */
|
||||||
.cmd-list { display: flex; flex-direction: column; gap: .5rem; }
|
.cmd-list { display: flex; flex-direction: column; gap: .5rem; }
|
||||||
.cmd-row {
|
.cmd-row {
|
||||||
|
|||||||
+9
-1
@@ -20,7 +20,7 @@ import {
|
|||||||
savePage, getPage, listPages, listPublishedPages, deletePage,
|
savePage, getPage, listPages, listPublishedPages, deletePage,
|
||||||
} from '../db.js';
|
} from '../db.js';
|
||||||
import crypto from 'node:crypto';
|
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 { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.js';
|
||||||
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
||||||
import { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.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() };
|
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 ---
|
// --- Text-Vorlagen: was der Bot nach außen schreibt ---
|
||||||
// (nicht /api/templates — das sind die gespeicherten Composer-Nachrichten)
|
// (nicht /api/templates — das sind die gespeicherten Composer-Nachrichten)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user