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); }