feat(frontend): moderneres visuelles Redesign (Farben, Tiefe, Dashboard-Hero)
Kartenhintergrund und Seitenhintergrund waren fast farbgleich, wodurch alles flach wirkte trotz vorhandenem Schatten. Staerkerer Kontrast (--background/--card/--muted/--border), neue Gradient- und Schatten-Tokens (--gradient-honey/-sage/-sky, --shadow-soft/-elevated). Card-Komponente: rounded-2xl, echter sichtbarer Schatten, optionaler Hover-Lift (interactive-Prop). Sidebar: solide Gradient-Badge, kraeftiger Active-Pill-Zustand statt blassem Tint. Header: sticky mit Blur-Schatten statt hartem Rand. Dashboard: Gradient-Hero mit Begruessung und Honeycomb-Muster, farbige Gradient-Icon-Badges pro KPI-Kachel, Hinweise-Zeilen mit Icon statt rohem Enum-Badge (dabei toten CONSPICUOUS_COLONY-Style-Key gefunden und auf COLONY_ATTENTION korrigiert). Honeycomb-Hintergrund aus der Login-Seite in eine gemeinsame Komponente extrahiert und dort mit dem gleichen Gradient-Badge-Header konsistent gemacht.
This commit is contained in:
1 parent
6ab028d1be
commit
5cacc3c60d
7 files changed
+202
-103
No files matched your search
@@ -10,11 +10,14 @@ import {
|
||||
type LucideIcon,
|
||||
MapPin,
|
||||
Layers,
|
||||
PackageSearch,
|
||||
ShieldAlert,
|
||||
} from "lucide-react";
|
||||
import { useAuth } from "@/lib/auth/auth-context";
|
||||
import { fetchDashboardAlerts, fetchDashboardSummary } from "@/lib/dashboard/api";
|
||||
import { DashboardAlert } from "@/lib/dashboard/types";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DashboardAlert, DashboardAlertType } from "@/lib/dashboard/types";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { HoneycombPattern } from "@/components/honeycomb-pattern";
|
||||
|
||||
function alertHref(alert: DashboardAlert): string | null {
|
||||
if (alert.relatedType === "Colony") return `/colonies/${alert.relatedId}`;
|
||||
@@ -23,26 +26,43 @@ function alertHref(alert: DashboardAlert): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ALERT_STYLES: Record<string, string> = {
|
||||
OVERDUE_TASK: "border-destructive/30 bg-destructive/5",
|
||||
CONSPICUOUS_COLONY: "border-warning/40 bg-warning/10",
|
||||
LOW_STOCK: "border-warning/40 bg-warning/10",
|
||||
};
|
||||
function greeting(): string {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 11) return "Guten Morgen";
|
||||
if (hour < 18) return "Guten Tag";
|
||||
return "Guten Abend";
|
||||
}
|
||||
|
||||
interface StatTile {
|
||||
label: string;
|
||||
value: number;
|
||||
icon: LucideIcon;
|
||||
tone: "primary" | "secondary" | "warning";
|
||||
gradient: string;
|
||||
}
|
||||
|
||||
const TONE_CLASSES: Record<StatTile["tone"], string> = {
|
||||
primary: "bg-primary/10 text-primary",
|
||||
secondary: "bg-secondary/10 text-secondary",
|
||||
warning: "bg-warning/15 text-warning-foreground",
|
||||
const ALERT_META: Record<
|
||||
DashboardAlertType,
|
||||
{ label: string; icon: LucideIcon; className: string }
|
||||
> = {
|
||||
OVERDUE_TASK: {
|
||||
label: "Aufgabe überfällig",
|
||||
icon: ListTodo,
|
||||
className: "border-destructive/30 bg-destructive/5",
|
||||
},
|
||||
COLONY_ATTENTION: {
|
||||
label: "Volk auffällig",
|
||||
icon: ShieldAlert,
|
||||
className: "border-warning/40 bg-warning/10",
|
||||
},
|
||||
LOW_STOCK: {
|
||||
label: "Bestand niedrig",
|
||||
icon: PackageSearch,
|
||||
className: "border-warning/40 bg-warning/10",
|
||||
},
|
||||
};
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user } = useAuth();
|
||||
const { data: summary, isLoading: summaryLoading } = useQuery({
|
||||
queryKey: ["dashboard", "summary"],
|
||||
queryFn: fetchDashboardSummary,
|
||||
@@ -54,25 +74,62 @@ export default function DashboardPage() {
|
||||
|
||||
const tiles: StatTile[] = summary
|
||||
? [
|
||||
{ label: "Aktive Bienenstände", value: summary.apiariesActive, icon: MapPin, tone: "primary" },
|
||||
{ label: "Aktive Völker", value: summary.coloniesActive, icon: Layers, tone: "primary" },
|
||||
{ label: "Aktive Königinnen", value: summary.queensActive, icon: Crown, tone: "secondary" },
|
||||
{ label: "Offene Aufgaben", value: summary.openTasks, icon: ListTodo, tone: "secondary" },
|
||||
{ label: "Honigchargen", value: summary.honeyBatchesTotal, icon: Droplet, tone: "primary" },
|
||||
{
|
||||
label: "Aktive Bienenstände",
|
||||
value: summary.apiariesActive,
|
||||
icon: MapPin,
|
||||
gradient: "var(--gradient-honey)",
|
||||
},
|
||||
{
|
||||
label: "Aktive Völker",
|
||||
value: summary.coloniesActive,
|
||||
icon: Layers,
|
||||
gradient: "var(--gradient-honey)",
|
||||
},
|
||||
{
|
||||
label: "Aktive Königinnen",
|
||||
value: summary.queensActive,
|
||||
icon: Crown,
|
||||
gradient: "var(--gradient-sage)",
|
||||
},
|
||||
{
|
||||
label: "Offene Aufgaben",
|
||||
value: summary.openTasks,
|
||||
icon: ListTodo,
|
||||
gradient: "var(--gradient-sky)",
|
||||
},
|
||||
{
|
||||
label: "Honigchargen",
|
||||
value: summary.honeyBatchesTotal,
|
||||
icon: Droplet,
|
||||
gradient: "var(--gradient-honey)",
|
||||
},
|
||||
{
|
||||
label: "Artikel unter Mindestbestand",
|
||||
value: summary.stockItemsLowCount,
|
||||
icon: AlertTriangle,
|
||||
tone: summary.stockItemsLowCount > 0 ? "warning" : "secondary",
|
||||
gradient:
|
||||
summary.stockItemsLowCount > 0
|
||||
? "linear-gradient(135deg, oklch(0.72 0.15 70) 0%, oklch(0.56 0.15 50) 100%)"
|
||||
: "var(--gradient-sage)",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-6 p-4 sm:p-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Dashboard</h1>
|
||||
<p className="text-sm text-muted-foreground">Überblick über deine Imkerei</p>
|
||||
<div
|
||||
className="relative overflow-hidden rounded-2xl px-5 py-6 text-white shadow-[var(--shadow-elevated)] sm:px-8 sm:py-8"
|
||||
style={{ background: "var(--gradient-honey)" }}
|
||||
>
|
||||
<HoneycombPattern className="pointer-events-none absolute inset-0 h-full w-full text-white/[0.08]" />
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<h1 className="font-heading text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||
{greeting()}
|
||||
{user ? `, ${user.email.split("@")[0]}` : ""}
|
||||
</h1>
|
||||
<p className="text-sm text-white/85 sm:text-base">Überblick über deine Imkerei</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{summaryLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
@@ -80,12 +137,13 @@ export default function DashboardPage() {
|
||||
{tiles.map((tile) => {
|
||||
const Icon = tile.icon;
|
||||
return (
|
||||
<Card key={tile.label}>
|
||||
<Card key={tile.label} interactive>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<div
|
||||
className={`flex size-9 items-center justify-center rounded-lg ${TONE_CLASSES[tile.tone]}`}
|
||||
className="flex size-10 items-center justify-center rounded-xl text-white shadow-[var(--shadow-soft)]"
|
||||
style={{ background: tile.gradient }}
|
||||
>
|
||||
<Icon className="size-4.5" />
|
||||
<Icon className="size-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-semibold tabular-nums">{tile.value}</p>
|
||||
@@ -98,7 +156,7 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="mb-3 text-lg font-semibold tracking-tight">Hinweise</h2>
|
||||
<h2 className="mb-3 font-heading text-lg font-semibold tracking-tight">Hinweise</h2>
|
||||
<div className="flex flex-col gap-2">
|
||||
{alertsLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
{!alertsLoading && (!alerts || alerts.length === 0) ? (
|
||||
@@ -110,20 +168,23 @@ export default function DashboardPage() {
|
||||
) : null}
|
||||
{alerts?.map((alert) => {
|
||||
const href = alertHref(alert);
|
||||
const meta = ALERT_META[alert.type];
|
||||
const AlertIcon = meta?.icon ?? AlertTriangle;
|
||||
return (
|
||||
<div
|
||||
key={`${alert.type}-${alert.relatedId}`}
|
||||
className={`flex items-center justify-between rounded-xl border p-3 text-sm ${ALERT_STYLES[alert.type] ?? "border-border bg-card"}`}
|
||||
className={`flex items-center gap-3 rounded-xl border p-3 text-sm shadow-[var(--shadow-soft)] ${meta?.className ?? "border-border bg-card"}`}
|
||||
>
|
||||
<span>{alert.message}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">{alert.type}</Badge>
|
||||
{href ? (
|
||||
<Link className="font-medium text-primary underline-offset-4 hover:underline" href={href}>
|
||||
Ansehen
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
<AlertIcon className="size-4.5 shrink-0 text-muted-foreground" />
|
||||
<span className="flex-1">{alert.message}</span>
|
||||
{href ? (
|
||||
<Link
|
||||
className="shrink-0 font-medium text-primary underline-offset-4 hover:underline"
|
||||
href={href}
|
||||
>
|
||||
Ansehen
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in new issue
Block a user