generated from Dicken/dickendock
Commit 6: Adminbereich (TanStack Router, 8 Menüpunkte, Scan-Logs)
This commit is contained in:
84
apps/frontend/src/routes/admin/DashboardPage.tsx
Normal file
84
apps/frontend/src/routes/admin/DashboardPage.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useServices } from "../../hooks/useServices.js";
|
||||
import { useDevices } from "../../hooks/useDevices.js";
|
||||
import { useCategories } from "../../hooks/useCategories.js";
|
||||
import { AdminPageHeader } from "./AdminPageHeader.js";
|
||||
|
||||
function StatCard({ label, value }: { label: string; value: number | string }) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-black/10 bg-white p-5 dark:border-white/10 dark:bg-white/5">
|
||||
<div className="text-3xl font-semibold text-black dark:text-white">{value}</div>
|
||||
<div className="mt-1 text-sm text-black/50 dark:text-white/50">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
const { data: services } = useServices();
|
||||
const { data: devices } = useDevices();
|
||||
const { data: categories } = useCategories();
|
||||
|
||||
const onlineDevices = devices?.filter((d) => d.online).length ?? 0;
|
||||
const favoriteServices = services?.filter((s) => s.favorite).length ?? 0;
|
||||
|
||||
const recentlyScanned = [...(devices ?? [])]
|
||||
.filter((d) => d.lastScan)
|
||||
.sort((a, b) => (b.lastScan ?? "").localeCompare(a.lastScan ?? ""))
|
||||
.slice(0, 5);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminPageHeader
|
||||
title="Dashboard"
|
||||
description="Überblick über dein Homelab."
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<StatCard label="Geräte" value={devices?.length ?? 0} />
|
||||
<StatCard label="davon online" value={onlineDevices} />
|
||||
<StatCard label="Dienste" value={services?.length ?? 0} />
|
||||
<StatCard label="Favoriten" value={favoriteServices} />
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<h2 className="mb-3 text-sm font-semibold text-black/60 dark:text-white/60">
|
||||
Zuletzt gescannte Geräte
|
||||
</h2>
|
||||
{recentlyScanned.length === 0 ? (
|
||||
<p className="text-sm text-black/40 dark:text-white/40">
|
||||
Noch keine Scans durchgeführt. Starte einen Scan unter „Geräte“ oder „Scanner“.
|
||||
</p>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-2xl border border-black/10 dark:border-white/10">
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
{recentlyScanned.map((device) => (
|
||||
<tr
|
||||
key={device.id}
|
||||
className="border-b border-black/5 last:border-0 dark:border-white/5"
|
||||
>
|
||||
<td className="px-4 py-3 font-medium text-black dark:text-white">
|
||||
{device.hostname}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-black/40 dark:text-white/40">{device.ip}</td>
|
||||
<td className="px-4 py-3 text-black/40 dark:text-white/40">
|
||||
{device.lastScan ? new Date(device.lastScan).toLocaleString("de-DE") : "–"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<h2 className="mb-3 text-sm font-semibold text-black/60 dark:text-white/60">
|
||||
Kategorien
|
||||
</h2>
|
||||
<p className="text-sm text-black/40 dark:text-white/40">
|
||||
{categories?.length ?? 0} Kategorie(n) angelegt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user