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
+142
-63
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>
|
||||
<Button nativeButton={false} render={<Link href="/apiaries/new" />}>
|
||||
+ Neuer Bienenstand
|
||||
</Button>
|
||||
</div>
|
||||
<PageHeader
|
||||
icon={MapPin}
|
||||
title="Bienenstände"
|
||||
description="Standorte deiner Völker"
|
||||
actions={
|
||||
<Button nativeButton={false} render={<Link href="/apiaries/new" />}>
|
||||
+ Neuer Bienenstand
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<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,21 +50,25 @@ 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">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={exportMutation.isPending}
|
||||
onClick={() => exportMutation.mutate()}
|
||||
>
|
||||
CSV exportieren
|
||||
</Button>
|
||||
<Button nativeButton={false} render={<Link href="/colonies/new" />}>
|
||||
+ Neues Volk
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader
|
||||
icon={Layers}
|
||||
title="Völker"
|
||||
description="Alle Bienenvölker im Überblick"
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={exportMutation.isPending}
|
||||
onClick={() => exportMutation.mutate()}
|
||||
>
|
||||
CSV exportieren
|
||||
</Button>
|
||||
<Button nativeButton={false} render={<Link href="/colonies/new" />}>
|
||||
+ Neues Volk
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<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,19 +51,23 @@ 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">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={exportMutation.isPending}
|
||||
onClick={() => exportMutation.mutate()}
|
||||
>
|
||||
CSV exportieren
|
||||
</Button>
|
||||
<CreateHoneyBatchDialog />
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader
|
||||
icon={Droplet}
|
||||
title="Honigchargen"
|
||||
description="Vom Schleudern bis zur Abfüllung"
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={exportMutation.isPending}
|
||||
onClick={() => exportMutation.mutate()}
|
||||
>
|
||||
CSV exportieren
|
||||
</Button>
|
||||
<CreateHoneyBatchDialog />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Select
|
||||
value={status || "ALL"}
|
||||
|
||||
@@ -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>
|
||||
<Button nativeButton={false} render={<Link href="/queens/new" />}>
|
||||
+ Neue Königin
|
||||
</Button>
|
||||
</div>
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
|
||||
<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">
|
||||
|
||||
@@ -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>
|
||||
<Button nativeButton={false} render={<Link href="/tasks/new" />}>
|
||||
+ Neue Aufgabe
|
||||
</Button>
|
||||
</div>
|
||||
<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 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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user