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>
98 lines
4.5 KiB
TypeScript
98 lines
4.5 KiB
TypeScript
import { Card, CardContent, CardHeader, Input, Button, Chip, Switch, Separator, TextField, Label } from '@heroui/react';
|
|
import { RadioTower, Save, Trash2, Plus, Activity as ActivityIcon } from 'lucide-react';
|
|
import { useApp } from '../context/AppContext';
|
|
import { SectionCard } from '../components/shared/SectionCard';
|
|
import type { StatusService } from '../types';
|
|
|
|
export function Statuspage() {
|
|
const { statusDraft, setStatusDraft, saveStatuspage, statusServiceDraft, setStatusServiceDraft, addStatusService, deleteStatusService } = useApp();
|
|
|
|
const services = ((statusDraft?.services || []) as StatusService[]);
|
|
|
|
return (
|
|
<SectionCard title="Statuspage" subtitle="Statusseite und Service-Liste verwalten">
|
|
<div className="grid gap-5 xl:grid-cols-[420px_1fr]">
|
|
<Card>
|
|
<CardHeader className="px-5 pt-5 pb-0">
|
|
<h3 className="text-base font-semibold">Konfiguration</h3>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col gap-4 p-5">
|
|
<Switch isSelected={statusDraft?.enabled !== false} onChange={(v) => setStatusDraft((s) => ({ ...(s || {}), enabled: v }))}>
|
|
<div className="flex items-center gap-2"><RadioTower size={16} /> Statuspage aktiv</div>
|
|
</Switch>
|
|
|
|
<TextField>
|
|
<Label>Channel ID</Label>
|
|
<Input
|
|
placeholder="Channel für Status-Updates"
|
|
value={statusDraft?.channelId || ''}
|
|
onChange={(e) => setStatusDraft((s) => ({ ...(s || {}), channelId: e.target.value }))}
|
|
/>
|
|
</TextField>
|
|
|
|
<TextField>
|
|
<Label>Intervall (ms)</Label>
|
|
<Input
|
|
type="number"
|
|
value={String(statusDraft?.intervalMs || 60000)}
|
|
onChange={(e) => setStatusDraft((s) => ({ ...(s || {}), intervalMs: Number(e.target.value || 60000) }))}
|
|
/>
|
|
</TextField>
|
|
|
|
<Button variant="primary" onPress={saveStatuspage}>
|
|
<Save size={16} /> Statuspage speichern
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="px-5 pt-5 pb-0">
|
|
<h3 className="text-base font-semibold">Services ({services.length})</h3>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col gap-3 p-5">
|
|
{services.length ? services.map((service) => (
|
|
<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="size-2 rounded-full shrink-0" />
|
|
<div className="min-w-0">
|
|
<span className="font-medium">{service.name || 'Service'}</span>
|
|
<span className="ml-2 text-muted text-xs truncate">{service.url || ''}</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Chip size="sm" variant="soft" color={
|
|
service.status === 'operational' ? 'success' :
|
|
service.status === 'degraded' ? 'warning' :
|
|
service.status === 'down' ? 'danger' : 'default'
|
|
}>{service.status || 'unknown'}</Chip>
|
|
<Button isIconOnly size="sm" variant="danger-soft" onPress={() => deleteStatusService(service.id)}>
|
|
<Trash2 size={14} />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)) : (
|
|
<div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
|
|
<ActivityIcon size={20} />
|
|
Keine Services
|
|
</div>
|
|
)}
|
|
|
|
<Separator />
|
|
|
|
<div>
|
|
<h4 className="text-sm font-semibold mb-2">Service hinzufügen</h4>
|
|
<div className="flex flex-col gap-2">
|
|
<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 }))} />
|
|
<Button size="sm" variant="primary" onPress={addStatusService}>
|
|
<Plus size={14} /> Hinzufügen
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</SectionCard>
|
|
);
|
|
}
|