Vorbild war das MEE6-Dashboard: Kategorie-Reiter über einem Raster aus Karten, jede mit einem eigenen Symbol. Inhaltlich war unser Modul-Tab schon dort — es fehlten genau diese zwei Dinge. Die Gruppen standen als Überschriften untereinander, was bei 35 Karten eine lange Rolle ergab. Jetzt filtern Reiter mit Anzahl: Alle, die vier Gruppen, dazu „Noch einzurichten" und „Ausgeschaltet". Die beiden letzten sind bewusst dabei — mit genau diesen Fragen kommt man am häufigsten her. Die Gruppe steht klein unter dem Namen, damit sie im gefilterten Raster nicht verlorengeht. Jede Karte hat eine Symbol-Kachel, die bei laufendem Modul in der Markenfarbe leuchtet und sonst grau bleibt — man erkennt eine Funktion damit am Bild statt am Lesen. Die Zuordnung lag schon in der Funktionsseite; sie liegt jetzt in module-icons.jsx, damit beide Seiten dieselbe benutzen. Farbige Kachelbilder wie bei MEE6 gibt es bewusst nicht: die Strichsymbole passen zum Rest der Oberfläche, bunte Kacheln wären ein Fremdkörper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
126 lines
6.0 KiB
React
126 lines
6.0 KiB
React
// 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 {
|
|
IconMessage, IconTeam, IconServer, IconApi, IconPages, IconBrand,
|
|
IconBot, IconArrowRight,
|
|
} from '../icons.jsx';
|
|
import { moduleIcon } from '../module-icons.jsx';
|
|
|
|
// 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 = moduleIcon(f.id);
|
|
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>
|
|
</>
|
|
);
|
|
}
|