unify module-active toggle styling and regroup sidebar navigation

Extracts the accent-card toggle from Automod into a shared
ModuleActiveToggle component and applies it to every module's
"aktiv" switch (Statuspage, Dynamic Voice, Server Stats, Birthday,
Welcome) so they all match instead of the plain switch some pages
had. Also regroups the sidebar: Reaction Roles moves to Community
(it's member self-service, not moderation), and a new "Statistiken"
group splits Statuspage/Server Stats/Wachstum out of the catch-all
"Funktionen" section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 23:48:44 +02:00
parent aa246cd7ea
commit 54376fdad7
8 changed files with 64 additions and 30 deletions

View File

@@ -24,6 +24,7 @@ const navGroups = [
{ key: 'welcome', label: 'Willkommen', icon: <Sparkles size={18} /> }, { key: 'welcome', label: 'Willkommen', icon: <Sparkles size={18} /> },
{ key: 'birthday', label: 'Birthday', icon: <CalendarDays size={18} /> }, { key: 'birthday', label: 'Birthday', icon: <CalendarDays size={18} /> },
{ key: 'events', label: 'Events', icon: <Activity size={18} /> }, { key: 'events', label: 'Events', icon: <Activity size={18} /> },
{ key: 'reactionroles', label: 'Reaction Roles', icon: <Tag size={18} /> },
] ]
}, },
{ {
@@ -38,7 +39,6 @@ const navGroups = [
label: 'Moderation', label: 'Moderation',
items: [ items: [
{ key: 'automod', label: 'Automod', icon: <Shield size={18} /> }, { key: 'automod', label: 'Automod', icon: <Shield size={18} /> },
{ key: 'reactionroles', label: 'Reaction Roles', icon: <Tag size={18} /> },
{ key: 'tasks', label: 'Team-Aufgaben', icon: <ListChecks size={18} /> }, { key: 'tasks', label: 'Team-Aufgaben', icon: <ListChecks size={18} /> },
{ key: 'watchlist', label: 'Watchlist', icon: <Eye size={18} /> }, { key: 'watchlist', label: 'Watchlist', icon: <Eye size={18} /> },
] ]
@@ -48,6 +48,11 @@ const navGroups = [
items: [ items: [
{ key: 'dynamicvoice', label: 'Dynamic Voice', icon: <AudioLines size={18} /> }, { key: 'dynamicvoice', label: 'Dynamic Voice', icon: <AudioLines size={18} /> },
{ key: 'music', label: 'Musik', icon: <Music size={18} /> }, { key: 'music', label: 'Musik', icon: <Music size={18} /> },
]
},
{
label: 'Statistiken',
items: [
{ key: 'statuspage', label: 'Statuspage', icon: <RadioTower size={18} /> }, { key: 'statuspage', label: 'Statuspage', icon: <RadioTower size={18} /> },
{ key: 'serverstats', label: 'Server Stats', icon: <Activity size={18} /> }, { key: 'serverstats', label: 'Server Stats', icon: <Activity size={18} /> },
{ key: 'growth', label: 'Wachstum', icon: <TrendingUp size={18} /> }, { key: 'growth', label: 'Wachstum', icon: <TrendingUp size={18} /> },

View File

@@ -0,0 +1,25 @@
import type { ReactNode } from 'react';
import { AppSwitch } from './AppSwitch';
type Props = {
icon: ReactNode;
title: string;
description: string;
isSelected: boolean;
onChange: (value: boolean) => void;
};
export function ModuleActiveToggle({ icon, title, description, isSelected, onChange }: Props) {
return (
<div className="flex items-center gap-3 rounded-xl border border-accent bg-accent-soft p-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-accent text-accent-foreground">
{icon}
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-semibold">{title}</div>
<div className="text-xs text-muted">{description}</div>
</div>
<AppSwitch aria-label={title} isSelected={isSelected} onChange={onChange} />
</div>
);
}

View File

@@ -6,6 +6,7 @@ import { SectionCard } from '../components/shared/SectionCard';
import { ChannelSelect, type ChannelOption } from '../components/shared/ChannelSelect'; import { ChannelSelect, type ChannelOption } from '../components/shared/ChannelSelect';
import { RoleSelect, type RoleOption } from '../components/shared/RoleSelect'; import { RoleSelect, type RoleOption } from '../components/shared/RoleSelect';
import { AppSwitch } from '../components/shared/AppSwitch'; import { AppSwitch } from '../components/shared/AppSwitch';
import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
import { useGuildResources } from '../hooks/useGuildResources'; import { useGuildResources } from '../hooks/useGuildResources';
const FILTERS = [ const FILTERS = [
@@ -206,20 +207,13 @@ export function Automod() {
<h3 className="text-base font-semibold">Automod</h3> <h3 className="text-base font-semibold">Automod</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4 p-5"> <CardContent className="flex flex-col gap-4 p-5">
<div className="flex items-center gap-3 rounded-xl border border-accent bg-accent-soft p-3"> <ModuleActiveToggle
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-accent text-accent-foreground"> icon={<Shield size={16} />}
<Shield size={16} /> title="Automod aktiv"
</div> description="Schaltet alle Filter unten gesammelt ein oder aus."
<div className="min-w-0 flex-1"> isSelected={settings.automodEnabled !== false}
<div className="text-sm font-semibold">Automod aktiv</div> onChange={(v) => setSettings((s) => ({ ...s, automodEnabled: v }))}
<div className="text-xs text-muted">Schaltet alle Filter unten gesammelt ein oder aus.</div> />
</div>
<AppSwitch
aria-label="Automod aktiv"
isSelected={settings.automodEnabled !== false}
onChange={(v) => setSettings((s) => ({ ...s, automodEnabled: v }))}
/>
</div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{FILTERS.map((f) => { {FILTERS.map((f) => {

View File

@@ -3,7 +3,7 @@ import { CalendarDays, Save, Cake, Clock } from 'lucide-react';
import { useApp } from '../context/AppContext'; import { useApp } from '../context/AppContext';
import { SectionCard } from '../components/shared/SectionCard'; import { SectionCard } from '../components/shared/SectionCard';
import { ChannelSelect } from '../components/shared/ChannelSelect'; import { ChannelSelect } from '../components/shared/ChannelSelect';
import { AppSwitch } from '../components/shared/AppSwitch'; import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
import { useGuildResources } from '../hooks/useGuildResources'; import { useGuildResources } from '../hooks/useGuildResources';
export function Birthday() { export function Birthday() {
@@ -18,10 +18,12 @@ export function Birthday() {
<h3 className="text-base font-semibold">Konfiguration</h3> <h3 className="text-base font-semibold">Konfiguration</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4 p-5"> <CardContent className="flex flex-col gap-4 p-5">
<AppSwitch <ModuleActiveToggle
icon={<Cake size={16} />}
title="Birthday aktiv"
description="Speichert Geburtstage und sendet automatisch Glückwünsche."
isSelected={birthday.config?.enabled !== false} isSelected={birthday.config?.enabled !== false}
onChange={(v) => setBirthday((s) => ({ ...s, config: { ...s.config, enabled: v } }))} onChange={(v) => setBirthday((s) => ({ ...s, config: { ...s.config, enabled: v } }))}
label={<div className="flex items-center gap-2"><Cake size={16} /> Birthday aktiv</div>}
/> />
<TextField> <TextField>

View File

@@ -3,7 +3,7 @@ import { AudioLines, Save, Mic, Users } from 'lucide-react';
import { useApp } from '../context/AppContext'; import { useApp } from '../context/AppContext';
import { SectionCard } from '../components/shared/SectionCard'; import { SectionCard } from '../components/shared/SectionCard';
import { ChannelSelect } from '../components/shared/ChannelSelect'; import { ChannelSelect } from '../components/shared/ChannelSelect';
import { AppSwitch } from '../components/shared/AppSwitch'; import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
import { useGuildResources } from '../hooks/useGuildResources'; import { useGuildResources } from '../hooks/useGuildResources';
export function DynamicVoice() { export function DynamicVoice() {
@@ -18,10 +18,12 @@ export function DynamicVoice() {
<h3 className="text-base font-semibold">Konfiguration</h3> <h3 className="text-base font-semibold">Konfiguration</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4 p-5"> <CardContent className="flex flex-col gap-4 p-5">
<AppSwitch <ModuleActiveToggle
icon={<AudioLines size={16} />}
title="Dynamic Voice aktiv"
description="Erstellt private Voice-Channels aus der konfigurierten Lobby."
isSelected={settings.dynamicVoiceEnabled !== false} isSelected={settings.dynamicVoiceEnabled !== false}
onChange={(v) => setSettings((s) => ({ ...s, dynamicVoiceEnabled: v }))} onChange={(v) => setSettings((s) => ({ ...s, dynamicVoiceEnabled: v }))}
label={<div className="flex items-center gap-2"><AudioLines size={16} /> Dynamic Voice aktiv</div>}
/> />
<TextField> <TextField>

View File

@@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, Input, Button, Chip, Separator, TextFiel
import { Activity, Save, Trash2, Plus, BarChart3 } from 'lucide-react'; import { Activity, Save, Trash2, Plus, BarChart3 } from 'lucide-react';
import { useApp } from '../context/AppContext'; import { useApp } from '../context/AppContext';
import { SectionCard } from '../components/shared/SectionCard'; import { SectionCard } from '../components/shared/SectionCard';
import { AppSwitch } from '../components/shared/AppSwitch'; import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
export function ServerStats() { export function ServerStats() {
const { statsDraft, setStatsDraft, saveServerStats, statsItemDraft, setStatsItemDraft, addStatsItem, deleteStatsItem } = useApp(); const { statsDraft, setStatsDraft, saveServerStats, statsItemDraft, setStatsItemDraft, addStatsItem, deleteStatsItem } = useApp();
@@ -17,10 +17,12 @@ export function ServerStats() {
<h3 className="text-base font-semibold">Konfiguration</h3> <h3 className="text-base font-semibold">Konfiguration</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4 p-5"> <CardContent className="flex flex-col gap-4 p-5">
<AppSwitch <ModuleActiveToggle
icon={<BarChart3 size={16} />}
title="Server Stats aktiv"
description="Zeigt Mitglieder-/Channel-Zahlen als Voice-Statistiken an."
isSelected={statsDraft?.enabled === true} isSelected={statsDraft?.enabled === true}
onChange={(v) => setStatsDraft((s) => ({ ...(s || {}), enabled: v }))} onChange={(v) => setStatsDraft((s) => ({ ...(s || {}), enabled: v }))}
label={<div className="flex items-center gap-2"><BarChart3 size={16} /> Server Stats aktiv</div>}
/> />
<TextField> <TextField>

View File

@@ -4,7 +4,7 @@ import { useApp } from '../context/AppContext';
import { SectionCard } from '../components/shared/SectionCard'; import { SectionCard } from '../components/shared/SectionCard';
import type { StatusService } from '../types'; import type { StatusService } from '../types';
import { ChannelSelect } from '../components/shared/ChannelSelect'; import { ChannelSelect } from '../components/shared/ChannelSelect';
import { AppSwitch } from '../components/shared/AppSwitch'; import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
import { useGuildResources } from '../hooks/useGuildResources'; import { useGuildResources } from '../hooks/useGuildResources';
export function Statuspage() { export function Statuspage() {
@@ -21,10 +21,12 @@ export function Statuspage() {
<h3 className="text-base font-semibold">Konfiguration</h3> <h3 className="text-base font-semibold">Konfiguration</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4 p-5"> <CardContent className="flex flex-col gap-4 p-5">
<AppSwitch <ModuleActiveToggle
icon={<RadioTower size={16} />}
title="Statuspage aktiv"
description="Postet und aktualisiert die Statusseite mit deinen Services."
isSelected={statusDraft?.enabled !== false} isSelected={statusDraft?.enabled !== false}
onChange={(v) => setStatusDraft((s) => ({ ...(s || {}), enabled: v }))} onChange={(v) => setStatusDraft((s) => ({ ...(s || {}), enabled: v }))}
label={<div className="flex items-center gap-2"><RadioTower size={16} /> Statuspage aktiv</div>}
/> />
<TextField> <TextField>

View File

@@ -4,7 +4,7 @@ import { useApp } from '../context/AppContext';
import { SectionCard } from '../components/shared/SectionCard'; import { SectionCard } from '../components/shared/SectionCard';
import { DiscordPreview } from '../components/shared/DiscordPreview'; import { DiscordPreview } from '../components/shared/DiscordPreview';
import { ChannelSelect } from '../components/shared/ChannelSelect'; import { ChannelSelect } from '../components/shared/ChannelSelect';
import { AppSwitch } from '../components/shared/AppSwitch'; import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
import { useGuildResources } from '../hooks/useGuildResources'; import { useGuildResources } from '../hooks/useGuildResources';
export function Welcome() { export function Welcome() {
@@ -22,10 +22,12 @@ export function Welcome() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
<AppSwitch <ModuleActiveToggle
icon={<Sparkles size={16} />}
title="Welcome aktiv"
description="Sendet ein Begrüßungs-Embed, wenn neue Mitglieder beitreten."
isSelected={settings.welcomeConfig?.enabled !== false} isSelected={settings.welcomeConfig?.enabled !== false}
onChange={(v) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), enabled: v } }))} onChange={(v) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), enabled: v } }))}
label={<div className="flex items-center gap-2"><Sparkles size={16} /> Welcome aktiv</div>}
/> />
<TextField> <TextField>