migrate dashboard to real HeroUI v3 components and neutral theme
All checks were successful
Deploy Discord Bot / deploy (push) Successful in -1m4s
SonarQube / sonar (push) Successful in 1m8s

The frontend was built against HeroUI v2 props/classes (variant="light"/"flat",
color="primary", startContent, text-tiny, <Avatar src=/name=>) while
@heroui/react ^3.2.1 is installed, so most styling was silently ignored.
Rewrites every page/component onto the actual v3 API (variant/color enums,
Tooltip/Dropdown trigger-content structure, Avatar.Image/Fallback via a new
AppAvatar helper) and replaces the custom orange branding with HeroUI's
neutral default theme tokens. Also compacts the dashboard stat tiles, narrows
the sidebar, and swaps the activity stat grid for a single-hue comparison bar
chart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Pepe44DEV
2026-07-02 12:34:53 +02:00
parent 6599158737
commit 0c47dce508
32 changed files with 537 additions and 584 deletions

View File

@@ -9,6 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@heroui/react": "^3.2.1", "@heroui/react": "^3.2.1",
"@heroui/styles": "^3.2.1",
"framer-motion": "^12.23.24", "framer-motion": "^12.23.24",
"lucide-react": "^0.542.0", "lucide-react": "^0.542.0",
"react": "^19.2.0", "react": "^19.2.0",

View File

