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