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:
@@ -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} /> },
|
||||||
|
|||||||
25
frontend/src/components/shared/ModuleActiveToggle.tsx
Normal file
25
frontend/src/components/shared/ModuleActiveToggle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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">
|
|
||||||
<div className="text-sm font-semibold">Automod aktiv</div>
|
|
||||||
<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}
|
isSelected={settings.automodEnabled !== false}
|
||||||
onChange={(v) => setSettings((s) => ({ ...s, automodEnabled: v }))}
|
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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user