Modul-Tab im Plugin-Raster: Filter-Reiter und Symbole
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

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>
This commit is contained in:
2026-07-31 17:22:04 +02:00
co-authored by Claude Fable 5
parent 07afa094a2
commit c6977f9c4a
4 changed files with 152 additions and 64 deletions
+4 -26
View File
@@ -5,32 +5,10 @@ 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,
IconMessage, IconTeam, IconServer, IconApi, IconPages, IconBrand,
IconBot, IconArrowRight,
} 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,
};
import { moduleIcon } from '../module-icons.jsx';
// Was kein Discord-Modul ist, sondern zum Drumherum gehört
const EXTRAS = [
@@ -91,7 +69,7 @@ export default function BotFeatures() {
<h2 className="settings-title">// {group.label}</h2>
<div className="feature-grid">
{items.map((f) => {
const Icon = FEATURE_ICONS[f.id] ?? IconIdea;
const Icon = moduleIcon(f.id);
return (
<div className="card feature-card" key={f.id}>
<span className="feature-icon"><Icon size={20} /></span>
+67 -30
View File
@@ -8,6 +8,7 @@ import {
IconHome as IconLayers, IconWarning, IconCheck, IconMessage, IconRefresh,
IconSearch, IconSystem as IconTune,
} from '../icons.jsx';
import { moduleIcon } from '../module-icons.jsx';
import { Markdown } from '../markdown.jsx';
const API_SCOPES = [
@@ -225,6 +226,7 @@ export default function Settings({ me }) {
// Module (An/Aus für alle Funktionen)
const [modules, setModules] = useState([]);
const [moduleGroups, setModuleGroups] = useState([]);
const [modFilter, setModFilter] = useState('alle');
// Text-Vorlagen (was der Bot nach außen schreibt)
const [templates, setTemplates] = useState([]);
const [templateGroups, setTemplateGroups] = useState([]);
@@ -1997,6 +1999,25 @@ export default function Settings({ me }) {
</>
);
// Filter über dem Modul-Raster — bei 35 Karten ist eine lange Liste
// untereinander mühsam. „Noch einzurichten" ist bewusst dabei: das ist die
// Frage, mit der man am häufigsten herkommt.
const shownModules = modules.filter((m) => {
if (modFilter === 'alle') return true;
if (modFilter === 'todo') return m.enabled && m.missing.length > 0;
if (modFilter === 'aus') return !m.enabled;
return m.group === modFilter;
});
const modFilters = [
{ id: 'alle', label: 'Alle', n: modules.length },
...moduleGroups.map((g) => ({
id: g.id, label: g.label, n: modules.filter((m) => m.group === g.id).length,
})),
{ id: 'todo', label: 'Noch einzurichten', n: modules.filter((m) => m.enabled && m.missing.length > 0).length },
{ id: 'aus', label: 'Ausgeschaltet', n: modules.filter((m) => !m.enabled).length },
];
const groupLabel = (id) => moduleGroups.find((g) => g.id === id)?.label ?? '';
const tabModule = (
<div className="settings-section">
<h2 className="settings-title">// Module</h2>
@@ -2007,47 +2028,63 @@ export default function Settings({ me }) {
der Karte.
</p>
{moduleGroups.map((group) => {
const mods = modules.filter((m) => m.group === group.id);
if (mods.length === 0) return null;
return (
<div key={group.id} style={{ marginBottom: '1.8rem' }}>
<h3 className="settings-title" style={{ fontSize: '.66rem', marginBottom: '.8rem' }}>
{group.label}
</h3>
<div className="mod-grid">
{mods.map((m) => (
<div className={`card mod-card${m.enabled ? ' on' : ''}`} key={m.id}>
<div className="mod-head">
<div className="mod-filters">
{modFilters.map((f) => (
<button
key={f.id}
className={`mod-filter${modFilter === f.id ? ' active' : ''}`}
disabled={f.n === 0 && modFilter !== f.id}
onClick={() => setModFilter(f.id)}
>
{f.label} <span className="mod-filter-n">{f.n}</span>
</button>
))}
</div>
{shownModules.length === 0 ? (
<p className="notice">Hier ist gerade nichts.</p>
) : (
<div className="mod-grid">
{shownModules.map((m) => {
const Icon = moduleIcon(m.id);
return (
<div className={`card mod-card${m.enabled ? ' on' : ''}`} key={m.id}>
<div className="mod-head">
<span className="mod-icon"><Icon size={20} /></span>
<span className="mod-titles">
<span className="mod-name">{m.name}</span>
<button
className={`mod-switch${m.enabled ? ' on' : ''}`}
title={m.enabled ? 'Ausschalten' : 'Einschalten'}
onClick={() => toggleModule(m.id, !m.enabled)}
>
<span className="mod-knob" />
</button>
</div>
<p className="mod-desc">{m.desc}</p>
<span className="mod-group">{groupLabel(m.group)}</span>
</span>
<button
className={`mod-switch${m.enabled ? ' on' : ''}`}
title={m.enabled ? 'Ausschalten' : 'Einschalten'}
onClick={() => toggleModule(m.id, !m.enabled)}
>
<span className="mod-knob" />
</button>
</div>
<p className="mod-desc">{m.desc}</p>
<div className="mod-foot">
{m.enabled && m.missing.length > 0 && (
<p className="mod-missing with-icon">
<IconWarning size={13} /> Fehlt noch: {m.missing.join(', ')}
</p>
<span className="mod-missing with-icon">
<IconWarning size={13} /> Fehlt: {m.missing.join(', ')}
</span>
)}
{m.enabled && m.missing.length === 0 && (
<p className="mod-ready with-icon"><IconCheck size={13} /> Einsatzbereit</p>
<span className="mod-ready with-icon"><IconCheck size={13} /> Einsatzbereit</span>
)}
{!m.enabled && <span className="mod-off">Ausgeschaltet</span>}
{m.tab && (
<button className="mod-link" onClick={() => setTab(m.tab)}>
Einstellungen
</button>
)}
</div>
))}
</div>
</div>
);
})}
</div>
);
})}
</div>
)}
</div>
);