@@ -3,22 +3,6 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
:root {
--app-bg: #110f0d;
--app-bg-soft: #181513;
--app-panel: #1d1a1f;
--app-panel-strong: #221e24;
--app-panel-muted: #171419;
--app-border: rgba(255, 255, 255, 0.08);
--app-border-strong: rgba(255, 166, 77, 0.22);
--app-text: #f6f2ee;
--app-text-muted: #b8aca1;
--app-accent: #ff8a3d;
--app-accent-strong: #ff6a00;
--app-accent-soft: rgba(255, 138, 61, 0.14);
--app-accent-glow: rgba(255, 138, 61, 0.28);
}
html, body, #root { html, body, #root {
min-height: 100%; min-height: 100%;
} }
@@ -26,11 +10,8 @@ html, body, #root {
body { body {
margin: 0; margin: 0;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: background: var(--background);
radial-gradient(circle at top left, rgba(255, 106, 0, 0.12), transparent 22%), color: var(--foreground);
radial-gradient(circle at top right, rgba(255, 138, 61, 0.08), transparent 18%),
linear-gradient(180deg, #0f0d0c 0%, #141110 100%);
color: var(--app-text);
} }
* { * {
@@ -41,137 +22,37 @@ button {
cursor: pointer; cursor: pointer;
} }
input,
textarea,
select { select {
width: 100%; width: 100%;
border: 1px solid rgba(255, 255, 255, 0.14); border: var(--border-width-field) solid var(--field-border, var(--border));
border-radius: 14px; border-radius: var(--radius-field);
background: rgba(255, 255, 255, 0.05); background: var(--field-background, var(--default));
color: inherit; color: var(--field-foreground, var(--foreground));
font: inherit; font: inherit;
padding: 0.85rem 1rem; padding: 0.6rem 0.85rem;
transition: border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease; transition: border-color 150ms ease, background-color 150ms ease;
} }
textarea {
min-height: 120px;
resize: vertical;
}
input::placeholder,
textarea::placeholder {
color: rgba(255, 255, 255, 0.42);
}
input:hover,
textarea:hover,
select:hover { select:hover {
border-color: rgba(255, 255, 255, 0.22); background: var(--field-hover);
background: rgba(255, 255, 255, 0.07);
} }
input:focus,
textarea:focus,
select:focus { select:focus {
outline: none; outline: none;
border-color: var(--app-accent); border-color: var(--field-border-focus);
box-shadow: 0 0 0 3px rgba(255, 138, 61, 0.16); background: var(--field-focus);
background: rgba(255, 255, 255, 0.08);
} }
label { label {
display: inline-block; display: inline-block;
margin-bottom: 0.45rem; margin-bottom: 0.35rem;
font-size: 0.95rem; font-size: 0.875rem;
font-weight: 600; font-weight: 500;
} color: var(--foreground);
.surface-card {
background: linear-gradient(180deg, rgba(31, 27, 33, 0.96), rgba(23, 20, 25, 0.96));
border: 1px solid var(--app-border);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
.surface-card-strong {
background: linear-gradient(180deg, rgba(36, 30, 24, 0.98), rgba(25, 21, 18, 0.98));
border: 1px solid var(--app-border-strong);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.03),
0 18px 50px rgba(0, 0, 0, 0.28);
}
.surface-card-muted {
background: rgba(255, 255, 255, 0.025);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.accent-text {
color: var(--app-accent);
}
.accent-chip {
background: var(--app-accent-soft);
color: #ffb37f;
border: 1px solid rgba(255, 138, 61, 0.18);
}
.accent-button {
background: linear-gradient(135deg, var(--app-accent) 0%, var(--app-accent-strong) 100%);
color: white;
border: 1px solid rgba(255, 166, 77, 0.45);
box-shadow: 0 10px 24px rgba(255, 106, 0, 0.18);
}
.accent-button:hover {
filter: brightness(1.06);
}
.accent-button-soft {
background: var(--app-accent-soft);
color: #ffbc8e;
border: 1px solid rgba(255, 138, 61, 0.18);
}
.ghost-button {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.09);
color: var(--app-text);
}
.nav-button {
color: var(--app-text-muted);
}
.nav-button:hover {
background: rgba(255, 138, 61, 0.08);
color: var(--app-text);
}
.nav-button-active {
background: linear-gradient(135deg, rgba(255, 138, 61, 0.22), rgba(255, 106, 0, 0.24));
color: white;
border: 1px solid rgba(255, 138, 61, 0.28);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.sidebar-shell {
background: linear-gradient(180deg, rgba(16, 14, 13, 0.98), rgba(18, 15, 14, 0.98));
border-right: 1px solid rgba(255, 255, 255, 0.05);
}
.brand-mark {
background: linear-gradient(135deg, var(--app-accent) 0%, var(--app-accent-strong) 100%);
box-shadow: 0 12px 30px rgba(255, 106, 0, 0.22);
}
.section-subtle {
color: var(--app-text-muted);
} }
.scrollbar-thin { .scrollbar-thin {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
} }
.scrollbar-thin::-webkit-scrollbar { .scrollbar-thin::-webkit-scrollbar {
@@ -180,6 +61,6 @@ label {
} }
.scrollbar-thin::-webkit-scrollbar-thumb { .scrollbar-thin::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15); background: var(--border);
border-radius: 999px; border-radius: 999px;
} }

View File

@@ -8,8 +8,9 @@ export function AppLayout({ children }: { children: React.ReactNode }) {
if (loading) { if (loading) {
return ( return (
<div className="flex min-h-screen items-center justify-center"> <div className="flex min-h-screen flex-col items-center justify-center gap-3">
<Spinner color="primary" label="Dashboard wird geladen..." size="lg" /> <Spinner color="accent" size="lg" />
<p className="text-sm text-muted">Dashboard wird geladen...</p>
</div> </div>
); );
} }
@@ -17,7 +18,7 @@ export function AppLayout({ children }: { children: React.ReactNode }) {
if (!guilds.length) { if (!guilds.length) {
return ( return (
<div className="flex min-h-screen items-center justify-center"> <div className="flex min-h-screen items-center justify-center">
<p className="text-default-500">Keine Server verfügbar</p> <p className="text-muted">Keine Server verfügbar</p>
</div> </div>
); );
} }

View File

@@ -1,7 +1,8 @@
import { Button, Chip, Tooltip, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Avatar } from '@heroui/react'; import { Button, Chip, Tooltip, Dropdown, DropdownTrigger, DropdownPopover, DropdownMenu, DropdownItem } from '@heroui/react';
import { Moon, Sun, ChevronDown, LogOut, Settings } from 'lucide-react'; import { Moon, Sun, ChevronDown, LogOut, Settings } from 'lucide-react';
import { useTheme } from '../../hooks/useTheme'; import { useTheme } from '../../hooks/useTheme';
import { useApp } from '../../context/AppContext'; import { useApp } from '../../context/AppContext';
import { AppAvatar } from '../shared/AppAvatar';
const navLabels: Record<string, string> = { const navLabels: Record<string, string> = {
overview: 'Übersicht', overview: 'Übersicht',
@@ -30,14 +31,14 @@ export function Header() {
return ( return (
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<div className="flex items-center gap-1.5 text-sm section-subtle min-w-0"> <div className="flex items-center gap-1.5 text-sm text-muted min-w-0">
<button className="hover:text-white transition-colors" onClick={() => setSection('overview')}> <button className="hover:text-foreground transition-colors" onClick={() => setSection('overview')}>
Dashboard Dashboard
</button> </button>
{section !== 'overview' && ( {section !== 'overview' && (
<> <>
<span>/</span> <span>/</span>
<span className="accent-text font-semibold truncate">{navLabels[section] || section}</span> <span className="text-accent font-semibold truncate">{navLabels[section] || section}</span>
</> </>
)} )}
</div> </div>
@@ -45,33 +46,36 @@ export function Header() {
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
{statusMessage && ( {statusMessage && (
<Chip size="sm" variant="flat" className="accent-chip max-w-[220px]"> <Chip size="sm" variant="soft" color="accent" className="max-w-[220px]">
<span className="truncate">{statusMessage}</span> <span className="truncate">{statusMessage}</span>
</Chip> </Chip>
)} )}
<Tooltip content={dark ? 'Helles Design' : 'Dunkles Design'} placement="bottom"> <Tooltip>
<Button isIconOnly radius="lg" size="sm" variant="light" className="ghost-button" onPress={toggle}> <Tooltip.Trigger>
{dark ? <Sun size={16} /> : <Moon size={16} />} <Button isIconOnly size="sm" variant="ghost" onPress={toggle}>
</Button> {dark ? <Sun size={16} /> : <Moon size={16} />}
</Button>
</Tooltip.Trigger>
<Tooltip.Content placement="bottom">{dark ? 'Helles Design' : 'Dunkles Design'}</Tooltip.Content>
</Tooltip> </Tooltip>
<Dropdown placement="bottom-end"> <Dropdown>
<DropdownTrigger> <DropdownTrigger className="inline-flex h-9 items-center gap-2 rounded-3xl px-3 text-sm font-medium hover:bg-default-hover">
<Button className="ghost-button gap-2" radius="lg" size="sm" variant="light"> <AppAvatar name={user?.username} size="sm" className="size-6" />
<Avatar name={user?.username} size="sm" className="size-6" /> <span className="hidden sm:inline text-sm">{user?.username}</span>
<span className="hidden sm:inline text-sm">{user?.username}</span> <ChevronDown size={14} />
<ChevronDown size={14} />
</Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu aria-label="User Menu"> <DropdownPopover placement="bottom end">
<DropdownItem key="settings" startContent={<Settings size={14} />} onPress={() => setSection('settings')}> <DropdownMenu aria-label="User Menu">
Einstellungen <DropdownItem key="settings" onAction={() => setSection('settings')}>
</DropdownItem> <Settings size={14} /> Einstellungen
<DropdownItem key="logout" startContent={<LogOut size={14} />} color="danger" onPress={handleLogout}> </DropdownItem>
Abmelden <DropdownItem key="logout" variant="danger" onAction={handleLogout}>
</DropdownItem> <LogOut size={14} /> Abmelden
</DropdownMenu> </DropdownItem>
</DropdownMenu>
</DropdownPopover>
</Dropdown> </Dropdown>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { import {
Avatar, Button, Card, CardContent, ScrollShadow, Tooltip, Button, Card, CardContent, ScrollShadow, Tooltip,
Select, SelectTrigger, SelectValue, SelectPopover, ListBox, ListBoxItem Select, SelectTrigger, SelectValue, SelectPopover, ListBox, ListBoxItem
} from '@heroui/react'; } from '@heroui/react';
import { import {
@@ -9,6 +9,7 @@ import {
Shield, Sparkles, Tag, Ticket, Wrench Shield, Sparkles, Tag, Ticket, Wrench
} from 'lucide-react'; } from 'lucide-react';
import { useApp } from '../../context/AppContext'; import { useApp } from '../../context/AppContext';
import { AppAvatar } from '../shared/AppAvatar';
const navGroups = [ const navGroups = [
{ {
@@ -63,13 +64,13 @@ export function Sidebar() {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
return ( return (
<aside className={`sidebar-shell flex h-full flex-col transition-all duration-200 ${collapsed ? 'w-20' : 'w-72'}`}> <aside className={`bg-surface border-r border-border flex h-full flex-col transition-all duration-200 ${collapsed ? 'w-20' : 'w-60'}`}>
<div className={`flex items-center gap-3 px-4 pt-4 pb-3 ${collapsed ? 'justify-center' : ''}`}> <div className={`flex items-center gap-3 px-4 pt-4 pb-3 ${collapsed ? 'justify-center' : ''}`}>
<div className="brand-mark flex size-10 items-center justify-center rounded-2xl font-black text-white">P</div> <div className="bg-accent text-accent-foreground flex size-10 items-center justify-center rounded-2xl font-black">P</div>
{!collapsed && ( {!collapsed && (
<div className="min-w-0"> <div className="min-w-0">
<div className="text-base font-bold">Papo</div> <div className="text-base font-bold">Papo</div>
<div className="text-[10px] uppercase tracking-widest section-subtle">Dashboard</div> <div className="text-[10px] uppercase tracking-widest text-muted">Dashboard</div>
</div> </div>
)} )}
</div> </div>
@@ -102,7 +103,7 @@ export function Sidebar() {
{navGroups.map((group) => ( {navGroups.map((group) => (
<div key={group.label}> <div key={group.label}>
{!collapsed && ( {!collapsed && (
<div className="px-2 pb-1 text-[10px] font-semibold uppercase tracking-[0.18em] section-subtle"> <div className="px-2 pb-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted">
{group.label} {group.label}
</div> </div>
)} )}
@@ -112,17 +113,18 @@ export function Sidebar() {
.map((item) => { .map((item) => {
const isActive = section === item.key; const isActive = section === item.key;
return ( return (
<Tooltip key={item.key} content={collapsed ? item.label : ''} placement="right" offset={8}> <Tooltip key={item.key} isDisabled={!collapsed}>
<Button <Tooltip.Trigger>
className={`nav-button h-10 justify-start gap-3 px-3 font-medium ${isActive ? 'nav-button-active' : ''}`} <Button
radius="lg" className={`h-10 justify-start gap-3 px-3 font-medium ${isActive ? 'bg-accent-soft text-accent-soft-foreground' : 'text-muted'}`}
variant="light" variant="ghost"
size="sm" size="sm"
startContent={item.icon} onPress={() => setSection(item.key as any)}
onPress={() => setSection(item.key)} >
> {item.icon} {!collapsed && item.label}
{!collapsed && item.label} </Button>
</Button> </Tooltip.Trigger>
<Tooltip.Content placement="right" offset={8}>{item.label}</Tooltip.Content>
</Tooltip> </Tooltip>
); );
})} })}
@@ -132,21 +134,22 @@ export function Sidebar() {
{user?.isAdmin && ( {user?.isAdmin && (
<div> <div>
{!collapsed && ( {!collapsed && (
<div className="px-2 pb-1 text-[10px] font-semibold uppercase tracking-[0.18em] section-subtle"> <div className="px-2 pb-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted">
Admin Admin
</div> </div>
)} )}
<Tooltip content={collapsed ? 'Admin' : ''} placement="right" offset={8}> <Tooltip isDisabled={!collapsed}>
<Button <Tooltip.Trigger>
className={`nav-button h-10 justify-start gap-3 px-3 font-medium ${section === 'admin' ? 'nav-button-active' : ''}`} <Button
radius="lg" className={`h-10 justify-start gap-3 px-3 font-medium ${section === 'admin' ? 'bg-accent-soft text-accent-soft-foreground' : 'text-muted'}`}
variant="light" variant="ghost"
size="sm" size="sm"
startContent={<Wrench size={18} />} onPress={() => setSection('admin' as any)}
onPress={() => setSection('admin')} >
> <Wrench size={18} /> {!collapsed && 'Admin'}
{!collapsed && 'Admin'} </Button>
</Button> </Tooltip.Trigger>
<Tooltip.Content placement="right" offset={8}>Admin</Tooltip.Content>
</Tooltip> </Tooltip>
</div> </div>
)} )}
@@ -156,28 +159,30 @@ export function Sidebar() {
<div className="p-3 flex flex-col gap-2"> <div className="p-3 flex flex-col gap-2">
<Button <Button
isIconOnly isIconOnly
className="ghost-button w-full" className="w-full"
radius="lg"
size="sm" size="sm"
variant="light" variant="ghost"
onPress={() => setCollapsed((c) => !c)} onPress={() => setCollapsed((c) => !c)}
> >
{collapsed ? <PanelLeft size={16} /> : <PanelLeftClose size={16} />} {collapsed ? <PanelLeft size={16} /> : <PanelLeftClose size={16} />}
</Button> </Button>
<Card className="surface-card"> <Card>
<CardContent className={`flex items-center gap-3 p-2 ${collapsed ? 'justify-center' : ''}`}> <CardContent className={`flex items-center gap-3 p-2 ${collapsed ? 'justify-center' : ''}`}>
<Avatar name={user?.username} size="sm" className="shrink-0" /> <AppAvatar name={user?.username} size="sm" className="shrink-0" />
{!collapsed && ( {!collapsed && (
<> <>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="truncate text-xs font-semibold">{user?.username}</div> <div className="truncate text-xs font-semibold">{user?.username}</div>
<div className="text-[10px] section-subtle">Angemeldet</div> <div className="text-[10px] text-muted">Angemeldet</div>
</div> </div>
<Tooltip content="Abmelden" placement="top"> <Tooltip>
<Button isIconOnly radius="lg" size="sm" variant="light" className="ghost-button" onPress={handleLogout}> <Tooltip.Trigger>
<LogOut size={14} /> <Button isIconOnly size="sm" variant="ghost" onPress={handleLogout}>
</Button> <LogOut size={14} />
</Button>
</Tooltip.Trigger>
<Tooltip.Content placement="top">Abmelden</Tooltip.Content>
</Tooltip> </Tooltip>
</> </>
)} )}

View File

@@ -20,13 +20,13 @@ export function Topbar({ guildName, statusMessage, children }: Props) {
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold tracking-tight">{guildName}</h1> <h1 className="text-2xl font-bold tracking-tight">{guildName}</h1>
<p className="mt-1 text-small text-default-500">Bot-Dashboard Verwaltung</p> <p className="mt-1 text-sm text-muted">Bot-Dashboard Verwaltung</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{statusMessage && ( {statusMessage && (
<Chip color="warning" size="sm" variant="flat">{statusMessage}</Chip> <Chip color="warning" size="sm" variant="soft">{statusMessage}</Chip>
)} )}
<Button isIconOnly radius="lg" size="sm" variant="light" onPress={() => setDark((d) => !d)}> <Button isIconOnly size="sm" variant="ghost" onPress={() => setDark((d) => !d)}>
{dark ? <Sun size={16} /> : <Moon size={16} />} {dark ? <Sun size={16} /> : <Moon size={16} />}
</Button> </Button>
{children} {children}

View File

@@ -12,8 +12,8 @@ export function ActivityTile({ icon, label, value }: Props) {
<Card> <Card>
<CardContent className="flex flex-row items-center justify-between gap-4 p-4"> <CardContent className="flex flex-row items-center justify-between gap-4 p-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Chip color="primary" size="sm" variant="flat" startContent={icon}> <Chip color="accent" size="sm" variant="soft">
{label} {icon} {label}
</Chip> </Chip>
<div> <div>
<div className="text-2xl font-black">{value}</div> <div className="text-2xl font-black">{value}</div>

View File

@@ -0,0 +1,17 @@
import { Avatar } from '@heroui/react';
type Props = {
src?: string;
name?: string;
size?: 'sm' | 'md' | 'lg';
className?: string;
};
export function AppAvatar({ src, name, size = 'md', className }: Props) {
return (
<Avatar size={size} className={className}>
{src && <Avatar.Image src={src} alt={name || 'Avatar'} />}
<Avatar.Fallback>{name?.trim()?.[0]?.toUpperCase() || '?'}</Avatar.Fallback>
</Avatar>
);
}

View File

@@ -0,0 +1,29 @@
type Item = {
label: string;
value: number;
};
type Props = {
items: Item[];
};
export function BarComparisonChart({ items }: Props) {
const max = Math.max(1, ...items.map((i) => i.value));
return (
<div className="flex flex-col gap-3">
{items.map((item) => (
<div key={item.label} className="flex items-center gap-3">
<span className="w-24 shrink-0 truncate text-xs text-muted">{item.label}</span>
<div className="h-2 flex-1 rounded-full bg-default">
<div
className="h-2 rounded-r-full bg-accent"
style={{ width: `${Math.max(2, (item.value / max) * 100)}%` }}
/>
</div>
<span className="w-10 shrink-0 text-right text-xs font-semibold tabular-nums">{item.value}</span>
</div>
))}
</div>
);
}

View File

@@ -8,12 +8,12 @@ type Props = {
export function EmptyState({ message = 'Keine Daten vorhanden', icon }: Props) { export function EmptyState({ message = 'Keine Daten vorhanden', icon }: Props) {
return ( return (
<Card className="border border-default-100 bg-default-50/10"> <Card className="bg-surface-tertiary">
<CardContent className="flex flex-col items-center gap-3 py-8"> <CardContent className="flex flex-col items-center gap-3 py-8">
<div className="flex size-12 items-center justify-center rounded-full bg-default-100/50 text-default-400"> <div className="flex size-12 items-center justify-center rounded-full bg-default-soft text-muted">
{icon || <Inbox size={24} />} {icon || <Inbox size={24} />}
</div> </div>
<p className="text-small text-default-400">{message}</p> <p className="text-sm text-muted">{message}</p>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@@ -8,14 +8,14 @@ type Props = {
export function ErrorState({ message = 'Ein Fehler ist aufgetreten', onRetry }: Props) { export function ErrorState({ message = 'Ein Fehler ist aufgetreten', onRetry }: Props) {
return ( return (
<Card className="border border-danger-100/30 bg-danger-50/10"> <Card className="border border-danger-soft bg-danger-soft/40">
<CardContent className="flex flex-col items-center gap-3 py-8"> <CardContent className="flex flex-col items-center gap-3 py-8">
<div className="flex size-12 items-center justify-center rounded-full bg-danger-500/10 text-danger-400"> <div className="flex size-12 items-center justify-center rounded-full bg-danger-soft text-danger">
<AlertTriangle size={24} /> <AlertTriangle size={24} />
</div> </div>
<p className="text-small text-default-500">{message}</p> <p className="text-sm text-muted">{message}</p>
{onRetry && ( {onRetry && (
<button className="flex items-center gap-1 text-tiny text-primary-400 hover:text-primary-300 transition-colors" onClick={onRetry}> <button className="flex items-center gap-1 text-xs text-accent hover:opacity-80 transition-opacity" onClick={onRetry}>
<RefreshCw size={12} /> Erneut versuchen <RefreshCw size={12} /> Erneut versuchen
</button> </button>
)} )}

View File

@@ -9,7 +9,7 @@ type Props = {
export function ListPanel({ title, children, className }: Props) { export function ListPanel({ title, children, className }: Props) {
return ( return (
<Card className={className ?? ''}> <Card className={className}>
<CardHeader> <CardHeader>
<CardTitle>{title}</CardTitle> <CardTitle>{title}</CardTitle>
</CardHeader> </CardHeader>

View File

@@ -1,4 +1,4 @@
import { Card, CardContent } from '@heroui/react'; import { Card, CardContent, Skeleton } from '@heroui/react';
type Props = { type Props = {
lines?: number; lines?: number;
@@ -8,10 +8,10 @@ export function LoadingSkeleton({ lines = 3 }: Props) {
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{Array.from({ length: lines }).map((_, i) => ( {Array.from({ length: lines }).map((_, i) => (
<Card key={i} className="animate-pulse border border-default-100 bg-default-50/30"> <Card key={i}>
<CardContent className="p-5"> <CardContent className="gap-2 p-5">
<div className="h-4 w-3/4 rounded-lg bg-default-200/50" /> <Skeleton className="h-4 w-3/4 rounded-lg" />
{i < 2 && <div className="mt-3 h-3 w-1/2 rounded-lg bg-default-200/30" />} {i < 2 && <Skeleton className="mt-3 h-3 w-1/2 rounded-lg" />}
</CardContent> </CardContent>
</Card> </Card>
))} ))}

View File

@@ -10,11 +10,11 @@ type Props = {
export function SectionCard({ title, subtitle, children, action }: Props) { export function SectionCard({ title, subtitle, children, action }: Props) {
return ( return (
<Card className="surface-card-strong"> <Card className="bg-surface-secondary">
<CardHeader className="flex items-start justify-between gap-4"> <CardHeader className="flex items-start justify-between gap-4">
<div className="min-w-0 flex flex-col gap-1"> <div className="min-w-0 flex flex-col gap-1">
<CardTitle>{title}</CardTitle> <CardTitle>{title}</CardTitle>
{subtitle && <CardDescription className="section-subtle">{subtitle}</CardDescription>} {subtitle && <CardDescription>{subtitle}</CardDescription>}
</div> </div>
{action && <div className="shrink-0">{action}</div>} {action && <div className="shrink-0">{action}</div>}
</CardHeader> </CardHeader>

View File

@@ -1,4 +1,4 @@
import { Card, CardContent, Chip } from '@heroui/react'; import { Card, CardContent } from '@heroui/react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
type Props = { type Props = {
@@ -6,27 +6,33 @@ type Props = {
label: string; label: string;
value: string | number; value: string | number;
trend?: string; trend?: string;
color?: 'primary' | 'success' | 'warning' | 'danger' | 'default'; color?: 'accent' | 'success' | 'warning' | 'danger' | 'default';
}; };
export function StatCard({ icon, label, value, trend, color = 'primary' }: Props) { const iconClasses: Record<NonNullable<Props['color']>, string> = {
const chipColor = color === 'default' ? 'default' : color; accent: 'bg-accent-soft text-accent-soft-foreground',
success: 'bg-success-soft text-success-soft-foreground',
warning: 'bg-warning-soft text-warning-soft-foreground',
danger: 'bg-danger-soft text-danger-soft-foreground',
default: 'bg-default-soft text-muted',
};
export function StatCard({ icon, label, value, trend, color = 'default' }: Props) {
return ( return (
<Card className="surface-card"> <Card>
<CardContent className="flex flex-col gap-2 p-4"> <CardContent className="flex flex-row items-center gap-3 p-3">
<div className="flex items-center justify-between"> <div className={`flex size-8 shrink-0 items-center justify-center rounded-lg ${iconClasses[color]}`}>
<Chip color={chipColor} size="sm" variant="flat" startContent={icon} className={color === 'primary' ? 'accent-chip' : undefined}> {icon}
{label}
</Chip>
{trend && (
<span className={`text-tiny font-medium ${trend.startsWith('+') ? 'text-success-400' : trend.startsWith('-') ? 'text-danger-400' : 'text-default-400'}`}>
{trend}
</span>
)}
</div> </div>
<div className="text-2xl font-bold tracking-tight">{value}</div> <div className="min-w-0 flex-1">
<div className="text-tiny section-subtle">{label}</div> <div className="truncate text-xs text-muted">{label}</div>
<div className="text-lg font-bold leading-tight tracking-tight">{value}</div>
</div>
{trend && (
<span className={`shrink-0 text-xs font-medium ${trend.startsWith('+') ? 'text-success' : trend.startsWith('-') ? 'text-danger' : 'text-muted'}`}>
{trend}
</span>
)}
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@@ -10,55 +10,55 @@ export function Admin() {
if (!user?.isAdmin) return null; if (!user?.isAdmin) return null;
return ( return (
<SectionCard title="Admin" subtitle="Bot-weite <EFBFBD>bersichten"> <SectionCard title="Admin" subtitle="Bot-weite Übersichten">
<div className="grid gap-5 xl:grid-cols-2"> <div className="grid gap-5 xl:grid-cols-2">
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Bot Overview</h3> <h3 className="text-base font-semibold">Bot Overview</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-3 p-5"> <CardContent className="flex flex-col gap-3 p-5">
<div className="flex items-center justify-between rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-3 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Server size={14} className="text-primary-400" /> <Server size={14} className="text-accent" />
<span className="text-default-500">Guilds</span> <span className="text-muted">Guilds</span>
</div> </div>
<span className="font-semibold">{admin.overview?.guilds ?? '-'}</span> <span className="font-semibold">{admin.overview?.guilds ?? '-'}</span>
</div> </div>
<div className="flex items-center justify-between rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-3 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Activity size={14} className="text-success-400" /> <Activity size={14} className="text-success" />
<span className="text-default-500">Aktive Guilds (24h)</span> <span className="text-muted">Aktive Guilds (24h)</span>
</div> </div>
<span className="font-semibold">{admin.overview?.activeGuilds ?? '-'}</span> <span className="font-semibold">{admin.overview?.activeGuilds ?? '-'}</span>
</div> </div>
<div className="flex items-center justify-between rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-3 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock size={14} className="text-warning-400" /> <Clock size={14} className="text-warning" />
<span className="text-default-500">Uptime</span> <span className="text-muted">Uptime</span>
</div> </div>
<span className="font-semibold">{admin.overview?.uptime ?? '-'}</span> <span className="font-semibold">{admin.overview?.uptime ?? '-'}</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="flex items-center justify-between px-5 pt-5 pb-0"> <CardHeader className="flex items-center justify-between px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Letzte Admin Logs</h3> <h3 className="text-base font-semibold">Letzte Admin Logs</h3>
<Chip size="sm" variant="flat" color="warning"> <Chip size="sm" variant="soft" color="warning">
{(admin.logs || []).length} Eintr<EFBFBD>ge {(admin.logs || []).length} Einträge
</Chip> </Chip>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-2 p-5"> <CardContent className="flex flex-col gap-2 p-5">
{(admin.logs || []).length ? (admin.logs || []).slice(0, 20).map((log, i) => ( {(admin.logs || []).length ? (admin.logs || []).slice(0, 20).map((log, i) => (
<div key={i} className="flex items-start gap-3 rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div key={i} className="bg-surface-tertiary flex items-start gap-3 rounded-xl px-4 py-3 text-sm">
<Terminal size={14} className="mt-0.5 text-default-400 shrink-0" /> <Terminal size={14} className="mt-0.5 text-muted shrink-0" />
<div className="min-w-0"> <div className="min-w-0">
<p className="text-default-500">{log.message || '-'}</p> <p className="text-muted">{log.message || '-'}</p>
<p className="text-tiny text-default-400 mt-0.5">{formatDate(log.timestamp)}</p> <p className="text-xs text-muted mt-0.5">{formatDate(log.timestamp)}</p>
</div> </div>
</div> </div>
)) : ( )) : (
<div className="flex flex-col items-center gap-2 py-4 text-center text-tiny text-default-400"> <div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
<Terminal size={20} /> <Terminal size={20} />
Keine Logs Keine Logs
</div> </div>

View File

@@ -9,7 +9,7 @@ export function Automod() {
return ( return (
<SectionCard title="Automod" subtitle="Filter, Logging und Sicherheit"> <SectionCard title="Automod" subtitle="Filter, Logging und Sicherheit">
<div className="grid gap-5 xl:grid-cols-2"> <div className="grid gap-5 xl:grid-cols-2">
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Filter konfigurieren</h3> <h3 className="text-base font-semibold">Filter konfigurieren</h3>
</CardHeader> </CardHeader>
@@ -35,7 +35,7 @@ export function Automod() {
<TextField> <TextField>
<Label>Log Channel ID</Label> <Label>Log Channel ID</Label>
<Input <Input
placeholder="Channel ID f<EFBFBD>r Logs" placeholder="Channel ID für Logs"
value={settings.automodConfig?.logChannelId || ''} value={settings.automodConfig?.logChannelId || ''}
onChange={(e) => setSettings((s) => ({ ...s, automodConfig: { ...(s.automodConfig || {}), logChannelId: e.target.value } }))} onChange={(e) => setSettings((s) => ({ ...s, automodConfig: { ...(s.automodConfig || {}), logChannelId: e.target.value } }))}
/> />
@@ -52,29 +52,29 @@ export function Automod() {
<Separator /> <Separator />
<Button color="primary" startContent={<Save size={16} />} onPress={() => saveSettingsPayload({ automodEnabled: settings.automodEnabled !== false, automodConfig: settings.automodConfig || {} }, 'Automod gespeichert')}> <Button variant="primary" onPress={() => saveSettingsPayload({ automodEnabled: settings.automodEnabled !== false, automodConfig: settings.automodConfig || {} }, 'Automod gespeichert')}>
Speichern <Save size={16} /> Speichern
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Info</h3> <h3 className="text-base font-semibold">Info</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-3 p-5"> <CardContent className="flex flex-col gap-3 p-5">
<div className="rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div className="bg-surface-tertiary rounded-xl px-4 py-3 text-sm">
<p className="text-default-500">Die Automod-Einstellungen werden nach dem Speichern sofort aktiv.</p> <p className="text-muted">Die Automod-Einstellungen werden nach dem Speichern sofort aktiv.</p>
</div> </div>
<div className="rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div className="bg-surface-tertiary rounded-xl px-4 py-3 text-sm">
<p className="text-default-500">Bad-Word-Filter entfernt Nachrichten mit unerw<EFBFBD>nschten Begriffen.</p> <p className="text-muted">Bad-Word-Filter entfernt Nachrichten mit unerwünschten Begriffen.</p>
</div> </div>
<div className="rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div className="bg-surface-tertiary rounded-xl px-4 py-3 text-sm">
<p className="text-default-500">Link-Filter blockiert bekannte sch<EFBFBD>dliche Domains und nicht-whitelistete Links.</p> <p className="text-muted">Link-Filter blockiert bekannte schädliche Domains und nicht-whitelistete Links.</p>
</div> </div>
<div className="rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div className="bg-surface-tertiary rounded-xl px-4 py-3 text-sm">
<p className="text-default-500">Spam-Filter erkennt und unterdr<EFBFBD>ckt Mehrfachnachrichten in kurzer Zeit.</p> <p className="text-muted">Spam-Filter erkennt und unterdrückt Mehrfachnachrichten in kurzer Zeit.</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -1,4 +1,4 @@
import { Card, CardContent, CardHeader, Input, TextArea, Button, Chip, Switch, Separator, TextField, Label } from '@heroui/react'; import { Card, CardContent, CardHeader, Input, InputGroup, TextArea, Button, Chip, Switch, Separator, TextField, Label } from '@heroui/react';
import { CalendarDays, Save, Cake, Clock } from 'lucide-react'; 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';
@@ -7,9 +7,9 @@ export function Birthday() {
const { birthday, setBirthday, saveBirthday } = useApp(); const { birthday, setBirthday, saveBirthday } = useApp();
return ( return (
<SectionCard title="Birthday" subtitle="Geburtstags-Feature und gespeicherte Eintr<EFBFBD>ge"> <SectionCard title="Birthday" subtitle="Geburtstags-Feature und gespeicherte Einträge">
<div className="grid gap-5 xl:grid-cols-2"> <div className="grid gap-5 xl:grid-cols-2">
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Konfiguration</h3> <h3 className="text-base font-semibold">Konfiguration</h3>
</CardHeader> </CardHeader>
@@ -21,7 +21,7 @@ export function Birthday() {
<TextField> <TextField>
<Label>Channel ID</Label> <Label>Channel ID</Label>
<Input <Input
placeholder="Channel f<EFBFBD>r Geburtstagsnachrichten" placeholder="Channel für Geburtstagsnachrichten"
value={birthday.config?.channelId || ''} value={birthday.config?.channelId || ''}
onChange={(e) => setBirthday((s) => ({ ...s, config: { ...s.config, channelId: e.target.value } }))} onChange={(e) => setBirthday((s) => ({ ...s, config: { ...s.config, channelId: e.target.value } }))}
/> />
@@ -29,14 +29,16 @@ export function Birthday() {
<TextField> <TextField>
<Label>Sendezeit (Stunde)</Label> <Label>Sendezeit (Stunde)</Label>
<Input <InputGroup>
type="number" <InputGroup.Prefix><Clock size={16} className="text-muted" /></InputGroup.Prefix>
min="0" <InputGroup.Input
max="23" type="number"
value={String(birthday.config?.sendHour ?? 9)} min="0"
onChange={(e) => setBirthday((s) => ({ ...s, config: { ...s.config, sendHour: Number(e.target.value || 0) } }))} max="23"
startContent={<Clock size={16} className="text-default-400" />} value={String(birthday.config?.sendHour ?? 9)}
/> onChange={(e) => setBirthday((s) => ({ ...s, config: { ...s.config, sendHour: Number(e.target.value || 0) } }))}
/>
</InputGroup>
</TextField> </TextField>
<TextField> <TextField>
@@ -50,29 +52,29 @@ export function Birthday() {
<Separator /> <Separator />
<Button color="primary" startContent={<Save size={16} />} onPress={saveBirthday}>Speichern</Button> <Button variant="primary" onPress={saveBirthday}><Save size={16} /> Speichern</Button>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Gespeicherte Geburtstage ({(birthday.birthdays || []).length})</h3> <h3 className="text-base font-semibold">Gespeicherte Geburtstage ({(birthday.birthdays || []).length})</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-2 p-5"> <CardContent className="flex flex-col gap-2 p-5">
{(birthday.birthdays || []).length ? (birthday.birthdays || []).map((entry, i) => ( {(birthday.birthdays || []).length ? (birthday.birthdays || []).map((entry, i) => (
<div key={i} className="flex items-center justify-between rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div key={i} className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-3 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Cake size={14} className="text-primary-400" /> <Cake size={14} className="text-accent" />
<span className="font-medium">{entry.userId}</span> <span className="font-medium">{entry.userId}</span>
</div> </div>
<Chip size="sm" variant="flat" color="primary"> <Chip size="sm" variant="soft" color="accent">
{String(entry.birthDate || '').replace(/^--/, '')} {String(entry.birthDate || '').replace(/^--/, '')}
</Chip> </Chip>
</div> </div>
)) : ( )) : (
<div className="flex flex-col items-center gap-2 py-4 text-center text-tiny text-default-400"> <div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
<CalendarDays size={20} /> <CalendarDays size={20} />
Keine Eintr<EFBFBD>ge Keine Einträge
</div> </div>
)} )}
</CardContent> </CardContent>

View File

@@ -1,4 +1,4 @@
import { Card, CardContent, CardHeader, Avatar, Chip, Button, ScrollShadow } from '@heroui/react'; import { Card, CardContent, CardHeader, Chip, Button, ScrollShadow } from '@heroui/react';
import { import {
Bot, CalendarDays, Users, Ticket, Shield, MessageSquare, Bot, CalendarDays, Users, Ticket, Shield, MessageSquare,
ChevronRight, Activity, Clock, ArrowUpRight, RefreshCw, Send, ChevronRight, Activity, Clock, ArrowUpRight, RefreshCw, Send,
@@ -7,6 +7,8 @@ import {
import { useApp } from '../context/AppContext'; import { useApp } from '../context/AppContext';
import { formatDate, guildIconUrl } from '../utils/formatters'; import { formatDate, guildIconUrl } from '../utils/formatters';
import { StatCard } from '../components/shared/StatCard'; import { StatCard } from '../components/shared/StatCard';
import { AppAvatar } from '../components/shared/AppAvatar';
import { BarComparisonChart } from '../components/shared/BarComparisonChart';
export function Dashboard() { export function Dashboard() {
const { guildInfo, guilds, currentGuildId, overview, activity, logs, setSection } = useApp(); const { guildInfo, guilds, currentGuildId, overview, activity, logs, setSection } = useApp();
@@ -14,29 +16,29 @@ export function Dashboard() {
const moduleFlags = guildInfo?.modules || {}; const moduleFlags = guildInfo?.modules || {};
const quickActions = [ const quickActions = [
{ key: 'tickets', label: 'Ticket Panel senden', icon: <Send size={16} />, color: 'primary' as const }, { key: 'tickets', label: 'Ticket Panel senden', icon: <Send size={16} /> },
{ key: 'serverstats', label: 'Sync starten', icon: <RefreshCw size={16} />, color: 'success' as const }, { key: 'serverstats', label: 'Sync starten', icon: <RefreshCw size={16} /> },
{ key: 'modules', label: 'Module aktualisieren', icon: <Zap size={16} />, color: 'warning' as const }, { key: 'modules', label: 'Module aktualisieren', icon: <Zap size={16} /> },
{ key: 'settings', label: 'Einstellungen oeffnen', icon: <Settings size={16} />, color: 'default' as const }, { key: 'settings', label: 'Einstellungen oeffnen', icon: <Settings size={16} /> },
]; ];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Card className="surface-card-strong"> <Card className="bg-surface-secondary">
<CardContent className="flex flex-col gap-6 xl:flex-row xl:items-center xl:justify-between"> <CardContent className="flex flex-col gap-6 xl:flex-row xl:items-center xl:justify-between">
<div className="flex min-w-0 items-center gap-5"> <div className="flex min-w-0 items-center gap-5">
<Avatar className="size-20 shrink-0" radius="lg" src={guildIconUrl(selectedGuild)} /> <AppAvatar className="size-20 shrink-0" size="lg" src={guildIconUrl(selectedGuild)} name={guildInfo?.name || selectedGuild?.name} />
<div className="min-w-0"> <div className="min-w-0">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h1 className="truncate text-3xl font-black tracking-tight">{guildInfo?.name || selectedGuild?.name}</h1> <h1 className="truncate text-3xl font-black tracking-tight">{guildInfo?.name || selectedGuild?.name}</h1>
<Chip color="success" radius="sm" size="sm" startContent={<Bot size={12} />} variant="dot"> <Chip color="success" size="sm" variant="soft">
Online <Bot size={12} /> Online
</Chip> </Chip>
</div> </div>
<div className="mt-1 text-small section-subtle">ID: {guildInfo?.id || selectedGuild?.id}</div> <div className="mt-1 text-sm text-muted">ID: {guildInfo?.id || selectedGuild?.id}</div>
<div className="mt-3 flex flex-wrap gap-2"> <div className="mt-3 flex flex-wrap gap-2">
{Object.entries(moduleFlags).filter(([, v]) => v).map(([key]) => ( {Object.entries(moduleFlags).filter(([, v]) => v).map(([key]) => (
<Chip key={key} size="sm" variant="flat" className="accent-chip"> <Chip key={key} size="sm" variant="soft" color="accent">
{key.replace('Enabled', '').replace(/([A-Z])/g, ' $1').trim()} {key.replace('Enabled', '').replace(/([A-Z])/g, ' $1').trim()}
</Chip> </Chip>
))} ))}
@@ -44,8 +46,8 @@ export function Dashboard() {
</div> </div>
</div> </div>
<div className="flex gap-2 shrink-0"> <div className="flex gap-2 shrink-0">
<Chip radius="sm" size="sm" variant="flat" startContent={<Gauge size={12} />} className="accent-chip"> <Chip size="sm" variant="soft" color="accent">
Ping: {guildInfo?.ping || '-'}ms <Gauge size={12} /> Ping: {guildInfo?.ping || '-'}ms
</Chip> </Chip>
</div> </div>
</CardContent> </CardContent>
@@ -65,40 +67,44 @@ export function Dashboard() {
</div> </div>
<div className="grid gap-5 xl:grid-cols-2 2xl:grid-cols-3"> <div className="grid gap-5 xl:grid-cols-2 2xl:grid-cols-3">
<Card className="surface-card"> <Card>
<CardHeader> <CardHeader>
<div> <div>
<h2 className="text-lg font-bold">Activity Bereich</h2> <h2 className="text-lg font-bold">Activity Bereich</h2>
<p className="mt-0.5 text-tiny text-default-400">Live Statistiken</p> <p className="mt-0.5 text-xs text-muted">Live Statistiken</p>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="grid grid-cols-1 gap-3 sm:grid-cols-2"> <CardContent>
<StatCard icon={<MessageSquare size={16} />} label="Nachrichten" value={activity?.messages24h ?? 0} /> <BarComparisonChart
<StatCard icon={<Command size={16} />} label="Commands" value={activity?.commands24h ?? 0} color="success" /> items={[
<StatCard icon={<Shield size={16} />} label="Automod" value={activity?.automod24h ?? 0} color="warning" /> { label: 'Nachrichten', value: activity?.messages24h ?? 0 },
<StatCard icon={<Users size={16} />} label="Neue User" value={activity?.newUsers24h ?? 0} /> { label: 'Commands', value: activity?.commands24h ?? 0 },
{ label: 'Automod', value: activity?.automod24h ?? 0 },
{ label: 'Neue User', value: activity?.newUsers24h ?? 0 },
]}
/>
</CardContent> </CardContent>
</Card> </Card>
<Card className="surface-card"> <Card>
<CardHeader className="flex items-center justify-between"> <CardHeader className="flex items-center justify-between">
<div> <div>
<h2 className="text-lg font-bold">Guild Logs</h2> <h2 className="text-lg font-bold">Guild Logs</h2>
<p className="mt-0.5 text-tiny text-default-400">Letzte Ereignisse</p> <p className="mt-0.5 text-xs text-muted">Letzte Ereignisse</p>
</div> </div>
<Button size="sm" variant="light" className="ghost-button" endContent={<ChevronRight size={14} />} onPress={() => setSection('settings')}> <Button size="sm" variant="ghost" onPress={() => setSection('settings')}>
Alle Alle <ChevronRight size={14} />
</Button> </Button>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ScrollShadow className="max-h-[320px] space-y-2 pr-1" hideScrollBar> <ScrollShadow className="max-h-[320px] space-y-2 pr-1" hideScrollBar>
{logs.length ? logs.slice(0, 15).map((log, i) => ( {logs.length ? logs.slice(0, 15).map((log, i) => (
<Card key={`${log.timestamp}-${i}`} className="surface-card-muted"> <Card key={`${log.timestamp}-${i}`} className="bg-surface-tertiary">
<CardContent className="flex items-start gap-3 p-3"> <CardContent className="flex items-start gap-3 p-3">
<div className={`mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-lg ${ <div className={`mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-lg ${
log.level === 'error' ? 'bg-danger-500/10 text-danger-400' : log.level === 'error' ? 'bg-danger-soft text-danger' :
log.level === 'warn' ? 'bg-warning-500/10 text-warning-400' : log.level === 'warn' ? 'bg-warning-soft text-warning' :
'bg-orange-500/10 text-orange-300' 'bg-accent-soft text-accent-soft-foreground'
}`}> }`}>
{log.level === 'error' ? <Shield size={12} /> : {log.level === 'error' ? <Shield size={12} /> :
log.level === 'warn' ? <Bell size={12} /> : log.level === 'warn' ? <Bell size={12} /> :
@@ -109,20 +115,20 @@ export function Dashboard() {
<Chip <Chip
color={log.level === 'error' ? 'danger' : log.level === 'warn' ? 'warning' : 'default'} color={log.level === 'error' ? 'danger' : log.level === 'warn' ? 'warning' : 'default'}
size="sm" size="sm"
variant="flat" variant="soft"
> >
{(log.level || 'info').toUpperCase()} {(log.level || 'info').toUpperCase()}
</Chip> </Chip>
<span className="text-tiny text-default-400">{formatDate(log.timestamp)}</span> <span className="text-xs text-muted">{formatDate(log.timestamp)}</span>
</div> </div>
<p className="mt-1 text-small text-foreground/80"> <p className="mt-1 text-sm text-foreground/80">
{log.category ? <span className="text-default-500">[{log.category}] </span> : ''}{log.message || '-'} {log.category ? <span className="text-muted">[{log.category}] </span> : ''}{log.message || '-'}
</p> </p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
)) : ( )) : (
<div className="flex flex-col items-center gap-2 py-6 text-center text-small text-default-400"> <div className="flex flex-col items-center gap-2 py-6 text-center text-sm text-muted">
<Activity size={20} /> <Activity size={20} />
Keine Logs Keine Logs
</div> </div>
@@ -131,22 +137,20 @@ export function Dashboard() {
</CardContent> </CardContent>
</Card> </Card>
<Card className="surface-card"> <Card>
<CardHeader> <CardHeader>
<h2 className="text-lg font-bold">Quick Actions</h2> <h2 className="text-lg font-bold">Quick Actions</h2>
<p className="mt-0.5 text-tiny text-default-400">Schnellzugriff</p> <p className="mt-0.5 text-xs text-muted">Schnellzugriff</p>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-2"> <CardContent className="flex flex-col gap-2">
{quickActions.map((action) => ( {quickActions.map((action) => (
<Button <Button
key={action.key} key={action.key}
color="default" variant="tertiary"
startContent={action.icon} className="justify-start font-medium"
variant="light"
className={`justify-start font-medium ${action.color === 'default' ? 'ghost-button' : 'accent-button'}`}
onPress={() => setSection(action.key as any)} onPress={() => setSection(action.key as any)}
> >
{action.label} {action.icon} {action.label}
</Button> </Button>
))} ))}
</CardContent> </CardContent>
@@ -157,13 +161,20 @@ export function Dashboard() {
{(['tickets', 'supportlogin', 'automod', 'welcome', 'birthday', 'reactionroles'] as const).map((key) => { {(['tickets', 'supportlogin', 'automod', 'welcome', 'birthday', 'reactionroles'] as const).map((key) => {
const item = navItemMap[key]; const item = navItemMap[key];
return ( return (
<Card key={key} isPressable onPress={() => setSection(key)} className="surface-card"> <Card
key={key}
role="button"
tabIndex={0}
onClick={() => setSection(key)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setSection(key); }}
className="cursor-pointer border border-transparent transition-colors hover:border-accent"
>
<CardContent className="flex flex-col items-start gap-2"> <CardContent className="flex flex-col items-start gap-2">
<Chip size="sm" variant="flat" startContent={item.icon} className="accent-chip"> <Chip size="sm" variant="soft" color="accent">
{item.label} {item.icon} {item.label}
</Chip> </Chip>
<div className="font-semibold text-sm">{item.label}</div> <div className="font-semibold text-sm">{item.label}</div>
<div className="text-tiny text-default-400">Modul verwalten</div> <div className="text-xs text-muted">Modul verwalten</div>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@@ -9,7 +9,7 @@ export function DynamicVoice() {
return ( return (
<SectionCard title="Dynamic Voice" subtitle="Voice-Lobby, Template und Limits"> <SectionCard title="Dynamic Voice" subtitle="Voice-Lobby, Template und Limits">
<div className="grid gap-5 xl:grid-cols-2"> <div className="grid gap-5 xl:grid-cols-2">
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Konfiguration</h3> <h3 className="text-base font-semibold">Konfiguration</h3>
</CardHeader> </CardHeader>
@@ -30,7 +30,7 @@ export function DynamicVoice() {
<TextField> <TextField>
<Label>Kategorie ID</Label> <Label>Kategorie ID</Label>
<Input <Input
placeholder="Kategorie f<EFBFBD>r neue Channels" placeholder="Kategorie für neue Channels"
value={settings.dynamicVoiceConfig?.categoryId || ''} value={settings.dynamicVoiceConfig?.categoryId || ''}
onChange={(e) => setSettings((s) => ({ ...s, dynamicVoiceConfig: { ...(s.dynamicVoiceConfig || {}), categoryId: e.target.value } }))} onChange={(e) => setSettings((s) => ({ ...s, dynamicVoiceConfig: { ...(s.dynamicVoiceConfig || {}), categoryId: e.target.value } }))}
/> />
@@ -47,25 +47,25 @@ export function DynamicVoice() {
<Separator /> <Separator />
<Button color="primary" startContent={<Save size={16} />} onPress={() => saveSettingsPayload({ dynamicVoiceConfig: settings.dynamicVoiceConfig || {} }, 'Dynamic Voice gespeichert')}> <Button variant="primary" onPress={() => saveSettingsPayload({ dynamicVoiceConfig: settings.dynamicVoiceConfig || {} }, 'Dynamic Voice gespeichert')}>
Speichern <Save size={16} /> Speichern
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Info</h3> <h3 className="text-base font-semibold">Info</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-3 p-5"> <CardContent className="flex flex-col gap-3 p-5">
<div className="flex items-center gap-3 rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div className="bg-surface-tertiary flex items-center gap-3 rounded-xl px-4 py-3 text-sm">
<Mic size={16} className="text-primary-400" /> <Mic size={16} className="text-accent" />
<span className="text-default-500">Benutzer erstellen eigene Voice-Channels durch Beitreten der Lobby</span> <span className="text-muted">Benutzer erstellen eigene Voice-Channels durch Beitreten der Lobby</span>
</div> </div>
<div className="flex items-center gap-3 rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div className="bg-surface-tertiary flex items-center gap-3 rounded-xl px-4 py-3 text-sm">
<Users size={16} className="text-success-400" /> <Users size={16} className="text-success" />
<span className="text-default-500">Channel-Owner k<EFBFBD>nnen Limits und Berechtigungen verwalten</span> <span className="text-muted">Channel-Owner können Limits und Berechtigungen verwalten</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -14,26 +14,26 @@ export function Events() {
<h3 className="mb-3 text-base font-semibold">Bestehende Events ({(events || []).length})</h3> <h3 className="mb-3 text-base font-semibold">Bestehende Events ({(events || []).length})</h3>
<div className="space-y-3"> <div className="space-y-3">
{(events || []).length ? (events || []).map((event) => ( {(events || []).length ? (events || []).map((event) => (
<Card key={event.id} className="border border-default-100 bg-default-50/20"> <Card key={event.id}>
<CardContent className="flex flex-col gap-3 p-4"> <CardContent className="flex flex-col gap-3 p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<CalendarDays size={16} className="text-primary-400 shrink-0" /> <CalendarDays size={16} className="text-accent shrink-0" />
<span className="font-semibold text-small truncate">{event.title}</span> <span className="font-semibold text-sm truncate">{event.title}</span>
</div> </div>
<Button color="danger" size="sm" variant="flat" startContent={<Trash2 size={14} />} onPress={() => deleteEvent(event.id)}> <Button size="sm" variant="danger-soft" onPress={() => deleteEvent(event.id)}>
L<EFBFBD>schen <Trash2 size={14} /> schen
</Button> </Button>
</div> </div>
<p className="text-small text-default-400">{event.description || 'Keine Beschreibung'}</p> <p className="text-sm text-muted">{event.description || 'Keine Beschreibung'}</p>
<div className="flex items-center gap-2 text-tiny text-default-500"> <div className="flex items-center gap-2 text-xs text-muted">
<Clock size={12} /> <Clock size={12} />
{formatDate(event.startsAt)} {formatDate(event.startsAt)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
)) : ( )) : (
<div className="flex flex-col items-center gap-2 py-8 text-center text-small text-default-400"> <div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
<CalendarDays size={24} /> <CalendarDays size={24} />
Keine Events Keine Events
</div> </div>
@@ -41,7 +41,7 @@ export function Events() {
</div> </div>
</div> </div>
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Neues Event</h3> <h3 className="text-base font-semibold">Neues Event</h3>
</CardHeader> </CardHeader>
@@ -67,7 +67,7 @@ export function Events() {
<TextField> <TextField>
<Label>Channel ID</Label> <Label>Channel ID</Label>
<Input <Input
placeholder="Channel f<EFBFBD>r Erinnerungen" placeholder="Channel für Erinnerungen"
value={eventDraft.channelId} value={eventDraft.channelId}
onChange={(e) => setEventDraft((s) => ({ ...s, channelId: e.target.value }))} onChange={(e) => setEventDraft((s) => ({ ...s, channelId: e.target.value }))}
/> />
@@ -83,8 +83,8 @@ export function Events() {
/> />
</TextField> </TextField>
<Button color="primary" startContent={<Plus size={16} />} onPress={saveEvent}> <Button variant="primary" onPress={saveEvent}>
Event speichern <Plus size={16} /> Event speichern
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -1,6 +1,7 @@
import { Card, CardContent, Avatar, Button } from '@heroui/react'; import { Card, CardContent } from '@heroui/react';
import { useApp } from '../context/AppContext'; import { useApp } from '../context/AppContext';
import { guildIconUrl } from '../utils/formatters'; import { guildIconUrl } from '../utils/formatters';
import { AppAvatar } from '../components/shared/AppAvatar';
export function GuildSelect() { export function GuildSelect() {
const { guilds, setCurrentGuildId } = useApp(); const { guilds, setCurrentGuildId } = useApp();
@@ -8,26 +9,28 @@ export function GuildSelect() {
return ( return (
<div className="mx-auto flex min-h-screen max-w-6xl flex-col items-center justify-center px-6 py-12"> <div className="mx-auto flex min-h-screen max-w-6xl flex-col items-center justify-center px-6 py-12">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary-400 to-primary-600 text-2xl font-black text-white shadow-lg shadow-primary-500/25"> <div className="bg-accent text-accent-foreground mx-auto mb-4 flex size-16 items-center justify-center rounded-2xl text-2xl font-black">
P P
</div> </div>
<h1 className="text-3xl font-bold tracking-tight">Wähle einen Server</h1> <h1 className="text-3xl font-bold tracking-tight">Wähle einen Server</h1>
<p className="mt-2 text-default-500">Wähle einen Discord-Server aus, um das Dashboard zu öffnen.</p> <p className="mt-2 text-muted">Wähle einen Discord-Server aus, um das Dashboard zu öffnen.</p>
</div> </div>
<div className="grid w-full gap-4 sm:grid-cols-2 xl:grid-cols-3"> <div className="grid w-full gap-4 sm:grid-cols-2 xl:grid-cols-3">
{guilds.map((guild) => ( {guilds.map((guild) => (
<Card <Card
key={guild.id} key={guild.id}
isPressable role="button"
className="border border-default-100 bg-default-50/20 transition-all hover:border-primary-300 hover:shadow-lg hover:shadow-primary-500/5" tabIndex={0}
onPress={() => setCurrentGuildId(guild.id)} className="cursor-pointer border border-transparent transition-colors hover:border-accent"
onClick={() => setCurrentGuildId(guild.id)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setCurrentGuildId(guild.id); }}
> >
<CardContent className="flex flex-row items-center gap-4 p-5"> <CardContent className="flex flex-row items-center gap-4 p-5">
<Avatar src={guildIconUrl(guild)} name={guild.name} radius="lg" size="lg" /> <AppAvatar src={guildIconUrl(guild)} name={guild.name} size="lg" />
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate text-lg font-semibold">{guild.name}</div> <div className="truncate text-lg font-semibold">{guild.name}</div>
<div className="mt-0.5 text-small text-default-400">ID: {guild.id}</div> <div className="mt-0.5 text-sm text-muted">ID: {guild.id}</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -15,20 +15,20 @@ export function ModulesPage() {
{activeModules.length > 0 && ( {activeModules.length > 0 && (
<div> <div>
<h3 className="mb-3 flex items-center gap-2 text-base font-semibold"> <h3 className="mb-3 flex items-center gap-2 text-base font-semibold">
<CheckCircle size={16} className="text-success-400" /> <CheckCircle size={16} className="text-success" />
Aktive Module ({activeModules.length}) Aktive Module ({activeModules.length})
</h3> </h3>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
{activeModules.map((module) => ( {activeModules.map((module) => (
<Card key={module.key} className="surface-card-strong"> <Card key={module.key} className="bg-surface-secondary">
<CardContent className="flex flex-row items-center justify-between gap-4 p-4"> <CardContent className="flex flex-row items-center justify-between gap-4 p-4">
<div className="min-w-0"> <div className="min-w-0">
<div className="font-semibold">{module.name}</div> <div className="font-semibold">{module.name}</div>
{module.description && ( {module.description && (
<div className="text-small text-default-400 truncate">{module.description}</div> <div className="text-sm text-muted truncate">{module.description}</div>
)} )}
</div> </div>
<Switch isSelected={module.enabled} color="warning" onChange={(v) => toggleModule(module.key, v)} /> <Switch isSelected={module.enabled} onChange={(v) => toggleModule(module.key, v)} />
</CardContent> </CardContent>
</Card> </Card>
))} ))}
@@ -39,20 +39,20 @@ export function ModulesPage() {
{inactiveModules.length > 0 && ( {inactiveModules.length > 0 && (
<div> <div>
<h3 className="mb-3 flex items-center gap-2 text-base font-semibold"> <h3 className="mb-3 flex items-center gap-2 text-base font-semibold">
<XCircle size={16} className="text-default-400" /> <XCircle size={16} className="text-muted" />
Deaktivierte Module ({inactiveModules.length}) Deaktivierte Module ({inactiveModules.length})
</h3> </h3>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
{inactiveModules.map((module) => ( {inactiveModules.map((module) => (
<Card key={module.key} className="surface-card"> <Card key={module.key}>
<CardContent className="flex flex-row items-center justify-between gap-4 p-4"> <CardContent className="flex flex-row items-center justify-between gap-4 p-4">
<div className="min-w-0"> <div className="min-w-0">
<div className="font-semibold">{module.name}</div> <div className="font-semibold">{module.name}</div>
{module.description && ( {module.description && (
<div className="text-small text-default-400 truncate">{module.description}</div> <div className="text-sm text-muted truncate">{module.description}</div>
)} )}
</div> </div>
<Switch isSelected={module.enabled} color="primary" onChange={(v) => toggleModule(module.key, v)} /> <Switch isSelected={module.enabled} onChange={(v) => toggleModule(module.key, v)} />
</CardContent> </CardContent>
</Card> </Card>
))} ))}
@@ -61,9 +61,9 @@ export function ModulesPage() {
)} )}
{modules.length === 0 && ( {modules.length === 0 && (
<div className="flex flex-col items-center gap-2 py-8 text-center text-small text-default-400"> <div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
<Puzzle size={24} /> <Puzzle size={24} />
Keine Module verf<EFBFBD>gbar Keine Module verfügbar
</div> </div>
)} )}
</div> </div>

View File

@@ -11,46 +11,46 @@ export function MusicPage() {
<SectionCard title="Musik-Status" subtitle="Aktuelle Wiedergabe und Queues pro Guild."> <SectionCard title="Musik-Status" subtitle="Aktuelle Wiedergabe und Queues pro Guild.">
<div className="space-y-5"> <div className="space-y-5">
<div className="grid gap-4 sm:grid-cols-3"> <div className="grid gap-4 sm:grid-cols-3">
<StatCard icon={<Music size={18} />} label="Aktive Guilds" value={musicStatus.activeGuilds} color="primary" /> <StatCard icon={<Music size={18} />} label="Aktive Guilds" value={musicStatus.activeGuilds} color="accent" />
<StatCard icon={<Play size={18} />} label="Aktive Sessions" value={musicStatus.sessions.length} color="success" /> <StatCard icon={<Play size={18} />} label="Aktive Sessions" value={musicStatus.sessions.length} color="success" />
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-base font-semibold">Sessions ({musicStatus.sessions.length})</h3> <h3 className="text-base font-semibold">Sessions ({musicStatus.sessions.length})</h3>
{musicStatus.sessions.length ? musicStatus.sessions.map((session) => ( {musicStatus.sessions.length ? musicStatus.sessions.map((session) => (
<Card key={session.guildId} className="border border-default-100 bg-default-50/20"> <Card key={session.guildId}>
<CardContent className="flex flex-col gap-3 p-4"> <CardContent className="flex flex-col gap-3 p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Music size={16} className="text-primary-400" /> <Music size={16} className="text-accent" />
<span className="font-semibold text-small">{session.guildId}</span> <span className="font-semibold text-sm">{session.guildId}</span>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Chip size="sm" variant="flat" startContent={<Repeat size={12} />}> <Chip size="sm" variant="soft">
{session.loop} <Repeat size={12} /> {session.loop}
</Chip> </Chip>
<Chip size="sm" variant="flat" startContent={<List size={12} />}> <Chip size="sm" variant="soft">
{session.queueLength} in Queue <List size={12} /> {session.queueLength} in Queue
</Chip> </Chip>
</div> </div>
</div> </div>
{session.nowPlaying ? ( {session.nowPlaying ? (
<div className="rounded-xl bg-default-100/50 px-4 py-3 text-small"> <div className="bg-surface-tertiary rounded-xl px-4 py-3 text-sm">
<span className="text-default-500">Jetzt l<EFBFBD>uft: </span> <span className="text-muted">Jetzt läuft: </span>
<a href={session.nowPlaying.url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-primary-400 hover:underline"> <a href={session.nowPlaying.url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-accent hover:underline">
{session.nowPlaying.title} {session.nowPlaying.title}
<ExternalLink size={12} /> <ExternalLink size={12} />
</a> </a>
</div> </div>
) : ( ) : (
<div className="rounded-xl bg-default-100/30 px-4 py-3 text-small text-default-400"> <div className="bg-surface-tertiary rounded-xl px-4 py-3 text-sm text-muted">
Keine aktive Wiedergabe Keine aktive Wiedergabe
</div> </div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
)) : ( )) : (
<div className="flex flex-col items-center gap-2 py-8 text-center text-small text-default-400"> <div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
<Music size={24} /> <Music size={24} />
Keine aktiven Musik-Sessions Keine aktiven Musik-Sessions
</div> </div>

View File

@@ -13,20 +13,20 @@ export function ReactionRoles() {
<h3 className="mb-3 text-base font-semibold">Bestehende Sets ({reactionRoles.length})</h3> <h3 className="mb-3 text-base font-semibold">Bestehende Sets ({reactionRoles.length})</h3>
<div className="space-y-3"> <div className="space-y-3">
{reactionRoles.length ? reactionRoles.map((set, i) => ( {reactionRoles.length ? reactionRoles.map((set, i) => (
<Card key={set.id || i} className="border border-default-100 bg-default-50/20"> <Card key={set.id || i}>
<CardContent className="flex items-center gap-3 p-4"> <CardContent className="flex items-center gap-3 p-4">
<div className="flex size-10 items-center justify-center rounded-xl bg-primary-500/10 text-primary-400"> <div className="flex size-10 items-center justify-center rounded-xl bg-accent-soft text-accent-soft-foreground">
<Tag size={18} /> <Tag size={18} />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="font-semibold text-small truncate">{set.title || 'Reaction Role'}</div> <div className="font-semibold text-sm truncate">{set.title || 'Reaction Role'}</div>
<div className="text-tiny text-default-400 truncate">Channel: {set.channelId || '-'}</div> <div className="text-xs text-muted truncate">Channel: {set.channelId || '-'}</div>
</div> </div>
<Chip size="sm" variant="flat">{(set.entries?.length || 0)} Eintr<EFBFBD>ge</Chip> <Chip size="sm" variant="soft">{(set.entries?.length || 0)} Einträge</Chip>
</CardContent> </CardContent>
</Card> </Card>
)) : ( )) : (
<div className="flex flex-col items-center gap-2 py-8 text-center text-small text-default-400"> <div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
<Tag size={24} /> <Tag size={24} />
Keine Sets Keine Sets
</div> </div>
@@ -34,7 +34,7 @@ export function ReactionRoles() {
</div> </div>
</div> </div>
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Neues Set</h3> <h3 className="text-base font-semibold">Neues Set</h3>
</CardHeader> </CardHeader>
@@ -51,27 +51,27 @@ export function ReactionRoles() {
<TextField> <TextField>
<Label>Channel ID</Label> <Label>Channel ID</Label>
<Input <Input
placeholder="Channel f<EFBFBD>r die Nachricht" placeholder="Channel für die Nachricht"
value={reactionDraft.channelId} value={reactionDraft.channelId}
onChange={(e) => setReactionDraft((s) => ({ ...s, channelId: e.target.value }))} onChange={(e) => setReactionDraft((s) => ({ ...s, channelId: e.target.value }))}
/> />
</TextField> </TextField>
<div> <TextField>
<label className="block text-small font-medium mb-1">Eintr<EFBFBD>ge</label> <Label>Einträge</Label>
<TextArea <TextArea
placeholder="Emoji | Role ID | Label&#10;:emoji: | 123456789 | Rolle 1&#10;:wave: | 987654321 | Rolle 2" placeholder="Emoji | Role ID | Label&#10;:emoji: | 123456789 | Rolle 1&#10;:wave: | 987654321 | Rolle 2"
minRows={6} rows={6}
value={reactionDraft.entries} value={reactionDraft.entries}
onChange={(e) => setReactionDraft((s) => ({ ...s, entries: e.target.value }))} onChange={(e) => setReactionDraft((s) => ({ ...s, entries: e.target.value }))}
/> />
<p className="mt-1 text-tiny text-default-400"> <p className="mt-1 text-xs text-muted">
Pro Zeile: Emoji | Role ID | Label (optional) | Beschreibung (optional) Pro Zeile: Emoji | Role ID | Label (optional) | Beschreibung (optional)
</p> </p>
</div> </TextField>
<Button color="primary" startContent={<Save size={16} />} onPress={saveReactionRole}> <Button variant="primary" onPress={saveReactionRole}>
Reaction Role speichern <Save size={16} /> Reaction Role speichern
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -12,10 +12,14 @@ export function Register() {
} = useApp(); } = useApp();
return ( return (
<SectionCard title="Registrierungsformulare" subtitle="Bewerbungs-Formulare und eingegangene Antr<EFBFBD>ge verwalten."> <SectionCard title="Registrierungsformulare" subtitle="Bewerbungs-Formulare und eingegangene Anträge verwalten.">
<Tabs aria-label="Register Tabs" color="primary" selectedKey={registerTab} variant="bordered" onSelectionChange={(key) => setRegisterTab(String(key))}> <Tabs selectedKey={registerTab} variant="primary" onSelectionChange={(key) => setRegisterTab(String(key))}>
<Tab key="forms">Formulare</Tab> <Tabs.ListContainer>
<Tab key="apps">Anträge</Tab> <Tabs.List aria-label="Register Tabs">
<Tab id="forms">Formulare</Tab>
<Tab id="apps">Anträge</Tab>
</Tabs.List>
</Tabs.ListContainer>
</Tabs> </Tabs>
{registerTab === 'forms' && ( {registerTab === 'forms' && (
@@ -24,15 +28,15 @@ export function Register() {
<h3 className="mb-3 text-base font-semibold">Bestehende Formulare ({registerForms.length})</h3> <h3 className="mb-3 text-base font-semibold">Bestehende Formulare ({registerForms.length})</h3>
<div className="space-y-3"> <div className="space-y-3">
{registerForms.length ? registerForms.map((f) => ( {registerForms.length ? registerForms.map((f) => (
<Card key={f.id} className="border border-default-100 bg-default-50/20"> <Card key={f.id}>
<CardContent className="flex flex-col gap-3 p-4"> <CardContent className="flex flex-col gap-3 p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<FileText size={16} className="text-primary-400 shrink-0" /> <FileText size={16} className="text-accent shrink-0" />
<span className="font-semibold text-small truncate">{f.name}</span> <span className="font-semibold text-sm truncate">{f.name}</span>
</div> </div>
<div className="flex gap-1"> <div className="flex gap-1">
<Button isIconOnly size="sm" variant="light" onPress={() => { <Button isIconOnly size="sm" variant="ghost" onPress={() => {
setFormDraft({ setFormDraft({
name: f.name, description: f.description || '', name: f.name, description: f.description || '',
reviewChannelId: f.reviewChannelId || '', reviewChannelId: f.reviewChannelId || '',
@@ -45,25 +49,25 @@ export function Register() {
}}> }}>
<Pencil size={14} /> <Pencil size={14} />
</Button> </Button>
<Button isIconOnly size="sm" variant="light" color="danger" onPress={() => deleteForm(f.id)}> <Button isIconOnly size="sm" variant="danger-soft" onPress={() => deleteForm(f.id)}>
<Trash2 size={14} /> <Trash2 size={14} />
</Button> </Button>
</div> </div>
</div> </div>
<p className="text-small text-default-400">{f.description || 'Keine Beschreibung'}</p> <p className="text-sm text-muted">{f.description || 'Keine Beschreibung'}</p>
<div className="flex items-center gap-2 text-tiny"> <div className="flex items-center gap-2 text-xs">
<Chip size="sm" variant="flat" color={f.isActive ? 'success' : 'default'}> <Chip size="sm" variant="soft" color={f.isActive ? 'success' : 'default'}>
{f.isActive ? 'Aktiv' : 'Inaktiv'} {f.isActive ? 'Aktiv' : 'Inaktiv'}
</Chip> </Chip>
<span className="text-default-400">{f.fields?.length || 0} Felder</span> <span className="text-muted">{f.fields?.length || 0} Felder</span>
<Button size="sm" variant="flat" startContent={<Send size={12} />} onPress={() => sendFormPanel(f.id)}> <Button size="sm" variant="tertiary" onPress={() => sendFormPanel(f.id)}>
Panel senden <Send size={12} /> Panel senden
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
)) : ( )) : (
<div className="flex flex-col items-center gap-2 py-8 text-center text-small text-default-400"> <div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
<ClipboardList size={24} /> <ClipboardList size={24} />
Keine Formulare Keine Formulare
</div> </div>
@@ -71,7 +75,7 @@ export function Register() {
</div> </div>
</div> </div>
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">{editingFormId ? 'Formular bearbeiten' : 'Neues Formular'}</h3> <h3 className="text-base font-semibold">{editingFormId ? 'Formular bearbeiten' : 'Neues Formular'}</h3>
</CardHeader> </CardHeader>
@@ -94,15 +98,15 @@ export function Register() {
</TextField> </TextField>
<TextField> <TextField>
<Label>Felder (label|type|required|options)</Label> <Label>Felder (label|type|required|options)</Label>
<TextArea minRows={6} value={formDraft.fields} onChange={(e) => setFormDraft((s) => ({ ...s, fields: e.target.value }))} /> <TextArea rows={6} value={formDraft.fields} onChange={(e) => setFormDraft((s) => ({ ...s, fields: e.target.value }))} />
</TextField> </TextField>
<p className="text-tiny text-default-400"> <p className="text-xs text-muted">
Pro Zeile: label | type (text/paragraph/select/multi) | required | option1,option2 Pro Zeile: label | type (text/paragraph/select/multi) | required | option1,option2
</p> </p>
<div className="flex gap-2"> <div className="flex gap-2">
<Button color="primary" onPress={saveForm}>{editingFormId ? 'Aktualisieren' : 'Erstellen'}</Button> <Button variant="primary" onPress={saveForm}>{editingFormId ? 'Aktualisieren' : 'Erstellen'}</Button>
{editingFormId && ( {editingFormId && (
<Button variant="flat" onPress={() => { setEditingFormId(null); setFormDraft({ name: '', description: '', reviewChannelId: '', notifyRoleIds: '', fields: '' }); }}> <Button variant="tertiary" onPress={() => { setEditingFormId(null); setFormDraft({ name: '', description: '', reviewChannelId: '', notifyRoleIds: '', fields: '' }); }}>
Abbrechen Abbrechen
</Button> </Button>
)} )}
@@ -114,23 +118,23 @@ export function Register() {
{registerTab === 'apps' && ( {registerTab === 'apps' && (
<div className="mt-5"> <div className="mt-5">
<h3 className="mb-3 text-base font-semibold">Eingegangene Antr<EFBFBD>ge ({registerApps.length})</h3> <h3 className="mb-3 text-base font-semibold">Eingegangene Anträge ({registerApps.length})</h3>
<div className="space-y-3"> <div className="space-y-3">
{registerApps.length ? registerApps.map((app) => ( {registerApps.length ? registerApps.map((app) => (
<Card key={app.id} className="border border-default-100 bg-default-50/20"> <Card key={app.id}>
<CardContent className="flex flex-col gap-3 p-4"> <CardContent className="flex flex-col gap-3 p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="font-semibold">{app.username || app.userId}</div> <div className="font-semibold">{app.username || app.userId}</div>
<Chip size="sm" variant="flat" color={app.status === 'approved' ? 'success' : app.status === 'rejected' ? 'danger' : 'warning'}> <Chip size="sm" variant="soft" color={app.status === 'approved' ? 'success' : app.status === 'rejected' ? 'danger' : 'warning'}>
{app.status} {app.status}
</Chip> </Chip>
</div> </div>
<div className="text-tiny text-default-400">{formatDate(app.createdAt)}</div> <div className="text-xs text-muted">{formatDate(app.createdAt)}</div>
{app.answers?.length ? ( {app.answers?.length ? (
<div className="space-y-1"> <div className="space-y-1">
{app.answers.map((a, i) => ( {app.answers.map((a, i) => (
<div key={i} className="text-small"> <div key={i} className="text-sm">
<span className="text-default-500">{a.label || 'Frage'}: </span> <span className="text-muted">{a.label || 'Frage'}: </span>
{a.value} {a.value}
</div> </div>
))} ))}
@@ -139,9 +143,9 @@ export function Register() {
</CardContent> </CardContent>
</Card> </Card>
)) : ( )) : (
<div className="flex flex-col items-center gap-2 py-8 text-center text-small text-default-400"> <div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
<ClipboardList size={24} /> <ClipboardList size={24} />
Keine Antr<EFBFBD>ge Keine Anträge
</div> </div>
)} )}
</div> </div>

View File

@@ -11,7 +11,7 @@ export function ServerStats() {
return ( return (
<SectionCard title="Server Stats" subtitle="Counter und Refresh-Intervall steuern"> <SectionCard title="Server Stats" subtitle="Counter und Refresh-Intervall steuern">
<div className="grid gap-5 xl:grid-cols-[420px_1fr]"> <div className="grid gap-5 xl:grid-cols-[420px_1fr]">
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Konfiguration</h3> <h3 className="text-base font-semibold">Konfiguration</h3>
</CardHeader> </CardHeader>
@@ -23,7 +23,7 @@ export function ServerStats() {
<TextField> <TextField>
<Label>Kategorie-Name</Label> <Label>Kategorie-Name</Label>
<Input <Input
placeholder="?? Server Stats" placeholder="Server Stats"
value={statsDraft?.categoryName || ''} value={statsDraft?.categoryName || ''}
onChange={(e) => setStatsDraft((s) => ({ ...(s || {}), categoryName: e.target.value }))} onChange={(e) => setStatsDraft((s) => ({ ...(s || {}), categoryName: e.target.value }))}
/> />
@@ -38,30 +38,30 @@ export function ServerStats() {
/> />
</TextField> </TextField>
<Button color="primary" startContent={<Save size={16} />} onPress={saveServerStats}> <Button variant="primary" onPress={saveServerStats}>
Server Stats speichern <Save size={16} /> Server Stats speichern
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Items ({items.length})</h3> <h3 className="text-base font-semibold">Items ({items.length})</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-3 p-5"> <CardContent className="flex flex-col gap-3 p-5">
{items.length ? items.map((item, i) => ( {items.length ? items.map((item, i) => (
<div key={i} className="flex items-center justify-between rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div key={i} className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-3 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Activity size={14} className="text-primary-400" /> <Activity size={14} className="text-accent" />
<span className="font-medium">{item.label || item.key}</span> <span className="font-medium">{item.label || item.key}</span>
<Chip size="sm" variant="flat">{item.type || '-'}</Chip> <Chip size="sm" variant="soft">{item.type || '-'}</Chip>
</div> </div>
<Button isIconOnly size="sm" variant="light" color="danger" onPress={() => deleteStatsItem(i)}> <Button isIconOnly size="sm" variant="danger-soft" onPress={() => deleteStatsItem(i)}>
<Trash2 size={14} /> <Trash2 size={14} />
</Button> </Button>
</div> </div>
)) : ( )) : (
<div className="flex flex-col items-center gap-2 py-4 text-center text-tiny text-default-400"> <div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
<BarChart3 size={20} /> <BarChart3 size={20} />
Keine Items Keine Items
</div> </div>
@@ -70,11 +70,11 @@ export function ServerStats() {
<Separator /> <Separator />
<div> <div>
<h4 className="text-small font-semibold mb-2">Item hinzuf<EFBFBD>gen</h4> <h4 className="text-sm font-semibold mb-2">Item hinzufügen</h4>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Input placeholder="Label" value={statsItemDraft.label} onChange={(e) => setStatsItemDraft((s) => ({ ...s, label: e.target.value }))} /> <Input placeholder="Label" value={statsItemDraft.label} onChange={(e) => setStatsItemDraft((s) => ({ ...s, label: e.target.value }))} />
<select <select
className="w-full rounded-xl border border-default-200 bg-default-50 px-3 py-2 text-sm outline-none" className="w-full rounded-xl px-3 py-2 text-sm"
value={statsItemDraft.type} value={statsItemDraft.type}
onChange={(e) => setStatsItemDraft((s) => ({ ...s, type: e.target.value }))} onChange={(e) => setStatsItemDraft((s) => ({ ...s, type: e.target.value }))}
> >
@@ -85,8 +85,8 @@ export function ServerStats() {
<option value="online">Online</option> <option value="online">Online</option>
<option value="custom">Custom</option> <option value="custom">Custom</option>
</select> </select>
<Button size="sm" color="primary" startContent={<Plus size={14} />} onPress={addStatsItem}> <Button size="sm" variant="primary" onPress={addStatsItem}>
Hinzuf<EFBFBD>gen <Plus size={14} /> Hinzufügen
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -9,7 +9,7 @@ export function SettingsPage() {
return ( return (
<SectionCard title="Einstellungen & Logging" subtitle="Globale Guild-Settings und Log-Kategorien"> <SectionCard title="Einstellungen & Logging" subtitle="Globale Guild-Settings und Log-Kategorien">
<div className="grid gap-5 xl:grid-cols-2"> <div className="grid gap-5 xl:grid-cols-2">
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Allgemein</h3> <h3 className="text-base font-semibold">Allgemein</h3>
</CardHeader> </CardHeader>
@@ -43,13 +43,13 @@ export function SettingsPage() {
<Separator /> <Separator />
<Button color="primary" startContent={<Save size={16} />} onPress={() => saveSettingsPayload(settings, 'Settings gespeichert')}> <Button variant="primary" onPress={() => saveSettingsPayload(settings, 'Settings gespeichert')}>
Speichern <Save size={16} /> Speichern
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Logging Kategorien</h3> <h3 className="text-base font-semibold">Logging Kategorien</h3>
</CardHeader> </CardHeader>
@@ -76,8 +76,8 @@ export function SettingsPage() {
<Separator /> <Separator />
<Button color="primary" startContent={<Save size={16} />} onPress={() => saveSettingsPayload(settings, 'Settings gespeichert')}> <Button variant="primary" onPress={() => saveSettingsPayload(settings, 'Settings gespeichert')}>
Speichern <Save size={16} /> Speichern
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -12,7 +12,7 @@ export function Statuspage() {
return ( return (
<SectionCard title="Statuspage" subtitle="Statusseite und Service-Liste verwalten"> <SectionCard title="Statuspage" subtitle="Statusseite und Service-Liste verwalten">
<div className="grid gap-5 xl:grid-cols-[420px_1fr]"> <div className="grid gap-5 xl:grid-cols-[420px_1fr]">
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Konfiguration</h3> <h3 className="text-base font-semibold">Konfiguration</h3>
</CardHeader> </CardHeader>
@@ -24,7 +24,7 @@ export function Statuspage() {
<TextField> <TextField>
<Label>Channel ID</Label> <Label>Channel ID</Label>
<Input <Input
placeholder="Channel f<EFBFBD>r Status-Updates" placeholder="Channel für Status-Updates"
value={statusDraft?.channelId || ''} value={statusDraft?.channelId || ''}
onChange={(e) => setStatusDraft((s) => ({ ...(s || {}), channelId: e.target.value }))} onChange={(e) => setStatusDraft((s) => ({ ...(s || {}), channelId: e.target.value }))}
/> />
@@ -39,39 +39,39 @@ export function Statuspage() {
/> />
</TextField> </TextField>
<Button color="primary" startContent={<Save size={16} />} onPress={saveStatuspage}> <Button variant="primary" onPress={saveStatuspage}>
Statuspage speichern <Save size={16} /> Statuspage speichern
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Services ({services.length})</h3> <h3 className="text-base font-semibold">Services ({services.length})</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-3 p-5"> <CardContent className="flex flex-col gap-3 p-5">
{services.length ? services.map((service) => ( {services.length ? services.map((service) => (
<div key={service.id} className="flex items-center justify-between rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div key={service.id} className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-3 text-sm">
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<div className="size-2 rounded-full shrink-0" /> <div className="size-2 rounded-full shrink-0" />
<div className="min-w-0"> <div className="min-w-0">
<span className="font-medium">{service.name || 'Service'}</span> <span className="font-medium">{service.name || 'Service'}</span>
<span className="ml-2 text-default-400 text-tiny truncate">{service.url || ''}</span> <span className="ml-2 text-muted text-xs truncate">{service.url || ''}</span>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Chip size="sm" variant="flat" color={ <Chip size="sm" variant="soft" color={
service.status === 'operational' ? 'success' : service.status === 'operational' ? 'success' :
service.status === 'degraded' ? 'warning' : service.status === 'degraded' ? 'warning' :
service.status === 'down' ? 'danger' : 'default' service.status === 'down' ? 'danger' : 'default'
}>{service.status || 'unknown'}</Chip> }>{service.status || 'unknown'}</Chip>
<Button isIconOnly size="sm" variant="light" color="danger" onPress={() => deleteStatusService(service.id)}> <Button isIconOnly size="sm" variant="danger-soft" onPress={() => deleteStatusService(service.id)}>
<Trash2 size={14} /> <Trash2 size={14} />
</Button> </Button>
</div> </div>
</div> </div>
)) : ( )) : (
<div className="flex flex-col items-center gap-2 py-4 text-center text-tiny text-default-400"> <div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
<ActivityIcon size={20} /> <ActivityIcon size={20} />
Keine Services Keine Services
</div> </div>
@@ -80,12 +80,12 @@ export function Statuspage() {
<Separator /> <Separator />
<div> <div>
<h4 className="text-small font-semibold mb-2">Service hinzuf<EFBFBD>gen</h4> <h4 className="text-sm font-semibold mb-2">Service hinzufügen</h4>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Input placeholder="Name" value={statusServiceDraft.name} onChange={(e) => setStatusServiceDraft((s) => ({ ...s, name: e.target.value }))} /> <Input placeholder="Name" value={statusServiceDraft.name} onChange={(e) => setStatusServiceDraft((s) => ({ ...s, name: e.target.value }))} />
<Input placeholder="URL (optional)" value={statusServiceDraft.url} onChange={(e) => setStatusServiceDraft((s) => ({ ...s, url: e.target.value }))} /> <Input placeholder="URL (optional)" value={statusServiceDraft.url} onChange={(e) => setStatusServiceDraft((s) => ({ ...s, url: e.target.value }))} />
<Button size="sm" color="primary" startContent={<Plus size={14} />} onPress={addStatusService}> <Button size="sm" variant="primary" onPress={addStatusService}>
Hinzuf<EFBFBD>gen <Plus size={14} /> Hinzufügen
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,8 @@
import { Avatar, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, TextArea, Button, Chip, Switch, Separator, TextField, Label } from '@heroui/react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, Input, TextArea, Button, Chip, Switch, Separator, TextField, Label } from '@heroui/react';
import { LogIn, UserRound, Save, Send } from 'lucide-react'; import { LogIn, UserRound, Save, Send } 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 { AppAvatar } from '../components/shared/AppAvatar';
export function SupportLogin() { export function SupportLogin() {
const { supportLogin, setSupportLogin, saveSupportLogin } = useApp(); const { supportLogin, setSupportLogin, saveSupportLogin } = useApp();
@@ -9,7 +10,7 @@ export function SupportLogin() {
return ( return (
<SectionCard title="Support Login" subtitle="Login-Panel fuer Supporter konfigurieren"> <SectionCard title="Support Login" subtitle="Login-Panel fuer Supporter konfigurieren">
<div className="grid gap-5 xl:grid-cols-[1fr_400px]"> <div className="grid gap-5 xl:grid-cols-[1fr_400px]">
<Card className="surface-card"> <Card>
<CardHeader> <CardHeader>
<div> <div>
<CardTitle>Panel-Konfiguration</CardTitle> <CardTitle>Panel-Konfiguration</CardTitle>
@@ -27,8 +28,6 @@ export function SupportLogin() {
<TextField> <TextField>
<Label>Panel Channel ID</Label> <Label>Panel Channel ID</Label>
<Input <Input
variant="bordered"
size="lg"
placeholder="Channel ID eingeben" placeholder="Channel ID eingeben"
value={supportLogin?.config?.panelChannelId || ''} value={supportLogin?.config?.panelChannelId || ''}
onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, panelChannelId: e.target.value } } : s)} onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, panelChannelId: e.target.value } } : s)}
@@ -38,8 +37,6 @@ export function SupportLogin() {
<TextField> <TextField>
<Label>Titel</Label> <Label>Titel</Label>
<Input <Input
variant="bordered"
size="lg"
value={supportLogin?.config?.title || 'Support Login'} value={supportLogin?.config?.title || 'Support Login'}
onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, title: e.target.value } } : s)} onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, title: e.target.value } } : s)}
/> />
@@ -48,7 +45,6 @@ export function SupportLogin() {
<TextField> <TextField>
<Label>Beschreibung</Label> <Label>Beschreibung</Label>
<TextArea <TextArea
variant="bordered"
value={supportLogin?.config?.description || ''} value={supportLogin?.config?.description || ''}
onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, description: e.target.value } } : s)} onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, description: e.target.value } } : s)}
/> />
@@ -57,8 +53,6 @@ export function SupportLogin() {
<TextField> <TextField>
<Label>Login Button Label</Label> <Label>Login Button Label</Label>
<Input <Input
variant="bordered"
size="lg"
value={supportLogin?.config?.loginLabel || 'Ich bin jetzt im Support'} value={supportLogin?.config?.loginLabel || 'Ich bin jetzt im Support'}
onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, loginLabel: e.target.value } } : s)} onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, loginLabel: e.target.value } } : s)}
/> />
@@ -67,8 +61,6 @@ export function SupportLogin() {
<TextField> <TextField>
<Label>Logout Button Label</Label> <Label>Logout Button Label</Label>
<Input <Input
variant="bordered"
size="lg"
value={supportLogin?.config?.logoutLabel || 'Ich bin nicht mehr im Support'} value={supportLogin?.config?.logoutLabel || 'Ich bin nicht mehr im Support'}
onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, logoutLabel: e.target.value } } : s)} onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, logoutLabel: e.target.value } } : s)}
/> />
@@ -77,18 +69,18 @@ export function SupportLogin() {
<Separator /> <Separator />
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="light" className="accent-button" startContent={<Save size={16} />} onPress={saveSupportLogin}> <Button variant="primary" onPress={saveSupportLogin}>
Speichern & Panel senden <Save size={16} /> Speichern & Panel senden
</Button> </Button>
<Button variant="light" className="ghost-button" startContent={<Send size={16} />}> <Button variant="ghost">
Panel manuell senden <Send size={16} /> Panel manuell senden
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<div className="space-y-4"> <div className="space-y-4">
<Card className="surface-card"> <Card>
<CardHeader> <CardHeader>
<div> <div>
<CardTitle>Live Vorschau</CardTitle> <CardTitle>Live Vorschau</CardTitle>
@@ -96,10 +88,10 @@ export function SupportLogin() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Card className="surface-card-muted"> <Card className="bg-surface-tertiary">
<CardHeader> <CardHeader>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LogIn size={16} className="accent-text" /> <LogIn size={16} className="text-accent" />
<div> <div>
<CardTitle>{supportLogin?.config?.title || 'Support Login'}</CardTitle> <CardTitle>{supportLogin?.config?.title || 'Support Login'}</CardTitle>
<CardDescription>{supportLogin?.config?.description || 'Melde dich als Support an/ab.'}</CardDescription> <CardDescription>{supportLogin?.config?.description || 'Melde dich als Support an/ab.'}</CardDescription>
@@ -107,14 +99,14 @@ export function SupportLogin() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="flex gap-2"> <CardContent className="flex gap-2">
<Button size="sm" variant="light" className="accent-button">{supportLogin?.config?.loginLabel || 'Login'}</Button> <Button size="sm" variant="primary">{supportLogin?.config?.loginLabel || 'Login'}</Button>
<Button size="sm" variant="light" className="ghost-button">{supportLogin?.config?.logoutLabel || 'Logout'}</Button> <Button size="sm" variant="ghost">{supportLogin?.config?.logoutLabel || 'Logout'}</Button>
</CardContent> </CardContent>
</Card> </Card>
</CardContent> </CardContent>
</Card> </Card>
<Card className="surface-card"> <Card>
<CardHeader> <CardHeader>
<div> <div>
<CardTitle>Aktive Supporter</CardTitle> <CardTitle>Aktive Supporter</CardTitle>
@@ -123,20 +115,20 @@ export function SupportLogin() {
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-2"> <CardContent className="flex flex-col gap-2">
{supportLogin?.status?.active?.length ? supportLogin.status.active.map((s, i) => ( {supportLogin?.status?.active?.length ? supportLogin.status.active.map((s, i) => (
<Card key={i} className="surface-card-muted"> <Card key={i} className="bg-surface-tertiary">
<CardContent className="flex items-center gap-3"> <CardContent className="flex items-center gap-3">
<Avatar name={s.username?.[0]} size="sm" className="size-6" /> <AppAvatar name={s.username} size="sm" className="size-6" />
<span className="font-medium">{s.username || s.userId}</span> <span className="font-medium">{s.username || s.userId}</span>
<Chip size="sm" variant="flat" color="success" className="ml-auto">Online</Chip> <Chip size="sm" variant="soft" color="success" className="ml-auto">Online</Chip>
</CardContent> </CardContent>
</Card> </Card>
)) : ( )) : (
<div className="flex flex-col items-center gap-2 py-4 text-center text-tiny text-default-400"> <div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
<UserRound size={20} /> <UserRound size={20} />
Keine aktiven Supporter Keine aktiven Supporter
</div> </div>
)} )}
<p className="mt-2 text-tiny text-default-400"> <p className="mt-2 text-xs text-muted">
Support Role ID: {supportLogin?.supportRoleId || 'Nicht gesetzt'} Support Role ID: {supportLogin?.supportRoleId || 'Nicht gesetzt'}
</p> </p>
</CardContent> </CardContent>

View File

@@ -21,19 +21,23 @@ export function Tickets() {
return ( return (
<SectionCard title="Ticketsystem" subtitle="Ticket-Übersicht, Pipeline, SLA, Automationen und Knowledge Base."> <SectionCard title="Ticketsystem" subtitle="Ticket-Übersicht, Pipeline, SLA, Automationen und Knowledge Base.">
<Tabs aria-label="Ticket Tabs" color="primary" selectedKey={ticketTab} variant="bordered" onSelectionChange={(key) => setTicketTab(String(key))}> <Tabs selectedKey={ticketTab} variant="primary" onSelectionChange={(key) => setTicketTab(String(key))}>
<Tab key="overview">Übersicht</Tab> <Tabs.ListContainer>
<Tab key="pipeline">Pipeline</Tab> <Tabs.List aria-label="Ticket Tabs">
<Tab key="sla">SLA</Tab> <Tab id="overview">Übersicht</Tab>
<Tab key="automations">Automationen</Tab> <Tab id="pipeline">Pipeline</Tab>
<Tab key="kb">Knowledge Base</Tab> <Tab id="sla">SLA</Tab>
<Tab id="automations">Automationen</Tab>
<Tab id="kb">Knowledge Base</Tab>
</Tabs.List>
</Tabs.ListContainer>
</Tabs> </Tabs>
{ticketTab === 'overview' && ( {ticketTab === 'overview' && (
<div className="mt-5 space-y-5"> <div className="mt-5 space-y-5">
<div className="grid gap-4 sm:grid-cols-4"> <div className="grid gap-4 sm:grid-cols-4">
<StatCard icon={<Ticket size={18} />} label="Offen" value={overview?.tickets?.open ?? 0} color="warning" /> <StatCard icon={<Ticket size={18} />} label="Offen" value={overview?.tickets?.open ?? 0} color="warning" />
<StatCard icon={<Clock size={18} />} label="In Bearbeitung" value={overview?.tickets?.inProgress ?? 0} color="primary" /> <StatCard icon={<Clock size={18} />} label="In Bearbeitung" value={overview?.tickets?.inProgress ?? 0} color="accent" />
<StatCard icon={<CheckCircle size={18} />} label="Geschlossen" value={overview?.tickets?.closed ?? 0} color="default" /> <StatCard icon={<CheckCircle size={18} />} label="Geschlossen" value={overview?.tickets?.closed ?? 0} color="default" />
<StatCard icon={<UserRound size={18} />} label="Gesamt" value={tickets.length} /> <StatCard icon={<UserRound size={18} />} label="Gesamt" value={tickets.length} />
</div> </div>
@@ -43,36 +47,36 @@ export function Tickets() {
<h3 className="mb-3 text-base font-semibold">Offene Tickets ({openTickets.length})</h3> <h3 className="mb-3 text-base font-semibold">Offene Tickets ({openTickets.length})</h3>
<div className="space-y-3"> <div className="space-y-3">
{openTickets.length ? openTickets.map((t) => ( {openTickets.length ? openTickets.map((t) => (
<Card key={t.id} className="border border-default-100 bg-default-50/20"> <Card key={t.id}>
<CardContent className="flex flex-col gap-3 p-4"> <CardContent className="flex flex-col gap-3 p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<div className={`size-2 rounded-full shrink-0 ${ <div className={`size-2 rounded-full shrink-0 ${
t.status === 'open' ? 'bg-warning-400' : t.status === 'open' ? 'bg-warning' :
t.status === 'in-progress' ? 'bg-primary-400' : t.status === 'in-progress' ? 'bg-accent' :
t.status === 'waiting' ? 'bg-default-400' : 'bg-success-400' t.status === 'waiting' ? 'bg-default' : 'bg-success'
}`} /> }`} />
<span className="font-semibold text-small truncate">{t.topic || 'Ticket'}</span> <span className="font-semibold text-sm truncate">{t.topic || 'Ticket'}</span>
</div> </div>
<Chip size="sm" variant="flat" color={t.status === 'open' ? 'warning' : t.status === 'closed' ? 'default' : 'primary'}> <Chip size="sm" variant="soft" color={t.status === 'open' ? 'warning' : t.status === 'closed' ? 'default' : 'accent'}>
{t.status || 'open'} {t.status || 'open'}
</Chip> </Chip>
</div> </div>
<div className="flex items-center gap-2 text-tiny text-default-400"> <div className="flex items-center gap-2 text-xs text-muted">
<span>{t.category || 'Allgemein'}</span> <span>{t.category || 'Allgemein'}</span>
<span>·</span> <span>·</span>
<span>{formatDate(t.createdAt)}</span> <span>{formatDate(t.createdAt)}</span>
{t.claimedById && ( {t.claimedById && (
<> <>
<span>·</span> <span>·</span>
<Chip size="sm" variant="flat" color="success" className="h-5">Claimed</Chip> <Chip size="sm" variant="soft" color="success" className="h-5">Claimed</Chip>
</> </>
)} )}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<select <select
aria-label="Status" aria-label="Status"
className="w-40 rounded-xl border border-default-200 bg-default-50 px-3 py-2 text-sm text-foreground outline-none transition-colors focus:border-primary-400" className="w-40 rounded-xl text-sm"
value="" value=""
onChange={(e) => { if (e.target.value) updateTicketStatus(t.id, e.target.value); }} onChange={(e) => { if (e.target.value) updateTicketStatus(t.id, e.target.value); }}
> >
@@ -82,8 +86,8 @@ export function Tickets() {
<option value="waiting">Warten</option> <option value="waiting">Warten</option>
<option value="closed">Closed</option> <option value="closed">Closed</option>
</select> </select>
<Button size="sm" color="danger" variant="flat" onPress={() => closeTicket(t.id)}>Schließen</Button> <Button size="sm" variant="danger" onPress={() => closeTicket(t.id)}>Schließen</Button>
<Button size="sm" variant="flat" onPress={() => { setTicketDetail(t); loadTicketMessages(t.id); }}>Details</Button> <Button size="sm" variant="tertiary" onPress={() => { setTicketDetail(t); loadTicketMessages(t.id); }}>Details</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -95,12 +99,12 @@ export function Tickets() {
<h3 className="mb-3 text-base font-semibold">Alle Tickets ({tickets.length})</h3> <h3 className="mb-3 text-base font-semibold">Alle Tickets ({tickets.length})</h3>
<div className="space-y-2"> <div className="space-y-2">
{tickets.length ? tickets.map((t) => ( {tickets.length ? tickets.map((t) => (
<div key={t.id} className="flex items-center justify-between rounded-xl border border-default-100 bg-default-50/20 px-4 py-3"> <div key={t.id} className="bg-surface flex items-center justify-between rounded-xl px-4 py-3">
<div className="min-w-0"> <div className="min-w-0">
<div className="text-small font-medium truncate">{t.topic || t.id}</div> <div className="text-sm font-medium truncate">{t.topic || t.id}</div>
<div className="text-tiny text-default-400">{t.category || '-'} · {formatDate(t.createdAt)}</div> <div className="text-xs text-muted">{t.category || '-'} · {formatDate(t.createdAt)}</div>
</div> </div>
<Chip size="sm" variant="flat" color={t.status === 'open' ? 'warning' : t.status === 'closed' ? 'default' : 'primary'}> <Chip size="sm" variant="soft" color={t.status === 'open' ? 'warning' : t.status === 'closed' ? 'default' : 'accent'}>
{t.status} {t.status}
</Chip> </Chip>
</div> </div>
@@ -110,33 +114,33 @@ export function Tickets() {
</div> </div>
{ticketDetail && ( {ticketDetail && (
<Card className="border border-default-100"> <Card>
<CardHeader className="flex items-center justify-between px-5 pt-5 pb-0"> <CardHeader className="flex items-center justify-between px-5 pt-5 pb-0">
<h3 className="text-lg font-bold">{ticketDetail.topic || 'Ticket-Details'}</h3> <h3 className="text-lg font-bold">{ticketDetail.topic || 'Ticket-Details'}</h3>
<Button size="sm" variant="light" onPress={() => setTicketDetail(null)}>Schließen</Button> <Button size="sm" variant="ghost" onPress={() => setTicketDetail(null)}>Schließen</Button>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4 p-5"> <CardContent className="flex flex-col gap-4 p-5">
<div className="grid grid-cols-3 gap-3 text-small"> <div className="grid grid-cols-3 gap-3 text-sm">
<div className="rounded-lg bg-default-100/40 px-3 py-2"> <div className="rounded-lg bg-default-soft px-3 py-2">
<span className="text-default-400">Status:</span> {ticketDetail.status} <span className="text-muted">Status:</span> {ticketDetail.status}
</div> </div>
<div className="rounded-lg bg-default-100/40 px-3 py-2"> <div className="rounded-lg bg-default-soft px-3 py-2">
<span className="text-default-400">Priorität:</span> {ticketDetail.priority || 'normal'} <span className="text-muted">Priorität:</span> {ticketDetail.priority || 'normal'}
</div> </div>
<div className="rounded-lg bg-default-100/40 px-3 py-2"> <div className="rounded-lg bg-default-soft px-3 py-2">
<span className="text-default-400">Kategorie:</span> {ticketDetail.category || '-'} <span className="text-muted">Kategorie:</span> {ticketDetail.category || '-'}
</div> </div>
</div> </div>
<Separator /> <Separator />
<div> <div>
<h4 className="mb-2 text-small font-semibold">Nachrichten ({ticketMessages.length})</h4> <h4 className="mb-2 text-sm font-semibold">Nachrichten ({ticketMessages.length})</h4>
<div className="max-h-60 space-y-2 overflow-auto"> <div className="max-h-60 space-y-2 overflow-auto">
{ticketMessages.length ? ticketMessages.map((msg, i) => ( {ticketMessages.length ? ticketMessages.map((msg, i) => (
<div key={i} className="rounded-lg bg-default-100/40 px-3 py-2 text-small"> <div key={i} className="rounded-lg bg-default-soft px-3 py-2 text-sm">
<span className="text-tiny text-default-400">{msg.author?.tag || msg.authorId}: </span> <span className="text-xs text-muted">{msg.author?.tag || msg.authorId}: </span>
{msg.content || '(Embed)'} {msg.content || '(Embed)'}
</div> </div>
)) : <p className="text-tiny text-default-400">Keine Nachrichten</p>} )) : <p className="text-xs text-muted">Keine Nachrichten</p>}
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -149,31 +153,31 @@ export function Tickets() {
<div className="mt-5 grid gap-4 lg:grid-cols-2 2xl:grid-cols-4"> <div className="mt-5 grid gap-4 lg:grid-cols-2 2xl:grid-cols-4">
{[ {[
{ key: 'neu', label: 'Neu', color: 'warning' as const }, { key: 'neu', label: 'Neu', color: 'warning' as const },
{ key: 'in_bearbeitung', label: 'In Bearbeitung', color: 'primary' as const }, { key: 'in_bearbeitung', label: 'In Bearbeitung', color: 'accent' as const },
{ key: 'warten_auf_user', label: 'Warten auf User', color: 'default' as const }, { key: 'warten_auf_user', label: 'Warten auf User', color: 'default' as const },
{ key: 'erledigt', label: 'Erledigt', color: 'success' as const }, { key: 'erledigt', label: 'Erledigt', color: 'success' as const },
].map(({ key, label, color }) => ( ].map(({ key, label, color }) => (
<Card key={key} className="border border-default-100 bg-default-50/20"> <Card key={key}>
<CardHeader className="px-4 pt-4 pb-0"> <CardHeader className="px-4 pt-4 pb-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className={`size-2.5 rounded-full bg-${color}-400`} /> <div className={`size-2.5 rounded-full bg-${color}`} />
<h3 className="text-sm font-semibold">{label}</h3> <h3 className="text-sm font-semibold">{label}</h3>
<Chip size="sm" variant="flat" color={color}>{(pipeline[key] || []).length}</Chip> <Chip size="sm" variant="soft" color={color}>{(pipeline[key] || []).length}</Chip>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-2 p-4"> <CardContent className="flex flex-col gap-2 p-4">
{(pipeline[key] || []).length ? (pipeline[key] || []).map((t) => ( {(pipeline[key] || []).length ? (pipeline[key] || []).map((t) => (
<div key={t.id} className="rounded-xl border border-default-100 bg-default-50/30 p-3 text-small"> <div key={t.id} className="bg-surface-tertiary rounded-xl p-3 text-sm">
<div className="font-medium truncate">{t.topic || t.id}</div> <div className="font-medium truncate">{t.topic || t.id}</div>
<div className="mt-1 text-tiny text-default-400">{formatDate(t.createdAt)}</div> <div className="mt-1 text-xs text-muted">{formatDate(t.createdAt)}</div>
<Button size="sm" variant="flat" className="mt-2 h-6 min-w-0 px-2 text-tiny" onPress={() => { <Button size="sm" variant="tertiary" className="mt-2 h-6 min-w-0 px-2 text-xs" onPress={() => {
const nextStatus = key === 'neu' ? 'in-progress' : key === 'in_bearbeitung' ? 'waiting' : key === 'warten_auf_user' ? 'closed' : 'closed'; const nextStatus = key === 'neu' ? 'in-progress' : key === 'in_bearbeitung' ? 'waiting' : key === 'warten_auf_user' ? 'closed' : 'closed';
updateTicketStatus(t.id, nextStatus); updateTicketStatus(t.id, nextStatus);
}}> }}>
{key === 'erledigt' ? 'Schließen' : key === 'warten_auf_user' ? 'Schließen' : 'Weiter'} {key === 'erledigt' ? 'Schließen' : key === 'warten_auf_user' ? 'Schließen' : 'Weiter'}
</Button> </Button>
</div> </div>
)) : <p className="text-tiny text-default-400 text-center py-4">Keine Tickets</p>} )) : <p className="text-xs text-muted text-center py-4">Keine Tickets</p>}
</CardContent> </CardContent>
</Card> </Card>
))} ))}
@@ -182,41 +186,41 @@ export function Tickets() {
{ticketTab === 'sla' && ( {ticketTab === 'sla' && (
<div className="mt-5 grid gap-5 xl:grid-cols-2"> <div className="mt-5 grid gap-5 xl:grid-cols-2">
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">SLA pro Supporter</h3> <h3 className="text-base font-semibold">SLA pro Supporter</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-2 p-5"> <CardContent className="flex flex-col gap-2 p-5">
{(sla.supporters || []).length ? sla.supporters.map((row: any, i: number) => ( {(sla.supporters || []).length ? sla.supporters.map((row: any, i: number) => (
<div key={i} className="flex items-center justify-between rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <div key={i} className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-3 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<UserRound size={14} className="text-default-400" /> <UserRound size={14} className="text-muted" />
<span className="font-medium">{row.supporterId || '-'}</span> <span className="font-medium">{row.supporterId || '-'}</span>
</div> </div>
<div className="flex gap-3 text-tiny text-default-500"> <div className="flex gap-3 text-xs text-muted">
<span>{row.tickets || 0} Tickets</span> <span>{row.tickets || 0} Tickets</span>
<span>TTC: {row.avgTTC ? `${Math.round(row.avgTTC / 1000)}s` : '-'}</span> <span>TTC: {row.avgTTC ? `${Math.round(row.avgTTC / 1000)}s` : '-'}</span>
<span>TTFR: {row.avgTTFR ? `${Math.round(row.avgTTFR / 1000)}s` : '-'}</span> <span>TTFR: {row.avgTTFR ? `${Math.round(row.avgTTFR / 1000)}s` : '-'}</span>
</div> </div>
</div> </div>
)) : <p className="text-tiny text-default-400 text-center py-4">Keine Daten</p>} )) : <p className="text-xs text-muted text-center py-4">Keine Daten</p>}
</CardContent> </CardContent>
</Card> </Card>
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">SLA pro Tag</h3> <h3 className="text-base font-semibold">SLA pro Tag</h3>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-2 p-5"> <CardContent className="flex flex-col gap-2 p-5">
{(sla.days || []).length ? sla.days.slice(-14).map((row: any, i: number) => ( {(sla.days || []).length ? sla.days.slice(-14).map((row: any, i: number) => (
<div key={i} className="flex items-center justify-between rounded-xl border border-default-100 bg-default-50/30 px-4 py-2 text-tiny"> <div key={i} className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-2 text-xs">
<span className="font-medium">{row.date || '-'}</span> <span className="font-medium">{row.date || '-'}</span>
<div className="flex gap-2 text-default-500"> <div className="flex gap-2 text-muted">
<span>{row.tickets || 0}</span> <span>{row.tickets || 0}</span>
<span>TTC: {row.avgTTC ? `${Math.round(row.avgTTC / 1000)}s` : '-'}</span> <span>TTC: {row.avgTTC ? `${Math.round(row.avgTTC / 1000)}s` : '-'}</span>
<span>TTFR: {row.avgTTFR ? `${Math.round(row.avgTTFR / 1000)}s` : '-'}</span> <span>TTFR: {row.avgTTFR ? `${Math.round(row.avgTTFR / 1000)}s` : '-'}</span>
</div> </div>
</div> </div>
)) : <p className="text-tiny text-default-400 text-center py-4">Keine Daten</p>} )) : <p className="text-xs text-muted text-center py-4">Keine Daten</p>}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -228,20 +232,20 @@ export function Tickets() {
<h3 className="mb-3 text-base font-semibold">Regeln ({automations.length})</h3> <h3 className="mb-3 text-base font-semibold">Regeln ({automations.length})</h3>
<div className="space-y-3"> <div className="space-y-3">
{automations.length ? automations.map((rule) => ( {automations.length ? automations.map((rule) => (
<Card key={rule.id} className="border border-default-100 bg-default-50/20"> <Card key={rule.id}>
<CardContent className="flex flex-col gap-2 p-4"> <CardContent className="flex flex-col gap-2 p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="font-semibold text-small">{rule.name || 'Automation'}</div> <div className="font-semibold text-sm">{rule.name || 'Automation'}</div>
<Chip size="sm" variant="flat" color={rule.active !== false ? 'success' : 'default'}> <Chip size="sm" variant="soft" color={rule.active !== false ? 'success' : 'default'}>
{rule.active !== false ? 'Aktiv' : 'Inaktiv'} {rule.active !== false ? 'Aktiv' : 'Inaktiv'}
</Chip> </Chip>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" variant="flat" startContent={<Pencil size={14} />} onPress={() => setAutomationEditDraft({ id: rule.id, name: rule.name || '', conditionValue: rule.condition?.category || '', actionValue: rule.action?.message || '' })}> <Button size="sm" variant="tertiary" onPress={() => setAutomationEditDraft({ id: rule.id, name: rule.name || '', conditionValue: rule.condition?.category || '', actionValue: rule.action?.message || '' })}>
Bearbeiten <Pencil size={14} /> Bearbeiten
</Button> </Button>
<Button size="sm" variant="flat" color="danger" startContent={<Trash2 size={14} />} onPress={() => deleteAutomation(rule.id)}> <Button size="sm" variant="danger-soft" onPress={() => deleteAutomation(rule.id)}>
Löschen <Trash2 size={14} /> Löschen
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@@ -251,7 +255,7 @@ export function Tickets() {
</div> </div>
<div> <div>
{automationEditDraft ? ( {automationEditDraft ? (
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Automation bearbeiten</h3> <h3 className="text-base font-semibold">Automation bearbeiten</h3>
</CardHeader> </CardHeader>
@@ -269,13 +273,13 @@ export function Tickets() {
<TextArea value={automationEditDraft.actionValue} onChange={(e) => setAutomationEditDraft((s) => ({ ...s, actionValue: e.target.value }))} /> <TextArea value={automationEditDraft.actionValue} onChange={(e) => setAutomationEditDraft((s) => ({ ...s, actionValue: e.target.value }))} />
</TextField> </TextField>
<div className="flex gap-2"> <div className="flex gap-2">
<Button color="primary" onPress={() => updateAutomation(automationEditDraft.id)}>Aktualisieren</Button> <Button variant="primary" onPress={() => updateAutomation(automationEditDraft.id)}>Aktualisieren</Button>
<Button variant="flat" onPress={() => setAutomationEditDraft(null)}>Abbrechen</Button> <Button variant="tertiary" onPress={() => setAutomationEditDraft(null)}>Abbrechen</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Neue Automation</h3> <h3 className="text-base font-semibold">Neue Automation</h3>
</CardHeader> </CardHeader>
@@ -292,7 +296,7 @@ export function Tickets() {
<Label>Aktion / Nachricht</Label> <Label>Aktion / Nachricht</Label>
<TextArea value={automationDraft.actionValue} onChange={(e) => setAutomationDraft((s) => ({ ...s, actionValue: e.target.value }))} /> <TextArea value={automationDraft.actionValue} onChange={(e) => setAutomationDraft((s) => ({ ...s, actionValue: e.target.value }))} />
</TextField> </TextField>
<Button color="primary" onPress={saveAutomation}>Automation speichern</Button> <Button variant="primary" onPress={saveAutomation}>Automation speichern</Button>
</CardContent> </CardContent>
</Card> </Card>
)} )}
@@ -306,19 +310,19 @@ export function Tickets() {
<h3 className="mb-3 text-base font-semibold">Artikel ({kbArticles.length})</h3> <h3 className="mb-3 text-base font-semibold">Artikel ({kbArticles.length})</h3>
<div className="space-y-3"> <div className="space-y-3">
{kbArticles.length ? kbArticles.map((article) => ( {kbArticles.length ? kbArticles.map((article) => (
<Card key={article.id} className="border border-default-100 bg-default-50/20"> <Card key={article.id}>
<CardContent className="flex flex-col gap-2 p-4"> <CardContent className="flex flex-col gap-2 p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="font-semibold text-small truncate">{article.title || 'Artikel'}</div> <div className="font-semibold text-sm truncate">{article.title || 'Artikel'}</div>
<Chip size="sm" variant="flat">{(article.keywords?.length || 0)} Keywords</Chip> <Chip size="sm" variant="soft">{(article.keywords?.length || 0)} Keywords</Chip>
</div> </div>
<p className="text-small text-default-400 line-clamp-2">{article.content || '-'}</p> <p className="text-sm text-muted line-clamp-2">{article.content || '-'}</p>
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" variant="flat" startContent={<Pencil size={14} />} onPress={() => setKbEditDraft({ id: article.id, title: article.title || '', keywords: (article.keywords || []).join(', '), content: article.content || '' })}> <Button size="sm" variant="tertiary" onPress={() => setKbEditDraft({ id: article.id, title: article.title || '', keywords: (article.keywords || []).join(', '), content: article.content || '' })}>
Bearbeiten <Pencil size={14} /> Bearbeiten
</Button> </Button>
<Button size="sm" variant="flat" color="danger" startContent={<Trash2 size={14} />} onPress={() => deleteKbArticle(article.id)}> <Button size="sm" variant="danger-soft" onPress={() => deleteKbArticle(article.id)}>
Löschen <Trash2 size={14} /> Löschen
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@@ -328,7 +332,7 @@ export function Tickets() {
</div> </div>
<div> <div>
{kbEditDraft ? ( {kbEditDraft ? (
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Artikel bearbeiten</h3> <h3 className="text-base font-semibold">Artikel bearbeiten</h3>
</CardHeader> </CardHeader>
@@ -343,16 +347,16 @@ export function Tickets() {
</TextField> </TextField>
<TextField> <TextField>
<Label>Inhalt</Label> <Label>Inhalt</Label>
<TextArea minRows={5} value={kbEditDraft.content} onChange={(e) => setKbEditDraft((s) => ({ ...s, content: e.target.value }))} /> <TextArea rows={5} value={kbEditDraft.content} onChange={(e) => setKbEditDraft((s) => ({ ...s, content: e.target.value }))} />
</TextField> </TextField>
<div className="flex gap-2"> <div className="flex gap-2">
<Button color="primary" onPress={() => updateKbArticle(kbEditDraft.id)}>Aktualisieren</Button> <Button variant="primary" onPress={() => updateKbArticle(kbEditDraft.id)}>Aktualisieren</Button>
<Button variant="flat" onPress={() => setKbEditDraft(null)}>Abbrechen</Button> <Button variant="tertiary" onPress={() => setKbEditDraft(null)}>Abbrechen</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Neuer KB-Artikel</h3> <h3 className="text-base font-semibold">Neuer KB-Artikel</h3>
</CardHeader> </CardHeader>
@@ -367,9 +371,9 @@ export function Tickets() {
</TextField> </TextField>
<TextField> <TextField>
<Label>Inhalt</Label> <Label>Inhalt</Label>
<TextArea minRows={5} value={kbDraft.content} onChange={(e) => setKbDraft((s) => ({ ...s, content: e.target.value }))} /> <TextArea rows={5} value={kbDraft.content} onChange={(e) => setKbDraft((s) => ({ ...s, content: e.target.value }))} />
</TextField> </TextField>
<Button color="primary" onPress={saveKbArticle}>Artikel speichern</Button> <Button variant="primary" onPress={saveKbArticle}>Artikel speichern</Button>
</CardContent> </CardContent>
</Card> </Card>
)} )}

View File

@@ -9,7 +9,7 @@ export function Welcome() {
return ( return (
<SectionCard title="Willkommen" subtitle="Welcome-Embeds und Join-Nachrichten"> <SectionCard title="Willkommen" subtitle="Welcome-Embeds und Join-Nachrichten">
<div className="grid gap-5 xl:grid-cols-2"> <div className="grid gap-5 xl:grid-cols-2">
<Card className="surface-card"> <Card>
<CardHeader> <CardHeader>
<div> <div>
<CardTitle>Welcome konfigurieren</CardTitle> <CardTitle>Welcome konfigurieren</CardTitle>
@@ -24,8 +24,6 @@ export function Welcome() {
<TextField> <TextField>
<Label>Channel ID</Label> <Label>Channel ID</Label>
<Input <Input
variant="bordered"
size="lg"
placeholder="Channel ID fuer Willkommensnachrichten" placeholder="Channel ID fuer Willkommensnachrichten"
value={settings.welcomeConfig?.channelId || settings.welcomeChannelId || ''} value={settings.welcomeConfig?.channelId || settings.welcomeChannelId || ''}
onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), channelId: e.target.value } }))} onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), channelId: e.target.value } }))}
@@ -35,8 +33,6 @@ export function Welcome() {
<TextField> <TextField>
<Label>Titel</Label> <Label>Titel</Label>
<Input <Input
variant="bordered"
size="lg"
placeholder="Willkommen {user}!" placeholder="Willkommen {user}!"
value={settings.welcomeConfig?.embedTitle || ''} value={settings.welcomeConfig?.embedTitle || ''}
onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedTitle: e.target.value } }))} onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedTitle: e.target.value } }))}
@@ -46,7 +42,6 @@ export function Welcome() {
<TextField> <TextField>
<Label>Beschreibung</Label> <Label>Beschreibung</Label>
<TextArea <TextArea
variant="bordered"
placeholder="Beschreibung des Embeds" placeholder="Beschreibung des Embeds"
value={settings.welcomeConfig?.embedDescription || ''} value={settings.welcomeConfig?.embedDescription || ''}
onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedDescription: e.target.value } }))} onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedDescription: e.target.value } }))}
@@ -56,8 +51,6 @@ export function Welcome() {
<TextField> <TextField>
<Label>Footer</Label> <Label>Footer</Label>
<Input <Input
variant="bordered"
size="lg"
placeholder={new Date().getFullYear().toString()} placeholder={new Date().getFullYear().toString()}
value={settings.welcomeConfig?.embedFooter || ''} value={settings.welcomeConfig?.embedFooter || ''}
onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedFooter: e.target.value } }))} onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedFooter: e.target.value } }))}
@@ -66,13 +59,13 @@ export function Welcome() {
<Separator /> <Separator />
<Button size="lg" variant="light" className="accent-button" startContent={<Save size={16} />} onPress={() => saveSettingsPayload({ welcomeConfig: settings.welcomeConfig || {} }, 'Welcome gespeichert')}> <Button size="lg" variant="primary" onPress={() => saveSettingsPayload({ welcomeConfig: settings.welcomeConfig || {} }, 'Welcome gespeichert')}>
Speichern <Save size={16} /> Speichern
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
<Card className="surface-card"> <Card>
<CardHeader> <CardHeader>
<div> <div>
<CardTitle>Live Vorschau</CardTitle> <CardTitle>Live Vorschau</CardTitle>
@@ -80,22 +73,22 @@ export function Welcome() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
<Card className="surface-card-muted"> <Card className="bg-surface-tertiary">
<CardHeader> <CardHeader>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Sparkles size={16} className="accent-text" /> <Sparkles size={16} className="text-accent" />
<CardTitle>{settings.welcomeConfig?.embedTitle || 'Willkommen!'}</CardTitle> <CardTitle>{settings.welcomeConfig?.embedTitle || 'Willkommen!'}</CardTitle>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-default-700 dark:text-default-300">{settings.welcomeConfig?.embedDescription || 'Willkommen auf dem Server!'}</p> <p className="text-foreground/80">{settings.welcomeConfig?.embedDescription || 'Willkommen auf dem Server!'}</p>
{settings.welcomeConfig?.embedFooter && ( {settings.welcomeConfig?.embedFooter && (
<p className="text-small section-subtle">{settings.welcomeConfig.embedFooter}</p> <p className="text-sm text-muted">{settings.welcomeConfig.embedFooter}</p>
)} )}
</CardContent> </CardContent>
</Card> </Card>
<p className="text-small section-subtle"> <p className="text-sm text-muted">
Nutze {'{user}'} fuer den Benutzernamen und {'{server}'} fuer den Servernamen. Nutze {'{user}'} fuer den Benutzernamen und {'{server}'} fuer den Servernamen.
</p> </p>
</CardContent> </CardContent>