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
+142 -63

No files matched your search

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