diff --git a/frontend/src/module-icons.jsx b/frontend/src/module-icons.jsx
new file mode 100644
index 0000000..5d74718
--- /dev/null
+++ b/frontend/src/module-icons.jsx
@@ -0,0 +1,32 @@
+// Ein Symbol je Bot-Funktion. Bewusst im Frontend und nicht im Modul-Register:
+// der Bot-Code soll nichts über die Oberfläche wissen müssen.
+//
+// Genutzt von der Produktseite (/features) und den Modul-Karten in der Config —
+// deshalb liegt die Zuordnung hier und nicht in einer der beiden Seiten.
+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, IconIdea,
+} from './icons.jsx';
+
+export const MODULE_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,
+};
+
+/** Symbol einer Funktion — Glühbirne, falls für eine neue noch keins hinterlegt ist */
+export const moduleIcon = (id) => MODULE_ICONS[id] ?? IconIdea;
diff --git a/frontend/src/pages/BotFeatures.jsx b/frontend/src/pages/BotFeatures.jsx
index a85adc8..e4e6ebb 100644
--- a/frontend/src/pages/BotFeatures.jsx
+++ b/frontend/src/pages/BotFeatures.jsx
@@ -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() {
// {group.label}
{items.map((f) => {
- const Icon = FEATURE_ICONS[f.id] ?? IconIdea;
+ const Icon = moduleIcon(f.id);
return (
diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx
index 41a448e..8184cda 100644
--- a/frontend/src/pages/Settings.jsx
+++ b/frontend/src/pages/Settings.jsx
@@ -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 = (
// Module
@@ -2007,47 +2028,63 @@ export default function Settings({ me }) {
der Karte.
- {moduleGroups.map((group) => {
- const mods = modules.filter((m) => m.group === group.id);
- if (mods.length === 0) return null;
- return (
-
-
- {group.label}
-
-
- {mods.map((m) => (
-
-
+
+ {modFilters.map((f) => (
+
+ ))}
+
+
+ {shownModules.length === 0 ? (
+
Hier ist gerade nichts.
+ ) : (
+
+ {shownModules.map((m) => {
+ const Icon = moduleIcon(m.id);
+ return (
+
+
+
+
{m.name}
-
-
-
{m.desc}
+
{groupLabel(m.group)}
+
+
+
+
{m.desc}
+
{m.enabled && m.missing.length > 0 && (
-
- Fehlt noch: {m.missing.join(', ')}
-
+
+ Fehlt: {m.missing.join(', ')}
+
)}
{m.enabled && m.missing.length === 0 && (
-
Einsatzbereit
+
Einsatzbereit
)}
+ {!m.enabled &&
Ausgeschaltet}
{m.tab && (
)}
- ))}
-
-
- );
- })}
+
+ );
+ })}
+
+ )}
);
diff --git a/frontend/src/style.css b/frontend/src/style.css
index e39e10d..5cc3261 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -1759,28 +1759,69 @@ button.with-icon, a.with-icon { justify-content: center; }
display: grid; gap: .9rem;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
+/* Filter über dem Raster */
+.mod-filters { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1.2rem; }
+.mod-filter {
+ display: inline-flex; align-items: center; gap: .45rem;
+ font-family: var(--mono); font-size: .62rem;
+ letter-spacing: .12em; text-transform: uppercase;
+ color: var(--muted); background: var(--bg2);
+ border: 1px solid var(--border); border-radius: 99px;
+ padding: .35rem .8rem; cursor: pointer;
+ transition: color .15s, border-color .15s, background .15s;
+}
+.mod-filter:hover:not(:disabled) { color: var(--text); border-color: var(--neon); }
+.mod-filter.active { color: #0a0a0a; background: var(--neon); border-color: var(--neon); }
+.mod-filter:disabled { opacity: .3; cursor: default; }
+.mod-filter-n {
+ font-size: .58rem; opacity: .7;
+ padding-left: .35rem; border-left: 1px solid currentColor;
+}
+
.mod-card {
- display: flex; flex-direction: column; gap: .5rem;
- padding: 1.1rem 1.2rem; opacity: .6;
- transition: opacity .2s, border-color .2s;
+ display: flex; flex-direction: column; gap: .6rem;
+ padding: 1.1rem 1.2rem; opacity: .55;
+ transition: opacity .2s, border-color .2s, transform .2s;
}
.mod-card.on { opacity: 1; }
-.mod-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
+.mod-card:hover { border-color: var(--neon); transform: translateY(-2px); }
+.mod-head { display: flex; align-items: flex-start; gap: .8rem; }
+/* Symbol-Kachel wie auf der Funktionsseite — macht die Karten unterscheidbar */
+.mod-icon {
+ flex: none; display: inline-flex; align-items: center; justify-content: center;
+ width: 2.4rem; height: 2.4rem; border-radius: 9px;
+ background: var(--bg3); border: 1px solid var(--border);
+ color: var(--muted2);
+ transition: color .2s, border-color .2s;
+}
+.mod-card.on .mod-icon { color: var(--neon); border-color: rgba(245, 197, 24, .3); }
+.mod-titles { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
.mod-name {
- font-family: var(--display); font-size: 1.2rem;
+ font-family: var(--display); font-size: 1.15rem;
letter-spacing: .04em; color: var(--text);
}
-.mod-desc { color: var(--muted); font-weight: 300; font-size: .95rem; line-height: 1.45; flex: 1; }
-.mod-missing, .mod-ready {
+.mod-group {
+ font-family: var(--mono); font-size: .55rem;
+ letter-spacing: .16em; text-transform: uppercase; color: var(--muted2);
+}
+.mod-desc { color: var(--muted); font-weight: 300; font-size: .93rem; line-height: 1.45; flex: 1; }
+.mod-foot {
+ display: flex; align-items: center; justify-content: space-between;
+ gap: .8rem; flex-wrap: wrap;
+ padding-top: .2rem;
+}
+.mod-missing, .mod-ready, .mod-off {
font-family: var(--mono); font-size: .62rem;
letter-spacing: .1em; text-transform: uppercase;
}
.mod-missing { color: var(--neon2); }
.mod-ready { color: var(--success); }
+.mod-off { color: var(--muted2); }
.mod-link {
- align-self: flex-start; background: none; border: 0; padding: 0;
+ background: none; border: 0; padding: 0; margin-left: auto;
font-family: var(--mono); font-size: .62rem; letter-spacing: .15em;
text-transform: uppercase; color: var(--muted2); cursor: pointer;
+ white-space: nowrap;
transition: color .15s;
}
.mod-link:hover { color: var(--neon); }