feat(frontend): einheitliche Seiten-Header mit Icon-Badge

Neue PageHeader-Komponente (Gradient-Icon-Badge + Titel + Beschreibung
+ Aktionen) ersetzt die nackten <h1>-Zeilen auf den meistgenutzten
Seiten (Voelker, Bienenstaende, Koeniginnen, Aufgaben, Honigchargen,
Statistik, Webhooks, Benutzerverwaltung). Gradient variiert pro
Bereich (Honig/Salbei/Himmel), passend zum jeweiligen Sidebar-Icon.
This commit is contained in:
Nick Szaunig committed 2026-08-23 20:31:03 +02:00
1 parent 3af56c96fa
commit 60b59c120a
9 files changed
+115 -36

No files matched your search

@@ -2,12 +2,14 @@
import { useState } from "react";
import Link from "next/link";
import { MapPin } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { fetchApiaries } from "@/lib/apiaries/api";
import { ApiaryStatus } from "@/lib/apiaries/types";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { PageHeader } from "@/components/page-header";
import {
Select,
SelectContent,
@@ -35,12 +37,16 @@ export default function ApiariesPage() {
return (
<main className="flex flex-col gap-4 p-4 sm:p-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold tracking-tight">Bienenstände</h1>
<PageHeader
icon={MapPin}
title="Bienenstände"
description="Standorte deiner Völker"
actions={
<Button nativeButton={false} render={<Link href="/apiaries/new" />}>
+ Neuer Bienenstand
</Button>
</div>
}
/>
<div className="flex flex-wrap items-center gap-3">
<Select
@@ -2,6 +2,7 @@
import { useState } from "react";
import Link from "next/link";
import { Layers } from "lucide-react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { fetchColonies } from "@/lib/colonies/api";
import { ColonyStatus } from "@/lib/colonies/types";
@@ -9,6 +10,7 @@ import { exportColoniesCsv } from "@/lib/export/api";
import { statusTone } from "@/lib/status-tone";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { PageHeader } from "@/components/page-header";
import {
Select,
SelectContent,
@@ -48,9 +50,12 @@ export default function ColoniesPage() {
return (
<main className="flex flex-col gap-4 p-4 sm:p-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold tracking-tight">Völker</h1>
<div className="flex gap-2">
<PageHeader
icon={Layers}
title="Völker"
description="Alle Bienenvölker im Überblick"
actions={
<>
<Button
variant="outline"
disabled={exportMutation.isPending}
@@ -61,8 +66,9 @@ export default function ColoniesPage() {
<Button nativeButton={false} render={<Link href="/colonies/new" />}>
+ Neues Volk
</Button>
</div>
</div>
</>
}
/>
<Select
value={status || "ALL"}
@@ -2,6 +2,7 @@
import { useState } from "react";
import Link from "next/link";
import { Droplet } from "lucide-react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { fetchHoneyBatches } from "@/lib/honey-batches/api";
import { HoneyBatchStatus } from "@/lib/honey-batches/types";
@@ -9,6 +10,7 @@ import { exportHoneyBatchesCsv } from "@/lib/export/api";
import { statusTone } from "@/lib/status-tone";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { PageHeader } from "@/components/page-header";
import {
Select,
SelectContent,
@@ -49,9 +51,12 @@ export default function HoneyBatchesPage() {
return (
<main className="flex flex-col gap-4 p-4 sm:p-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold tracking-tight">Honigchargen</h1>
<div className="flex gap-2">
<PageHeader
icon={Droplet}
title="Honigchargen"
description="Vom Schleudern bis zur Abfüllung"
actions={
<>
<Button
variant="outline"
disabled={exportMutation.isPending}
@@ -60,8 +65,9 @@ export default function HoneyBatchesPage() {
CSV exportieren
</Button>
<CreateHoneyBatchDialog />
</div>
</div>
</>
}
/>
<Select
value={status || "ALL"}
+10 -3
View File
@@ -2,11 +2,13 @@
import { useState } from "react";
import Link from "next/link";
import { Crown } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { fetchQueens } from "@/lib/queens/api";
import { QueenStatus } from "@/lib/queens/types";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { PageHeader } from "@/components/page-header";
import {
Select,
SelectContent,
@@ -44,12 +46,17 @@ export default function QueensPage() {
return (
<main className="flex flex-col gap-4 p-4 sm:p-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold tracking-tight">Königinnen</h1>
<PageHeader
icon={Crown}
title="Königinnen"
description="Zuchtlinien und Bewertungen"
gradient="var(--gradient-sage)"
actions={
<Button nativeButton={false} render={<Link href="/queens/new" />}>
+ Neue Königin
</Button>
</div>
}
/>
<Select
value={status || "ALL"}
@@ -1,11 +1,13 @@
"use client";
import { Users } from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { activateUser, deactivateUser, fetchUsers } from "@/lib/users/api";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ConfirmDialog } from "@/components/confirm-dialog";
import { PageHeader } from "@/components/page-header";
import {
Table,
TableBody,
@@ -45,10 +47,13 @@ export default function UsersPage() {
return (
<main className="flex flex-col gap-4 p-4 sm:p-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold tracking-tight">Benutzerverwaltung</h1>
<CreateUserDialog />
</div>
<PageHeader
icon={Users}
title="Benutzerverwaltung"
description="Zugänge und Rollen verwalten"
gradient="var(--gradient-sage)"
actions={<CreateUserDialog />}
/>
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
{isError ? (
@@ -1,10 +1,12 @@
"use client";
import { Webhook } from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { deleteWebhook, fetchWebhooks } from "@/lib/webhooks/api";
import { Badge } from "@/components/ui/badge";
import { ConfirmDialog } from "@/components/confirm-dialog";
import { PageHeader } from "@/components/page-header";
import {
Table,
TableBody,
@@ -33,15 +35,13 @@ export default function WebhooksPage() {
return (
<main className="flex flex-col gap-4 p-4 sm:p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Webhooks</h1>
<p className="text-sm text-muted-foreground">
Externe Systeme über Ereignisse in ApiCore benachrichtigen
</p>
</div>
<CreateWebhookDialog />
</div>
<PageHeader
icon={Webhook}
title="Webhooks"
description="Externe Systeme über Ereignisse in ApiCore benachrichtigen"
gradient="var(--gradient-sky)"
actions={<CreateWebhookDialog />}
/>
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
{isError ? (
@@ -1,5 +1,6 @@
"use client";
import { BarChart3 } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { fetchApiaries } from "@/lib/apiaries/api";
import { fetchColonies } from "@/lib/colonies/api";
@@ -12,6 +13,7 @@ import {
fetchVarroaStats,
} from "@/lib/statistics/api";
import { StatBarList } from "@/components/stat-bar-list";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -286,10 +288,12 @@ export default function StatisticsPage() {
return (
<main className="flex flex-col gap-6 p-4 sm:p-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Statistik</h1>
<p className="text-sm text-muted-foreground">Auswertungen über deine Imkerei</p>
</div>
<PageHeader
icon={BarChart3}
title="Statistik"
description="Auswertungen über deine Imkerei"
gradient="var(--gradient-sky)"
/>
<Tabs defaultValue="colonies">
<TabsList className="w-full sm:w-fit">
+10 -3
View File
@@ -2,11 +2,13 @@
import { useState } from "react";
import Link from "next/link";
import { ListTodo } from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { completeTask, fetchTasks } from "@/lib/tasks/api";
import { TaskPriority, TaskStatus } from "@/lib/tasks/types";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { PageHeader } from "@/components/page-header";
import {
Select,
SelectContent,
@@ -56,12 +58,17 @@ export default function TasksPage() {
return (
<main className="flex flex-col gap-4 p-4 sm:p-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold tracking-tight">Aufgaben</h1>
<PageHeader
icon={ListTodo}
title="Aufgaben"
description="Was als Nächstes ansteht"
gradient="var(--gradient-sky)"
actions={
<Button nativeButton={false} render={<Link href="/tasks/new" />}>
+ Neue Aufgabe
</Button>
</div>
}
/>
<div className="flex flex-wrap items-center gap-3">
<Select
@@ -0,0 +1,38 @@
import { ReactNode } from "react";
import type { LucideIcon } from "lucide-react";
interface PageHeaderProps {
icon: LucideIcon;
title: string;
description?: string;
gradient?: string;
actions?: ReactNode;
}
export function PageHeader({
icon: Icon,
title,
description,
gradient = "var(--gradient-honey)",
actions,
}: PageHeaderProps) {
return (
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3">
<span
className="flex size-10 shrink-0 items-center justify-center rounded-xl text-white shadow-[var(--shadow-soft)]"
style={{ background: gradient }}
>
<Icon className="size-5" />
</span>
<div>
<h1 className="font-heading text-2xl font-semibold tracking-tight">{title}</h1>
{description ? (
<p className="text-sm text-muted-foreground">{description}</p>
) : null}
</div>
</div>
{actions ? <div className="flex flex-wrap gap-2">{actions}</div> : null}
</div>
);
}