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

@@ -12,8 +12,8 @@ export function ActivityTile({ icon, label, value }: Props) {
<Card>
<CardContent className="flex flex-row items-center justify-between gap-4 p-4">
<div className="flex items-center gap-3">
<Chip color="primary" size="sm" variant="flat" startContent={icon}>
{label}
<Chip color="accent" size="sm" variant="soft">
{icon} {label}
</Chip>
<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) {
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">
<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} />}
</div>
<p className="text-small text-default-400">{message}</p>
<p className="text-sm text-muted">{message}</p>
</CardContent>
</Card>
);

View File

@@ -8,14 +8,14 @@ type Props = {
export function ErrorState({ message = 'Ein Fehler ist aufgetreten', onRetry }: Props) {
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">
<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} />
</div>
<p className="text-small text-default-500">{message}</p>
<p className="text-sm text-muted">{message}</p>
{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
</button>
)}

View File

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

View File

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

View File

@@ -10,11 +10,11 @@ type Props = {
export function SectionCard({ title, subtitle, children, action }: Props) {
return (
<Card className="surface-card-strong">
<Card className="bg-surface-secondary">
<CardHeader className="flex items-start justify-between gap-4">
<div className="min-w-0 flex flex-col gap-1">
<CardTitle>{title}</CardTitle>
{subtitle && <CardDescription className="section-subtle">{subtitle}</CardDescription>}
{subtitle && <CardDescription>{subtitle}</CardDescription>}
</div>
{action && <div className="shrink-0">{action}</div>}
</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';
type Props = {
@@ -6,27 +6,33 @@ type Props = {
label: string;
value: string | number;
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 chipColor = color === 'default' ? 'default' : color;
const iconClasses: Record<NonNullable<Props['color']>, string> = {
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 (
<Card className="surface-card">
<CardContent className="flex flex-col gap-2 p-4">
<div className="flex items-center justify-between">
<Chip color={chipColor} size="sm" variant="flat" startContent={icon} className={color === 'primary' ? 'accent-chip' : undefined}>
{label}
</Chip>
{trend && (
<span className={`text-tiny font-medium ${trend.startsWith('+') ? 'text-success-400' : trend.startsWith('-') ? 'text-danger-400' : 'text-default-400'}`}>
{trend}
</span>
)}
<Card>
<CardContent className="flex flex-row items-center gap-3 p-3">
<div className={`flex size-8 shrink-0 items-center justify-center rounded-lg ${iconClasses[color]}`}>
{icon}
</div>
<div className="text-2xl font-bold tracking-tight">{value}</div>
<div className="text-tiny section-subtle">{label}</div>
<div className="min-w-0 flex-1">
<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>
</Card>
);