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:
1 parent
3af56c96fa
commit
60b59c120a
9 files changed
+115
-36
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}
|
||||||
|
title="Bienenstände"
|
||||||
|
description="Standorte deiner Völker"
|
||||||
|
actions={
|
||||||
<Button nativeButton={false} render={<Link href="/apiaries/new" />}>
|
<Button nativeButton={false} render={<Link href="/apiaries/new" />}>
|
||||||
+ Neuer Bienenstand
|
+ Neuer Bienenstand
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<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,9 +50,12 @@ 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"
|
||||||
|
description="Alle Bienenvölker im Überblick"
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={exportMutation.isPending}
|
disabled={exportMutation.isPending}
|
||||||
@@ -61,8 +66,9 @@ export default function ColoniesPage() {
|
|||||||
<Button nativeButton={false} render={<Link href="/colonies/new" />}>
|
<Button nativeButton={false} render={<Link href="/colonies/new" />}>
|
||||||
+ Neues Volk
|
+ Neues Volk
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<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,9 +51,12 @@ 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"
|
||||||
|
description="Vom Schleudern bis zur Abfüllung"
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={exportMutation.isPending}
|
disabled={exportMutation.isPending}
|
||||||
@@ -60,8 +65,9 @@ export default function HoneyBatchesPage() {
|
|||||||
CSV exportieren
|
CSV exportieren
|
||||||
</Button>
|
</Button>
|
||||||
<CreateHoneyBatchDialog />
|
<CreateHoneyBatchDialog />
|
||||||
</div>
|
</>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={status || "ALL"}
|
value={status || "ALL"}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
title="Königinnen"
|
||||||
|
description="Zuchtlinien und Bewertungen"
|
||||||
|
gradient="var(--gradient-sage)"
|
||||||
|
actions={
|
||||||
<Button nativeButton={false} render={<Link href="/queens/new" />}>
|
<Button nativeButton={false} render={<Link href="/queens/new" />}>
|
||||||
+ Neue Königin
|
+ Neue Königin
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<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">
|
||||||
|
|||||||
@@ -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}
|
||||||
|
title="Aufgaben"
|
||||||
|
description="Was als Nächstes ansteht"
|
||||||
|
gradient="var(--gradient-sky)"
|
||||||
|
actions={
|
||||||
<Button nativeButton={false} render={<Link href="/tasks/new" />}>
|
<Button nativeButton={false} render={<Link href="/tasks/new" />}>
|
||||||
+ Neue Aufgabe
|
+ Neue Aufgabe
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user