simplify heroui layouts and add sonarqube workflow
Some checks failed
Deploy Discord Bot / deploy (push) Has been cancelled
SonarQube / sonar (push) Has been cancelled

This commit is contained in:
Pepe44DEV
2026-07-01 15:46:04 +02:00
parent da72f49255
commit a604fb494f
10 changed files with 237 additions and 227 deletions

View File

@@ -0,0 +1,22 @@
name: SonarQube
on:
push:
branches:
- main
- master
pull_request:
jobs:
sonar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: sonarsource/sonarqube-scan-action@v5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: http://10.0.0.15:9001

View File

@@ -1,19 +1,20 @@
import { useState } from 'react'; import { useState } from 'react';
import { Avatar, Button, Chip, ScrollShadow, Tooltip } from '@heroui/react';
import { import {
ChevronLeft, ChevronRight, LogOut, Server, PanelLeftClose, PanelLeft, Avatar, Button, Card, CardContent, ScrollShadow, Tooltip,
Activity, AudioLines, CalendarDays, ClipboardList, Home, Select, SelectTrigger, SelectValue, SelectPopover, ListBox, ListBoxItem
LogIn, Music, Puzzle, RadioTower, Settings, Shield, Sparkles, } from '@heroui/react';
Tag, Ticket, Wrench, Bot import {
LogOut, PanelLeftClose, PanelLeft, Activity, AudioLines, CalendarDays,
ClipboardList, Home, LogIn, Music, Puzzle, RadioTower, Settings,
Shield, Sparkles, Tag, Ticket, Wrench
} from 'lucide-react'; } from 'lucide-react';
import { useApp } from '../../context/AppContext'; import { useApp } from '../../context/AppContext';
import { guildIconUrl } from '../../utils/formatters';
const navGroups = [ const navGroups = [
{ {
label: 'Dashboard', label: 'Dashboard',
items: [ items: [
{ key: 'overview', label: 'Übersicht', icon: <Home size={18} /> }, { key: 'overview', label: 'Uebersicht', icon: <Home size={18} /> },
] ]
}, },
{ {
@@ -61,44 +62,41 @@ export function Sidebar() {
const { user, guilds, currentGuildId, section, setCurrentGuildId, setSection, handleLogout } = useApp(); const { user, guilds, currentGuildId, section, setCurrentGuildId, setSection, handleLogout } = useApp();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const selectedGuild = guilds.find((g) => g.id === currentGuildId);
return ( return (
<aside className={`flex h-full flex-col border-r border-default-100 bg-gradient-to-b from-default-50/40 to-background transition-all duration-200 ${collapsed ? 'w-16' : 'w-64'}`}> <aside className={`flex h-full flex-col transition-all duration-200 ${collapsed ? 'w-20' : 'w-72'}`}>
<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' : ''}`}>
<Avatar name="Papo" radius="lg" />
{!collapsed && ( {!collapsed && (
<> <div className="min-w-0">
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-primary-400 to-primary-600 text-lg font-bold text-white shadow-lg shadow-primary-500/25"> <div className="text-base font-bold">Papo</div>
P <div className="text-[10px] uppercase tracking-widest text-default-400">Dashboard</div>
</div>
<div className="min-w-0">
<div className="text-base font-bold">Papo</div>
<div className="text-[10px] uppercase tracking-widest text-default-400">Dashboard</div>
</div>
</>
)}
{collapsed && (
<div className="flex size-10 items-center justify-center rounded-xl bg-gradient-to-br from-primary-400 to-primary-600 text-lg font-bold text-white shadow-lg shadow-primary-500/25">
P
</div> </div>
)} )}
</div> </div>
<div className={`px-3 pb-2 ${collapsed ? 'px-2' : ''}`}> <div className="px-3 pb-2">
<select <Select
className={`w-full rounded-xl border border-default-200 bg-default-50 text-sm text-foreground outline-none transition-colors focus:border-primary-400 ${collapsed ? 'px-1 py-2 text-center text-[10px]' : 'px-3 py-2'}`} aria-label="Guild auswaehlen"
value={currentGuildId} selectedKey={currentGuildId}
onChange={(e) => setCurrentGuildId(e.target.value)} onSelectionChange={(key) => {
title={selectedGuild?.name} if (typeof key === 'string') setCurrentGuildId(key);
}}
> >
{guilds.map((g) => ( <SelectTrigger>
<option key={g.id} value={g.id}>{collapsed ? g.name.slice(0, 2) : g.name}</option> <SelectValue />
))} </SelectTrigger>
</select> <SelectPopover>
<ListBox>
{guilds.map((g) => (
<ListBoxItem key={g.id} id={g.id} textValue={g.name}>
{collapsed ? g.name.slice(0, 2) : g.name}
</ListBoxItem>
))}
</ListBox>
</SelectPopover>
</Select>
</div> </div>
<div className="h-px bg-default-200 mx-3" />
<ScrollShadow className="flex-1 px-2 py-2" hideScrollBar> <ScrollShadow className="flex-1 px-2 py-2" hideScrollBar>
<nav className="flex flex-col gap-4"> <nav className="flex flex-col gap-4">
{navGroups.map((group) => ( {navGroups.map((group) => (
@@ -116,9 +114,10 @@ export function Sidebar() {
return ( return (
<Tooltip key={item.key} content={collapsed ? item.label : ''} placement="right" offset={8}> <Tooltip key={item.key} content={collapsed ? item.label : ''} placement="right" offset={8}>
<Button <Button
className={`h-9 justify-start gap-3 px-2 ${isActive ? 'bg-primary-500/15 text-primary-400 font-semibold' : 'text-default-500 hover:text-foreground hover:bg-default-100/40'}`} className="h-9 justify-start gap-3 px-2"
color={isActive ? 'primary' : 'default'}
radius="lg" radius="lg"
variant="light" variant={isActive ? 'flat' : 'light'}
size="sm" size="sm"
startContent={item.icon} startContent={item.icon}
onPress={() => setSection(item.key)} onPress={() => setSection(item.key)}
@@ -140,9 +139,10 @@ export function Sidebar() {
)} )}
<Tooltip content={collapsed ? 'Admin' : ''} placement="right" offset={8}> <Tooltip content={collapsed ? 'Admin' : ''} placement="right" offset={8}>
<Button <Button
className={`h-9 justify-start gap-3 px-2 ${section === 'admin' ? 'bg-warning-500/15 text-warning-400 font-semibold' : 'text-default-500 hover:text-foreground hover:bg-default-100/40'}`} className="h-9 justify-start gap-3 px-2"
color={section === 'admin' ? 'warning' : 'default'}
radius="lg" radius="lg"
variant="light" variant={section === 'admin' ? 'flat' : 'light'}
size="sm" size="sm"
startContent={<Wrench size={18} />} startContent={<Wrench size={18} />}
onPress={() => setSection('admin')} onPress={() => setSection('admin')}
@@ -155,8 +155,6 @@ export function Sidebar() {
</nav> </nav>
</ScrollShadow> </ScrollShadow>
<div className="h-px bg-default-200 mx-3" />
<div className="p-3 flex flex-col gap-2"> <div className="p-3 flex flex-col gap-2">
<Button <Button
isIconOnly isIconOnly
@@ -169,25 +167,24 @@ export function Sidebar() {
{collapsed ? <PanelLeft size={16} /> : <PanelLeftClose size={16} />} {collapsed ? <PanelLeft size={16} /> : <PanelLeftClose size={16} />}
</Button> </Button>
<div className={`flex items-center gap-3 rounded-xl border border-default-100 bg-default-50/20 p-2 ${collapsed ? 'justify-center' : ''}`}> <Card>
<Avatar name={user?.username} size="sm" className="shrink-0" /> <CardContent className={`flex items-center gap-3 p-2 ${collapsed ? 'justify-center' : ''}`}>
{!collapsed && ( <Avatar name={user?.username} size="sm" className="shrink-0" />
<> {!collapsed && (
<div className="flex-1 min-w-0"> <>
<div className="truncate text-xs font-semibold">{user?.username}</div> <div className="flex-1 min-w-0">
<div className="flex items-center gap-1 text-[10px] text-success-400"> <div className="truncate text-xs font-semibold">{user?.username}</div>
<div className="size-1.5 rounded-full bg-success-400" /> <div className="text-[10px] text-default-400">Angemeldet</div>
Online
</div> </div>
</div> <Tooltip content="Abmelden" placement="top">
<Tooltip content="Abmelden" placement="top"> <Button isIconOnly color="danger" radius="lg" size="sm" variant="light" onPress={handleLogout}>
<Button isIconOnly color="danger" radius="lg" size="sm" variant="light" onPress={handleLogout}> <LogOut size={14} />
<LogOut size={14} /> </Button>
</Button> </Tooltip>
</Tooltip> </>
</> )}
)} </CardContent>
</div> </Card>
</div> </div>
</aside> </aside>
); );

View File

@@ -1,4 +1,4 @@
import { Card, CardContent } from '@heroui/react'; import { Card, CardContent, Chip } from '@heroui/react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
type Props = { type Props = {
@@ -9,14 +9,13 @@ type Props = {
export function ActivityTile({ icon, label, value }: Props) { export function ActivityTile({ icon, label, value }: Props) {
return ( return (
<Card className="border border-default-100 bg-default-50/20"> <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">
<div className="flex size-10 items-center justify-center rounded-xl bg-primary-500/10 text-primary-400"> <Chip color="primary" size="sm" variant="flat" startContent={icon}>
{icon} {label}
</div> </Chip>
<div> <div>
<div className="text-tiny uppercase tracking-widest text-default-500">{label}</div>
<div className="text-2xl font-black">{value}</div> <div className="text-2xl font-black">{value}</div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { Card, CardHeader, CardContent } from '@heroui/react'; import { Card, CardHeader, CardContent, CardTitle } from '@heroui/react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
type Props = { type Props = {
@@ -8,11 +8,11 @@ type Props = {
export function FormPanel({ title, children }: Props) { export function FormPanel({ title, children }: Props) {
return ( return (
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader>
<h3 className="text-base font-semibold">{title}</h3> <CardTitle>{title}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4 px-5 py-5">{children}</CardContent> <CardContent className="flex flex-col gap-4">{children}</CardContent>
</Card> </Card>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Card, CardHeader, CardContent } from '@heroui/react'; import { Card, CardHeader, CardContent, CardTitle } from '@heroui/react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
type Props = { type Props = {
@@ -9,11 +9,11 @@ type Props = {
export function ListPanel({ title, children, className }: Props) { export function ListPanel({ title, children, className }: Props) {
return ( return (
<Card className={`border border-default-100 bg-default-50/20 ${className ?? ''}`}> <Card className={className ?? ''}>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader>
<h3 className="text-base font-semibold">{title}</h3> <CardTitle>{title}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="px-5 py-5"> <CardContent>
{children} {children}
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -10,15 +10,15 @@ type Props = {
export function SectionCard({ title, subtitle, children, action }: Props) { export function SectionCard({ title, subtitle, children, action }: Props) {
return ( return (
<Card className="border border-default-100 bg-gradient-to-b from-default-50/40 to-background"> <Card>
<CardHeader className="flex items-start justify-between gap-4 px-6 pt-6 pb-0"> <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>{subtitle}</CardDescription>} {subtitle && <CardDescription>{subtitle}</CardDescription>}
</div> </div>
{action && <div className="shrink-0">{action}</div>} {action && <div className="shrink-0">{action}</div>}
</CardHeader> </CardHeader>
<CardContent className="px-6 py-5">{children}</CardContent> <CardContent>{children}</CardContent>
</Card> </Card>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Card, CardContent } from '@heroui/react'; import { Card, CardContent, Chip } from '@heroui/react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
type Props = { type Props = {
@@ -10,13 +10,15 @@ type Props = {
}; };
export function StatCard({ icon, label, value, trend, color = 'primary' }: Props) { export function StatCard({ icon, label, value, trend, color = 'primary' }: Props) {
const chipColor = color === 'default' ? 'default' : color;
return ( return (
<Card className="border border-default-100 bg-gradient-to-br from-default-50/20 to-background hover:border-default-200 transition-all"> <Card>
<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={`flex size-9 items-center justify-center rounded-xl bg-${color}-500/10 text-${color}-400`}> <Chip color={chipColor} size="sm" variant="flat" startContent={icon}>
{icon} {label}
</div> </Chip>
{trend && ( {trend && (
<span className={`text-tiny font-medium ${trend.startsWith('+') ? 'text-success-400' : trend.startsWith('-') ? 'text-danger-400' : 'text-default-400'}`}> <span className={`text-tiny font-medium ${trend.startsWith('+') ? 'text-success-400' : trend.startsWith('-') ? 'text-danger-400' : 'text-default-400'}`}>
{trend} {trend}
@@ -24,7 +26,7 @@ export function StatCard({ icon, label, value, trend, color = 'primary' }: Props
)} )}
</div> </div>
<div className="text-2xl font-bold tracking-tight">{value}</div> <div className="text-2xl font-bold tracking-tight">{value}</div>
<div className="text-tiny uppercase tracking-widest text-default-500">{label}</div> <div className="text-tiny text-default-500">{label}</div>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@@ -1,15 +1,15 @@
import { Card, CardContent, CardHeader, Avatar, Chip, Button, Separator, ScrollShadow } from '@heroui/react'; import { Card, CardContent, CardHeader, Avatar, Chip, Button, ScrollShadow } from '@heroui/react';
import { import {
Bot, UserRound, CalendarDays, Users, Cable, Ticket, Shield, MessageSquare, Bot, CalendarDays, Users, Ticket, Shield, MessageSquare,
Command, ChevronRight, Activity, Clock, ArrowUpRight, RefreshCw, Send, ChevronRight, Activity, Clock, ArrowUpRight, RefreshCw, Send,
Settings, Trash2, Sparkles, Home, Server, Hash, Gauge, Zap, Bell, Tag Settings, Sparkles, Hash, Gauge, Zap, Bell, Tag, Command
} from 'lucide-react'; } from 'lucide-react';
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';
export function Dashboard() { export function Dashboard() {
const { guildInfo, guilds, currentGuildId, overview, activity, logs, setSection, section } = useApp(); const { guildInfo, guilds, currentGuildId, overview, activity, logs, setSection } = useApp();
const selectedGuild = guilds.find((g) => g.id === currentGuildId); const selectedGuild = guilds.find((g) => g.id === currentGuildId);
const moduleFlags = guildInfo?.modules || {}; const moduleFlags = guildInfo?.modules || {};
@@ -17,13 +17,13 @@ export function Dashboard() {
{ key: 'tickets', label: 'Ticket Panel senden', icon: <Send size={16} />, color: 'primary' as const }, { key: 'tickets', label: 'Ticket Panel senden', icon: <Send size={16} />, color: 'primary' as const },
{ key: 'serverstats', label: 'Sync starten', icon: <RefreshCw size={16} />, color: 'success' as const }, { key: 'serverstats', label: 'Sync starten', icon: <RefreshCw size={16} />, color: 'success' as const },
{ key: 'modules', label: 'Module aktualisieren', icon: <Zap size={16} />, color: 'warning' as const }, { key: 'modules', label: 'Module aktualisieren', icon: <Zap size={16} />, color: 'warning' as const },
{ key: 'settings', label: 'Einstellungen öffnen', icon: <Settings size={16} />, color: 'default' as const }, { key: 'settings', label: 'Einstellungen oeffnen', icon: <Settings size={16} />, color: 'default' as const },
]; ];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Card className="border border-default-100 bg-gradient-to-r from-primary-500/5 via-default-50/40 to-background overflow-hidden"> <Card>
<CardContent className="flex flex-col gap-6 p-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)} /> <Avatar className="size-20 shrink-0" radius="lg" src={guildIconUrl(selectedGuild)} />
<div className="min-w-0"> <div className="min-w-0">
@@ -65,49 +65,23 @@ 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="border border-default-100 bg-gradient-to-b from-default-50/40 to-background"> <Card>
<CardHeader className="flex items-center justify-between px-5 pt-5 pb-0"> <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-tiny text-default-400">Live Statistiken</p>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-3 p-5"> <CardContent className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="grid grid-cols-2 gap-3"> <StatCard icon={<MessageSquare size={16} />} label="Nachrichten" value={activity?.messages24h ?? 0} />
<div className="rounded-xl border border-default-100 bg-default-50/20 p-4"> <StatCard icon={<Command size={16} />} label="Commands" value={activity?.commands24h ?? 0} color="success" />
<div className="flex items-center gap-2 text-tiny uppercase tracking-widest text-default-500"> <StatCard icon={<Shield size={16} />} label="Automod" value={activity?.automod24h ?? 0} color="warning" />
<MessageSquare size={14} className="text-primary-400" /> Nachrichten <StatCard icon={<Users size={16} />} label="Neue User" value={activity?.newUsers24h ?? 0} />
</div>
<div className="mt-1 text-3xl font-black">{activity?.messages24h ?? 0}</div>
<div className="text-tiny text-default-400">in den letzten 24h</div>
</div>
<div className="rounded-xl border border-default-100 bg-default-50/20 p-4">
<div className="flex items-center gap-2 text-tiny uppercase tracking-widest text-default-500">
<Command size={14} className="text-success-400" /> Commands
</div>
<div className="mt-1 text-3xl font-black">{activity?.commands24h ?? 0}</div>
<div className="text-tiny text-default-400">in den letzten 24h</div>
</div>
<div className="rounded-xl border border-default-100 bg-default-50/20 p-4">
<div className="flex items-center gap-2 text-tiny uppercase tracking-widest text-default-500">
<Shield size={14} className="text-warning-400" /> Automod
</div>
<div className="mt-1 text-3xl font-black">{activity?.automod24h ?? 0}</div>
<div className="text-tiny text-default-400">Aktionen (24h)</div>
</div>
<div className="rounded-xl border border-default-100 bg-default-50/20 p-4">
<div className="flex items-center gap-2 text-tiny uppercase tracking-widest text-default-500">
<Users size={14} className="text-secondary-400" /> Neue User
</div>
<div className="mt-1 text-3xl font-black">{activity?.newUsers24h ?? 0}</div>
<div className="text-tiny text-default-400">Beitritte (24h)</div>
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border border-default-100 bg-gradient-to-b from-default-50/40 to-background"> <Card>
<CardHeader className="flex items-center justify-between px-5 pt-5 pb-0"> <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-tiny text-default-400">Letzte Ereignisse</p>
@@ -116,36 +90,37 @@ export function Dashboard() {
Alle Alle
</Button> </Button>
</CardHeader> </CardHeader>
<CardContent className="p-5"> <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) => (
<div key={`${log.timestamp}-${i}`} className="flex items-start gap-3 rounded-xl border border-default-100 bg-default-50/20 p-3"> <Card key={`${log.timestamp}-${i}`}>
<div className={`mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-lg ${ <CardContent className="flex items-start gap-3 p-3">
log.level === 'error' ? 'bg-danger-500/10 text-danger-400' : <div className={`mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-lg ${
log.level === 'warn' ? 'bg-warning-500/10 text-warning-400' : log.level === 'error' ? 'bg-danger-500/10 text-danger-400' :
'bg-primary-500/10 text-primary-400' log.level === 'warn' ? 'bg-warning-500/10 text-warning-400' :
}`}> 'bg-primary-500/10 text-primary-400'
{log.level === 'error' ? <Shield size={12} /> : }`}>
log.level === 'warn' ? <Bell size={12} /> : {log.level === 'error' ? <Shield size={12} /> :
<Activity size={12} />} log.level === 'warn' ? <Bell size={12} /> :
</div> <Activity size={12} />}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Chip
color={log.level === 'error' ? 'danger' : log.level === 'warn' ? 'warning' : 'default'}
size="sm"
variant="flat"
className="h-5"
>
{(log.level || 'info').toUpperCase()}
</Chip>
<span className="text-tiny text-default-400">{formatDate(log.timestamp)}</span>
</div> </div>
<p className="mt-1 text-small text-foreground/80"> <div className="min-w-0 flex-1">
{log.category ? <span className="text-default-500">[{log.category}] </span> : ''}{log.message || '-'} <div className="flex items-center gap-2">
</p> <Chip
</div> color={log.level === 'error' ? 'danger' : log.level === 'warn' ? 'warning' : 'default'}
</div> size="sm"
variant="flat"
>
{(log.level || 'info').toUpperCase()}
</Chip>
<span className="text-tiny text-default-400">{formatDate(log.timestamp)}</span>
</div>
<p className="mt-1 text-small text-foreground/80">
{log.category ? <span className="text-default-500">[{log.category}] </span> : ''}{log.message || '-'}
</p>
</div>
</CardContent>
</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-small text-default-400">
<Activity size={20} /> <Activity size={20} />
@@ -156,12 +131,12 @@ export function Dashboard() {
</CardContent> </CardContent>
</Card> </Card>
<Card className="border border-default-100 bg-gradient-to-b from-default-50/40 to-background"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <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-tiny text-default-400">Schnellzugriff</p>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-2 p-5"> <CardContent className="flex flex-col gap-2">
{quickActions.map((action) => ( {quickActions.map((action) => (
<Button <Button
key={action.key} key={action.key}
@@ -182,11 +157,11 @@ 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 className="border border-default-100 bg-default-50/20 transition-all hover:border-primary-300 hover:shadow-md" onPress={() => setSection(key)}> <Card key={key} isPressable onPress={() => setSection(key)}>
<CardContent className="flex flex-col items-start gap-2 p-4"> <CardContent className="flex flex-col items-start gap-2">
<div className="flex size-10 items-center justify-center rounded-xl bg-primary-500/10 text-primary-400"> <Chip color="primary" size="sm" variant="flat" startContent={item.icon}>
{item.icon} {item.label}
</div> </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-tiny text-default-400">Modul verwalten</div>
</CardContent> </CardContent>

View File

@@ -1,5 +1,5 @@
import { Card, CardContent, CardHeader, Input, TextArea, Button, Chip, Switch, Separator, TextField, Label } from '@heroui/react'; import { Avatar, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, TextArea, Button, Chip, Switch, Separator, TextField, Label } from '@heroui/react';
import { LogIn, UserRound, Eye, Save, Send, RefreshCw } 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';
@@ -7,13 +7,16 @@ export function SupportLogin() {
const { supportLogin, setSupportLogin, saveSupportLogin } = useApp(); const { supportLogin, setSupportLogin, saveSupportLogin } = useApp();
return ( return (
<SectionCard title="Support Login" subtitle="Login-Panel f<EFBFBD>r 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="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader>
<h3 className="text-base font-semibold">Panel-Konfiguration</h3> <div>
<CardTitle>Panel-Konfiguration</CardTitle>
<CardDescription>Texte und Zielkanal mit normalen HeroUI-Feldern pflegen.</CardDescription>
</div>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4 p-5"> <CardContent className="flex flex-col gap-4">
<Switch <Switch
isSelected={supportLogin?.config?.autoRefresh !== false} isSelected={supportLogin?.config?.autoRefresh !== false}
onChange={(v) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, autoRefresh: v } } : s)} onChange={(v) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, autoRefresh: v } } : s)}
@@ -76,41 +79,48 @@ export function SupportLogin() {
</Card> </Card>
<div className="space-y-4"> <div className="space-y-4">
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader>
<h3 className="text-base font-semibold">Live Vorschau</h3> <div>
<CardTitle>Live Vorschau</CardTitle>
<CardDescription>Panel in einer normalen HeroUI-Karte.</CardDescription>
</div>
</CardHeader> </CardHeader>
<CardContent className="p-5"> <CardContent>
<div className="rounded-xl border border-default-100 bg-gradient-to-b from-default-50/40 to-background p-4"> <Card>
<div className="flex items-center gap-3 mb-3"> <CardHeader>
<div className="flex size-10 items-center justify-center rounded-xl bg-primary-500/10 text-primary-400"> <div className="flex items-center gap-2">
<LogIn size={20} /> <LogIn size={16} />
<div>
<CardTitle>{supportLogin?.config?.title || 'Support Login'}</CardTitle>
<CardDescription>{supportLogin?.config?.description || 'Melde dich als Support an/ab.'}</CardDescription>
</div>
</div> </div>
<div> </CardHeader>
<div className="font-semibold">{supportLogin?.config?.title || 'Support Login'}</div> <CardContent className="flex gap-2">
<div className="text-tiny text-default-400">{supportLogin?.config?.description || 'Melde dich als Support an/ab.'}</div>
</div>
</div>
<div className="flex gap-2">
<Button size="sm" color="primary" variant="flat">{supportLogin?.config?.loginLabel || 'Login'}</Button> <Button size="sm" color="primary" variant="flat">{supportLogin?.config?.loginLabel || 'Login'}</Button>
<Button size="sm" variant="flat">{supportLogin?.config?.logoutLabel || 'Logout'}</Button> <Button size="sm" variant="flat">{supportLogin?.config?.logoutLabel || 'Logout'}</Button>
</div> </CardContent>
</div> </Card>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader>
<h3 className="text-base font-semibold">Aktive Supporter</h3> <div>
<CardTitle>Aktive Supporter</CardTitle>
<CardDescription>Aktueller Status aus der Guild.</CardDescription>
</div>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-2 p-5"> <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) => (
<div key={i} className="flex items-center gap-3 rounded-xl border border-default-100 bg-default-50/30 px-4 py-3 text-small"> <Card key={i}>
<div className="size-2 rounded-full bg-success-400" /> <CardContent className="flex items-center gap-3">
<Avatar name={s.username?.[0]} size="sm" className="size-6" /> <Avatar name={s.username?.[0]} 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="flat" color="success" className="ml-auto">Online</Chip>
</div> </CardContent>
</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-tiny text-default-400">
<UserRound size={20} /> <UserRound size={20} />

View File

@@ -1,5 +1,5 @@
import { Card, CardContent, CardHeader, Input, TextArea, Button, Chip, Switch, Separator, TextField, Label } from '@heroui/react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, Input, TextArea, Button, Switch, Separator, TextField, Label } from '@heroui/react';
import { Sparkles, Save, Eye } from 'lucide-react'; import { Sparkles, Save } 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';
@@ -9,11 +9,14 @@ 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="border border-default-100 bg-default-50/20"> <Card>
<CardHeader className="px-5 pt-5 pb-0"> <CardHeader>
<h3 className="text-base font-semibold">Welcome konfigurieren</h3> <div>
<CardTitle>Welcome konfigurieren</CardTitle>
<CardDescription>Standardfelder fuer das Welcome-Embed.</CardDescription>
</div>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4 p-5"> <CardContent className="flex flex-col gap-4">
<Switch isSelected={settings.welcomeConfig?.enabled !== false} onChange={(v) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), enabled: v } }))}> <Switch isSelected={settings.welcomeConfig?.enabled !== false} onChange={(v) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), enabled: v } }))}>
<div className="flex items-center gap-2"><Sparkles size={16} /> Welcome aktiv</div> <div className="flex items-center gap-2"><Sparkles size={16} /> Welcome aktiv</div>
</Switch> </Switch>
@@ -21,7 +24,7 @@ export function Welcome() {
<TextField> <TextField>
<Label>Channel ID</Label> <Label>Channel ID</Label>
<Input <Input
placeholder="Channel ID f<EFBFBD>r 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 } }))}
/> />
@@ -62,32 +65,34 @@ export function Welcome() {
</CardContent> </CardContent>
</Card> </Card>
<div className="flex flex-col gap-4"> <Card>
<Card className="border border-default-100 bg-default-50/20"> <CardHeader>
<CardHeader className="px-5 pt-5 pb-0"> <div>
<h3 className="text-base font-semibold">Live Vorschau</h3> <CardTitle>Live Vorschau</CardTitle>
</CardHeader> <CardDescription>Normale HeroUI-Karten ohne zusaetzliche Huelle.</CardDescription>
<CardContent className="p-5"> </div>
<div className="rounded-xl border border-default-100 bg-gradient-to-b from-default-50/40 to-background p-4"> </CardHeader>
<div className="flex items-start gap-3"> <CardContent className="flex flex-col gap-4">
<div className="flex size-10 items-center justify-center rounded-xl bg-primary-500/10 text-primary-400"> <Card>
<Sparkles size={20} /> <CardHeader>
</div> <div className="flex items-center gap-2">
<div className="min-w-0 flex-1"> <Sparkles size={16} />
<div className="font-semibold text-lg">{settings.welcomeConfig?.embedTitle || 'Willkommen!'}</div> <CardTitle>{settings.welcomeConfig?.embedTitle || 'Willkommen!'}</CardTitle>
<p className="mt-1 text-small text-default-500">{settings.welcomeConfig?.embedDescription || 'Willkommen auf dem Server!'}</p>
{settings.welcomeConfig?.embedFooter && (
<p className="mt-2 text-tiny text-default-400">{settings.welcomeConfig.embedFooter}</p>
)}
</div>
</div> </div>
</div> </CardHeader>
<p className="mt-3 text-tiny text-default-400"> <CardContent>
Nutze {'{user}'} f<EFBFBD>r den Benutzernamen und {'{server}'} f<EFBFBD>r den Servernamen. <p>{settings.welcomeConfig?.embedDescription || 'Willkommen auf dem Server!'}</p>
</p> {settings.welcomeConfig?.embedFooter && (
</CardContent> <p className="text-small text-default-500">{settings.welcomeConfig.embedFooter}</p>
</Card> )}
</div> </CardContent>
</Card>
<p className="text-small text-default-500">
Nutze {'{user}'} fuer den Benutzernamen und {'{server}'} fuer den Servernamen.
</p>
</CardContent>
</Card>
</div> </div>
</SectionCard> </SectionCard>
); );