Modul-Tab im Plugin-Raster: Filter-Reiter und Symbole
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:
@@ -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;
|
||||||
@@ -5,32 +5,10 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { apiGet } from '../api.js';
|
import { apiGet } from '../api.js';
|
||||||
import {
|
import {
|
||||||
IconDevlog, IconCommits, IconChangelog, IconActivity, IconMessage, IconPlay,
|
IconMessage, IconTeam, IconServer, IconApi, IconPages, IconBrand,
|
||||||
IconLevel, IconStar, IconGallery, IconThumbsUp, IconGiveaway, IconBirthday,
|
IconBot, IconArrowRight,
|
||||||
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';
|
} from '../icons.jsx';
|
||||||
|
import { moduleIcon } from '../module-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
|
// Was kein Discord-Modul ist, sondern zum Drumherum gehört
|
||||||
const EXTRAS = [
|
const EXTRAS = [
|
||||||
@@ -91,7 +69,7 @@ export default function BotFeatures() {
|
|||||||
<h2 className="settings-title">// {group.label}</h2>
|
<h2 className="settings-title">// {group.label}</h2>
|
||||||
<div className="feature-grid">
|
<div className="feature-grid">
|
||||||
{items.map((f) => {
|
{items.map((f) => {
|
||||||
const Icon = FEATURE_ICONS[f.id] ?? IconIdea;
|
const Icon = moduleIcon(f.id);
|
||||||
return (
|
return (
|
||||||
<div className="card feature-card" key={f.id}>
|
<div className="card feature-card" key={f.id}>
|
||||||
<span className="feature-icon"><Icon size={20} /></span>
|
<span className="feature-icon"><Icon size={20} /></span>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
IconHome as IconLayers, IconWarning, IconCheck, IconMessage, IconRefresh,
|
IconHome as IconLayers, IconWarning, IconCheck, IconMessage, IconRefresh,
|
||||||
IconSearch, IconSystem as IconTune,
|
IconSearch, IconSystem as IconTune,
|
||||||
} from '../icons.jsx';
|
} from '../icons.jsx';
|
||||||
|
import { moduleIcon } from '../module-icons.jsx';
|
||||||
import { Markdown } from '../markdown.jsx';
|
import { Markdown } from '../markdown.jsx';
|
||||||
|
|
||||||
const API_SCOPES = [
|
const API_SCOPES = [
|
||||||
@@ -225,6 +226,7 @@ export default function Settings({ me }) {
|
|||||||
// Module (An/Aus für alle Funktionen)
|
// Module (An/Aus für alle Funktionen)
|
||||||
const [modules, setModules] = useState([]);
|
const [modules, setModules] = useState([]);
|
||||||
const [moduleGroups, setModuleGroups] = useState([]);
|
const [moduleGroups, setModuleGroups] = useState([]);
|
||||||
|
const [modFilter, setModFilter] = useState('alle');
|
||||||
// Text-Vorlagen (was der Bot nach außen schreibt)
|
// Text-Vorlagen (was der Bot nach außen schreibt)
|
||||||
const [templates, setTemplates] = useState([]);
|
const [templates, setTemplates] = useState([]);
|
||||||
const [templateGroups, setTemplateGroups] = 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 = (
|
const tabModule = (
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Module</h2>
|
<h2 className="settings-title">// Module</h2>
|
||||||
@@ -2007,19 +2028,33 @@ export default function Settings({ me }) {
|
|||||||
der Karte.
|
der Karte.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{moduleGroups.map((group) => {
|
<div className="mod-filters">
|
||||||
const mods = modules.filter((m) => m.group === group.id);
|
{modFilters.map((f) => (
|
||||||
if (mods.length === 0) return null;
|
<button
|
||||||
return (
|
key={f.id}
|
||||||
<div key={group.id} style={{ marginBottom: '1.8rem' }}>
|
className={`mod-filter${modFilter === f.id ? ' active' : ''}`}
|
||||||
<h3 className="settings-title" style={{ fontSize: '.66rem', marginBottom: '.8rem' }}>
|
disabled={f.n === 0 && modFilter !== f.id}
|
||||||
{group.label}
|
onClick={() => setModFilter(f.id)}
|
||||||
</h3>
|
>
|
||||||
|
{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">
|
<div className="mod-grid">
|
||||||
{mods.map((m) => (
|
{shownModules.map((m) => {
|
||||||
|
const Icon = moduleIcon(m.id);
|
||||||
|
return (
|
||||||
<div className={`card mod-card${m.enabled ? ' on' : ''}`} key={m.id}>
|
<div className={`card mod-card${m.enabled ? ' on' : ''}`} key={m.id}>
|
||||||
<div className="mod-head">
|
<div className="mod-head">
|
||||||
|
<span className="mod-icon"><Icon size={20} /></span>
|
||||||
|
<span className="mod-titles">
|
||||||
<span className="mod-name">{m.name}</span>
|
<span className="mod-name">{m.name}</span>
|
||||||
|
<span className="mod-group">{groupLabel(m.group)}</span>
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
className={`mod-switch${m.enabled ? ' on' : ''}`}
|
className={`mod-switch${m.enabled ? ' on' : ''}`}
|
||||||
title={m.enabled ? 'Ausschalten' : 'Einschalten'}
|
title={m.enabled ? 'Ausschalten' : 'Einschalten'}
|
||||||
@@ -2029,26 +2064,28 @@ export default function Settings({ me }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="mod-desc">{m.desc}</p>
|
<p className="mod-desc">{m.desc}</p>
|
||||||
|
<div className="mod-foot">
|
||||||
{m.enabled && m.missing.length > 0 && (
|
{m.enabled && m.missing.length > 0 && (
|
||||||
<p className="mod-missing with-icon">
|
<span className="mod-missing with-icon">
|
||||||
<IconWarning size={13} /> Fehlt noch: {m.missing.join(', ')}
|
<IconWarning size={13} /> Fehlt: {m.missing.join(', ')}
|
||||||
</p>
|
</span>
|
||||||
)}
|
)}
|
||||||
{m.enabled && m.missing.length === 0 && (
|
{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 && (
|
{m.tab && (
|
||||||
<button className="mod-link" onClick={() => setTab(m.tab)}>
|
<button className="mod-link" onClick={() => setTab(m.tab)}>
|
||||||
Einstellungen →
|
Einstellungen →
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const tabTexte = (
|
const tabTexte = (
|
||||||
|
|||||||
+49
-8
@@ -1759,28 +1759,69 @@ button.with-icon, a.with-icon { justify-content: center; }
|
|||||||
display: grid; gap: .9rem;
|
display: grid; gap: .9rem;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
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 {
|
.mod-card {
|
||||||
display: flex; flex-direction: column; gap: .5rem;
|
display: flex; flex-direction: column; gap: .6rem;
|
||||||
padding: 1.1rem 1.2rem; opacity: .6;
|
padding: 1.1rem 1.2rem; opacity: .55;
|
||||||
transition: opacity .2s, border-color .2s;
|
transition: opacity .2s, border-color .2s, transform .2s;
|
||||||
}
|
}
|
||||||
.mod-card.on { opacity: 1; }
|
.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 {
|
.mod-name {
|
||||||
font-family: var(--display); font-size: 1.2rem;
|
font-family: var(--display); font-size: 1.15rem;
|
||||||
letter-spacing: .04em; color: var(--text);
|
letter-spacing: .04em; color: var(--text);
|
||||||
}
|
}
|
||||||
.mod-desc { color: var(--muted); font-weight: 300; font-size: .95rem; line-height: 1.45; flex: 1; }
|
.mod-group {
|
||||||
.mod-missing, .mod-ready {
|
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;
|
font-family: var(--mono); font-size: .62rem;
|
||||||
letter-spacing: .1em; text-transform: uppercase;
|
letter-spacing: .1em; text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.mod-missing { color: var(--neon2); }
|
.mod-missing { color: var(--neon2); }
|
||||||
.mod-ready { color: var(--success); }
|
.mod-ready { color: var(--success); }
|
||||||
|
.mod-off { color: var(--muted2); }
|
||||||
.mod-link {
|
.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;
|
font-family: var(--mono); font-size: .62rem; letter-spacing: .15em;
|
||||||
text-transform: uppercase; color: var(--muted2); cursor: pointer;
|
text-transform: uppercase; color: var(--muted2); cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
transition: color .15s;
|
transition: color .15s;
|
||||||
}
|
}
|
||||||
.mod-link:hover { color: var(--neon); }
|
.mod-link:hover { color: var(--neon); }
|
||||||
|
|||||||
Reference in New Issue
Block a user