style(frontend): grundlegendes Redesign (Design-System, Farben, Typografie)
Neues Design-System: warme Honig-/Wachs-Farbpalette (Amber-Primaer, Salbeigruen-Sekundaer) statt Standard-Graustufen, Space Grotesk fuer Ueberschriften, Inter fuer Fliesstext, IBM Plex Mono fuer Codes/IDs. Card-Komponente mit weicher Schattierung statt Ring, neuer Badge-Warning-Variante und statusTone()-Helper fuer semantische Status-Farben (Voelker/Honigchargen). Navigation umgebaut: aktiver Link-Zustand, Hexagon-Markenzeichen, "Mehr"-Dropdown fuer Admin-Links. Dashboard-Kacheln mit Icons/Farbakzenten, Login-Seite mit dezentem Waben-Hintergrundmuster als wiederkehrendes Signature-Element.
This commit is contained in:
1 parent
26b2eb0829
commit
e7348b0b68
38 files changed
+344
-195
No files matched your search
@@ -19,7 +19,7 @@ export default function ApiaryDetailPage() {
|
||||
}
|
||||
if (isError || !apiary) {
|
||||
return (
|
||||
<p role="alert" className="p-4 text-sm text-red-600 sm:p-6">
|
||||
<p role="alert" className="p-4 text-sm text-destructive sm:p-6">
|
||||
Bienenstand konnte nicht geladen werden.
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function NewApiaryPage() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -36,7 +36,7 @@ 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-xl font-semibold">Bienenstände</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Bienenstände</h1>
|
||||
<Button nativeButton={false} render={<Link href="/apiaries/new" />}>
|
||||
+ Neuer Bienenstand
|
||||
</Button>
|
||||
@@ -66,7 +66,7 @@ export default function ApiariesPage() {
|
||||
|
||||
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
{isError ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
Bienenstände konnten nicht geladen werden.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function ColonyDetailPage() {
|
||||
}
|
||||
if (isError || !colony) {
|
||||
return (
|
||||
<p role="alert" className="p-4 text-sm text-red-600 sm:p-6">
|
||||
<p role="alert" className="p-4 text-sm text-destructive sm:p-6">
|
||||
Volk konnte nicht geladen werden.
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function NewColonyPage() {
|
||||
</Select>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { fetchColonies } from "@/lib/colonies/api";
|
||||
import { ColonyStatus } from "@/lib/colonies/types";
|
||||
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 {
|
||||
@@ -48,7 +49,7 @@ 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-xl font-semibold">Völker</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Völker</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -82,7 +83,7 @@ export default function ColoniesPage() {
|
||||
|
||||
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
{isError ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
Völker konnten nicht geladen werden.
|
||||
</p>
|
||||
) : null}
|
||||
@@ -100,15 +101,16 @@ export default function ColoniesPage() {
|
||||
{colonies.map((colony) => (
|
||||
<TableRow key={colony.id}>
|
||||
<TableCell>
|
||||
<Link className="underline" href={`/colonies/${colony.id}`}>
|
||||
<Link
|
||||
className="font-mono font-medium text-primary underline-offset-4 hover:underline"
|
||||
href={`/colonies/${colony.id}`}
|
||||
>
|
||||
{colony.code}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>{colony.colonyType}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={colony.status === "ACTIVE" ? "default" : "secondary"}>
|
||||
{colony.status}
|
||||
</Badge>
|
||||
<Badge variant={statusTone(colony.status)}>{colony.status}</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Crown,
|
||||
Droplet,
|
||||
ListTodo,
|
||||
type LucideIcon,
|
||||
MapPin,
|
||||
Layers,
|
||||
} from "lucide-react";
|
||||
import { fetchDashboardAlerts, fetchDashboardSummary } from "@/lib/dashboard/api";
|
||||
import { DashboardAlert } from "@/lib/dashboard/types";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
function alertHref(alert: DashboardAlert): string | null {
|
||||
if (alert.relatedType === "Colony") return `/colonies/${alert.relatedId}`;
|
||||
@@ -14,6 +23,25 @@ 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",
|
||||
};
|
||||
|
||||
interface StatTile {
|
||||
label: string;
|
||||
value: number;
|
||||
icon: LucideIcon;
|
||||
tone: "primary" | "secondary" | "warning";
|
||||
}
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { data: summary, isLoading: summaryLoading } = useQuery({
|
||||
queryKey: ["dashboard", "summary"],
|
||||
@@ -24,58 +52,74 @@ export default function DashboardPage() {
|
||||
queryFn: fetchDashboardAlerts,
|
||||
});
|
||||
|
||||
const tiles = summary
|
||||
const tiles: StatTile[] = summary
|
||||
? [
|
||||
{ label: "Aktive Bienenstände", value: summary.apiariesActive },
|
||||
{ label: "Aktive Völker", value: summary.coloniesActive },
|
||||
{ label: "Aktive Königinnen", value: summary.queensActive },
|
||||
{ label: "Offene Aufgaben", value: summary.openTasks },
|
||||
{ label: "Honigchargen", value: summary.honeyBatchesTotal },
|
||||
{ label: "Artikel unter Mindestbestand", value: summary.stockItemsLowCount },
|
||||
{ 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: "Artikel unter Mindestbestand",
|
||||
value: summary.stockItemsLowCount,
|
||||
icon: AlertTriangle,
|
||||
tone: summary.stockItemsLowCount > 0 ? "warning" : "secondary",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-6 p-4 sm:p-6">
|
||||
<h1 className="text-xl font-semibold">Dashboard</h1>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Dashboard</h1>
|
||||
<p className="text-sm text-muted-foreground">Überblick über deine Imkerei</p>
|
||||
</div>
|
||||
|
||||
{summaryLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
{tiles.map((tile) => (
|
||||
{tiles.map((tile) => {
|
||||
const Icon = tile.icon;
|
||||
return (
|
||||
<Card key={tile.label}>
|
||||
<CardHeader className="pb-1">
|
||||
<CardTitle className="text-sm font-normal text-muted-foreground">
|
||||
{tile.label}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">{tile.value}</p>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<div
|
||||
className={`flex size-9 items-center justify-center rounded-lg ${TONE_CLASSES[tile.tone]}`}
|
||||
>
|
||||
<Icon className="size-4.5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-semibold tabular-nums">{tile.value}</p>
|
||||
<p className="text-xs text-muted-foreground">{tile.label}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Hinweise</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2 text-sm">
|
||||
{alertsLoading ? <p className="text-muted-foreground">Lädt…</p> : null}
|
||||
<div>
|
||||
<h2 className="mb-3 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) ? (
|
||||
<p className="text-muted-foreground">Keine offenen Hinweise.</p>
|
||||
<Card>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Keine offenen Hinweise.
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
{alerts?.map((alert) => {
|
||||
const href = alertHref(alert);
|
||||
return (
|
||||
<div
|
||||
key={`${alert.type}-${alert.relatedId}`}
|
||||
className="flex items-center justify-between rounded-md border p-3"
|
||||
className={`flex items-center justify-between rounded-xl border p-3 text-sm ${ALERT_STYLES[alert.type] ?? "border-border bg-card"}`}
|
||||
>
|
||||
<span>{alert.message}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">{alert.type}</Badge>
|
||||
{href ? (
|
||||
<Link className="underline" href={href}>
|
||||
<Link className="font-medium text-primary underline-offset-4 hover:underline" href={href}>
|
||||
Ansehen
|
||||
</Link>
|
||||
) : null}
|
||||
@@ -83,8 +127,8 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -151,7 +151,7 @@ export default function NewExtractionPage() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -183,7 +183,7 @@ export default function NewFeedingPage() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function HarvestDetailPage() {
|
||||
}
|
||||
if (isError || !harvest) {
|
||||
return (
|
||||
<p role="alert" className="p-4 text-sm text-red-600 sm:p-6">
|
||||
<p role="alert" className="p-4 text-sm text-destructive sm:p-6">
|
||||
Ernte konnte nicht geladen werden.
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -176,7 +176,7 @@ export default function NewHarvestPage() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -94,7 +94,7 @@ export function AddMeasurementDialog({ honeyBatchId }: { honeyBatchId: string })
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function HoneyBatchDetailPage() {
|
||||
}
|
||||
if (isError || !honeyBatch) {
|
||||
return (
|
||||
<p role="alert" className="p-4 text-sm text-red-600 sm:p-6">
|
||||
<p role="alert" className="p-4 text-sm text-destructive sm:p-6">
|
||||
Honigcharge konnte nicht geladen werden.
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -122,7 +122,7 @@ export function CreateHoneyBatchDialog() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { fetchHoneyBatches } from "@/lib/honey-batches/api";
|
||||
import { HoneyBatchStatus } from "@/lib/honey-batches/types";
|
||||
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 {
|
||||
@@ -49,7 +50,7 @@ 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-xl font-semibold">Honigchargen</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Honigchargen</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -83,7 +84,7 @@ export default function HoneyBatchesPage() {
|
||||
|
||||
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
{isError ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
Honigchargen konnten nicht geladen werden.
|
||||
</p>
|
||||
) : null}
|
||||
@@ -102,7 +103,10 @@ export default function HoneyBatchesPage() {
|
||||
{honeyBatches.map((honeyBatch) => (
|
||||
<TableRow key={honeyBatch.id}>
|
||||
<TableCell>
|
||||
<Link className="underline" href={`/honey-batches/${honeyBatch.id}`}>
|
||||
<Link
|
||||
className="font-mono font-medium text-primary underline-offset-4 hover:underline"
|
||||
href={`/honey-batches/${honeyBatch.id}`}
|
||||
>
|
||||
{honeyBatch.batchNumber}
|
||||
</Link>
|
||||
</TableCell>
|
||||
@@ -111,7 +115,7 @@ export default function HoneyBatchesPage() {
|
||||
{honeyBatch.amount !== null ? `${honeyBatch.amount} kg` : "—"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge>{honeyBatch.status}</Badge>
|
||||
<Badge variant={statusTone(honeyBatch.status)}>{honeyBatch.status}</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -194,7 +194,7 @@ export default function NewInspectionPage() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -2,14 +2,53 @@
|
||||
|
||||
import { ReactNode, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { Hexagon, MoreHorizontal } from "lucide-react";
|
||||
import { useAuth } from "@/lib/auth/auth-context";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { BottomNav } from "./bottom-nav";
|
||||
import { QuickAddMenu } from "./quick-add-menu";
|
||||
|
||||
const NAV_LINK_CLASS =
|
||||
"shrink-0 rounded-md px-2.5 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary";
|
||||
|
||||
const PRIMARY_LINKS = [
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
{ label: "Bienenstände", href: "/apiaries" },
|
||||
{ label: "Völker", href: "/colonies" },
|
||||
{ label: "Königinnen", href: "/queens" },
|
||||
{ label: "Aufgaben", href: "/tasks" },
|
||||
{ label: "Honigchargen", href: "/honey-batches" },
|
||||
{ label: "Lager", href: "/storage" },
|
||||
{ label: "Suche", href: "/search" },
|
||||
];
|
||||
|
||||
const ADMIN_LINKS = [
|
||||
{ label: "Benutzer", href: "/settings/users" },
|
||||
{ label: "Audit-Log", href: "/settings/audit-log" },
|
||||
];
|
||||
|
||||
function NavLink({ href, label, pathname }: { href: string; label: string; pathname: string }) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
aria-current={pathname.startsWith(href) ? "page" : undefined}
|
||||
className={NAV_LINK_CLASS}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { user, isLoading, logout } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -24,52 +63,32 @@ export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<header className="flex items-center justify-between gap-3 border-b px-4 py-3">
|
||||
<nav className="flex min-w-0 items-center gap-4 max-sm:gap-2">
|
||||
<span className="shrink-0 font-semibold">ApiCore</span>
|
||||
<div className="hidden min-w-0 items-center gap-4 overflow-x-auto whitespace-nowrap sm:flex">
|
||||
<Link className="text-sm text-muted-foreground hover:text-foreground" href="/dashboard">
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link className="text-sm text-muted-foreground hover:text-foreground" href="/apiaries">
|
||||
Bienenstände
|
||||
</Link>
|
||||
<Link className="text-sm text-muted-foreground hover:text-foreground" href="/colonies">
|
||||
Völker
|
||||
</Link>
|
||||
<Link className="text-sm text-muted-foreground hover:text-foreground" href="/queens">
|
||||
Königinnen
|
||||
</Link>
|
||||
<Link className="text-sm text-muted-foreground hover:text-foreground" href="/tasks">
|
||||
Aufgaben
|
||||
</Link>
|
||||
<Link
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
href="/honey-batches"
|
||||
>
|
||||
Honigchargen
|
||||
</Link>
|
||||
<Link className="text-sm text-muted-foreground hover:text-foreground" href="/storage">
|
||||
Lager
|
||||
</Link>
|
||||
<Link className="text-sm text-muted-foreground hover:text-foreground" href="/search">
|
||||
Suche
|
||||
<header className="flex items-center justify-between gap-3 border-b bg-card px-4 py-2.5">
|
||||
<nav className="flex min-w-0 items-center gap-3 max-sm:gap-2">
|
||||
<Link href="/dashboard" className="flex shrink-0 items-center gap-1.5 font-heading font-semibold">
|
||||
<Hexagon className="size-5 fill-primary/15 text-primary" strokeWidth={2.25} />
|
||||
ApiCore
|
||||
</Link>
|
||||
<div className="hidden min-w-0 items-center gap-1 overflow-x-auto whitespace-nowrap sm:flex">
|
||||
{PRIMARY_LINKS.map((item) => (
|
||||
<NavLink key={item.href} {...item} pathname={pathname} />
|
||||
))}
|
||||
{user.role === "ADMIN" ? (
|
||||
<Link
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
href="/settings/users"
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="sm" className="gap-1 text-muted-foreground" />}
|
||||
>
|
||||
Benutzer
|
||||
</Link>
|
||||
) : null}
|
||||
{user.role === "ADMIN" ? (
|
||||
<Link
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
href="/settings/audit-log"
|
||||
>
|
||||
Audit-Log
|
||||
</Link>
|
||||
<MoreHorizontal className="size-4" />
|
||||
Mehr
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{ADMIN_LINKS.map((item) => (
|
||||
<DropdownMenuItem key={item.href} render={<Link href={item.href} />}>
|
||||
{item.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function QueenDetailPage() {
|
||||
}
|
||||
if (isError || !queen) {
|
||||
return (
|
||||
<p role="alert" className="p-4 text-sm text-red-600 sm:p-6">
|
||||
<p role="alert" className="p-4 text-sm text-destructive sm:p-6">
|
||||
Königin konnte nicht geladen werden.
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function NewQueenPage() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -45,7 +45,7 @@ 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-xl font-semibold">Königinnen</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Königinnen</h1>
|
||||
<Button nativeButton={false} render={<Link href="/queens/new" />}>
|
||||
+ Neue Königin
|
||||
</Button>
|
||||
@@ -70,7 +70,7 @@ export default function QueensPage() {
|
||||
|
||||
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
{isError ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
Königinnen konnten nicht geladen werden.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function SearchPage() {
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||
<h1 className="text-xl font-semibold">Suche</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Suche</h1>
|
||||
<Input
|
||||
className="h-11 max-w-lg"
|
||||
placeholder="Bienenstand, Volk, Königin, Aufgabe, Charge…"
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function AuditLogPage() {
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||
<h1 className="text-xl font-semibold">Audit-Log</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Audit-Log</h1>
|
||||
|
||||
<form
|
||||
className="flex flex-wrap items-end gap-3"
|
||||
@@ -70,7 +70,7 @@ export default function AuditLogPage() {
|
||||
|
||||
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
{isError ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
Audit-Log konnte nicht geladen werden.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -110,7 +110,7 @@ export function CreateUserDialog() {
|
||||
</Select>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -31,13 +31,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-xl font-semibold">Benutzerverwaltung</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Benutzerverwaltung</h1>
|
||||
<CreateUserDialog />
|
||||
</div>
|
||||
|
||||
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
{isError ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
Benutzer konnten nicht geladen werden.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -115,7 +115,7 @@ export function CreateStockItemDialog() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -95,7 +95,7 @@ export function CreateStorageContainerDialog() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function StoragePage() {
|
||||
<main className="flex flex-col gap-8 p-4 sm:p-6">
|
||||
<section className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold">Lagerbehälter</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Lagerbehälter</h1>
|
||||
<CreateStorageContainerDialog />
|
||||
</div>
|
||||
{containersLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
@@ -63,7 +63,7 @@ export default function StoragePage() {
|
||||
|
||||
<section className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">Lagerartikel</h2>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Lagerartikel</h2>
|
||||
<CreateStockItemDialog />
|
||||
</div>
|
||||
{itemsLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
|
||||
@@ -94,7 +94,7 @@ export function RecordMovementDialog({ stockItemId }: { stockItemId: string }) {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function NewTaskPage() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -57,7 +57,7 @@ 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-xl font-semibold">Aufgaben</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Aufgaben</h1>
|
||||
<Button nativeButton={false} render={<Link href="/tasks/new" />}>
|
||||
+ Neue Aufgabe
|
||||
</Button>
|
||||
@@ -110,7 +110,7 @@ export default function TasksPage() {
|
||||
|
||||
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
{isError ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
Aufgaben konnten nicht geladen werden.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -153,7 +153,7 @@ export default function NewTreatmentPage() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -173,7 +173,7 @@ export default function NewVarroaMeasurementPage() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Hexagon } from "lucide-react";
|
||||
import { useAuth } from "@/lib/auth/auth-context";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -14,6 +15,33 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
function HoneycombBackdrop() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 h-full w-full text-primary/[0.07]"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="honeycomb"
|
||||
width="56"
|
||||
height="97"
|
||||
patternUnits="userSpaceOnUse"
|
||||
patternTransform="scale(1)"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
d="M28 0 56 16.2 56 48.5 28 64.7 0 48.5 0 16.2Z M28 64.7 56 80.8 56 97 28 97 0 97 0 80.8Z"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#honeycomb)" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const { login } = useAuth();
|
||||
@@ -37,11 +65,15 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center bg-zinc-50 p-4 dark:bg-black">
|
||||
<Card className="w-full max-w-sm">
|
||||
<main className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background p-4">
|
||||
<HoneycombBackdrop />
|
||||
<Card className="relative w-full max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>ApiCore Login</CardTitle>
|
||||
<CardDescription>Digitale Imkereiverwaltung</CardDescription>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<Hexagon className="size-6 fill-primary/15 text-primary" strokeWidth={2.25} />
|
||||
<CardTitle className="text-lg">ApiCore</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Digitale Imkereiverwaltung für Wabenhain</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
@@ -68,7 +100,7 @@ export default function LoginPage() {
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-heading: var(--font-sans);
|
||||
--font-sans: var(--font-body);
|
||||
--font-mono: var(--font-data);
|
||||
--font-heading: var(--font-heading);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
@@ -27,6 +27,10 @@
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-warning: var(--warning);
|
||||
--color-warning-foreground: var(--warning-foreground);
|
||||
--color-success: var(--success);
|
||||
--color-success-foreground: var(--success-foreground);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
@@ -49,72 +53,80 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--background: oklch(0.98 0.012 85);
|
||||
--foreground: oklch(0.24 0.025 55);
|
||||
--card: oklch(0.995 0.006 85);
|
||||
--card-foreground: oklch(0.24 0.025 55);
|
||||
--popover: oklch(0.995 0.006 85);
|
||||
--popover-foreground: oklch(0.24 0.025 55);
|
||||
--primary: oklch(0.6 0.135 50);
|
||||
--primary-foreground: oklch(0.98 0.02 85);
|
||||
--secondary: oklch(0.46 0.06 145);
|
||||
--secondary-foreground: oklch(0.98 0.015 100);
|
||||
--muted: oklch(0.93 0.016 85);
|
||||
--muted-foreground: oklch(0.5 0.03 65);
|
||||
--accent: oklch(0.94 0.028 75);
|
||||
--accent-foreground: oklch(0.28 0.04 50);
|
||||
--destructive: oklch(0.5 0.16 30);
|
||||
--border: oklch(0.88 0.02 78);
|
||||
--input: oklch(0.88 0.02 78);
|
||||
--ring: oklch(0.6 0.135 50);
|
||||
--warning: oklch(0.68 0.14 75);
|
||||
--warning-foreground: oklch(0.24 0.04 60);
|
||||
--success: oklch(0.46 0.06 145);
|
||||
--success-foreground: oklch(0.98 0.015 100);
|
||||
--chart-1: oklch(0.6 0.135 50);
|
||||
--chart-2: oklch(0.46 0.06 145);
|
||||
--chart-3: oklch(0.68 0.14 75);
|
||||
--chart-4: oklch(0.5 0.16 30);
|
||||
--chart-5: oklch(0.55 0.05 250);
|
||||
--radius: 0.75rem;
|
||||
--sidebar: oklch(0.98 0.012 85);
|
||||
--sidebar-foreground: oklch(0.24 0.025 55);
|
||||
--sidebar-primary: oklch(0.6 0.135 50);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.02 85);
|
||||
--sidebar-accent: oklch(0.94 0.028 75);
|
||||
--sidebar-accent-foreground: oklch(0.28 0.04 50);
|
||||
--sidebar-border: oklch(0.88 0.02 78);
|
||||
--sidebar-ring: oklch(0.6 0.135 50);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
--background: oklch(0.2 0.018 55);
|
||||
--foreground: oklch(0.94 0.02 80);
|
||||
--card: oklch(0.25 0.02 55);
|
||||
--card-foreground: oklch(0.94 0.02 80);
|
||||
--popover: oklch(0.25 0.02 55);
|
||||
--popover-foreground: oklch(0.94 0.02 80);
|
||||
--primary: oklch(0.72 0.13 55);
|
||||
--primary-foreground: oklch(0.2 0.02 55);
|
||||
--secondary: oklch(0.62 0.075 145);
|
||||
--secondary-foreground: oklch(0.15 0.02 145);
|
||||
--muted: oklch(0.29 0.018 55);
|
||||
--muted-foreground: oklch(0.7 0.025 75);
|
||||
--accent: oklch(0.32 0.03 60);
|
||||
--accent-foreground: oklch(0.94 0.02 80);
|
||||
--destructive: oklch(0.62 0.16 30);
|
||||
--border: oklch(1 0 0 / 12%);
|
||||
--input: oklch(1 0 0 / 16%);
|
||||
--ring: oklch(0.72 0.13 55);
|
||||
--warning: oklch(0.74 0.13 75);
|
||||
--warning-foreground: oklch(0.2 0.03 60);
|
||||
--success: oklch(0.62 0.075 145);
|
||||
--success-foreground: oklch(0.15 0.02 145);
|
||||
--chart-1: oklch(0.72 0.13 55);
|
||||
--chart-2: oklch(0.62 0.075 145);
|
||||
--chart-3: oklch(0.74 0.13 75);
|
||||
--chart-4: oklch(0.62 0.16 30);
|
||||
--chart-5: oklch(0.65 0.06 250);
|
||||
--sidebar: oklch(0.22 0.02 55);
|
||||
--sidebar-foreground: oklch(0.94 0.02 80);
|
||||
--sidebar-primary: oklch(0.72 0.13 55);
|
||||
--sidebar-primary-foreground: oklch(0.2 0.02 55);
|
||||
--sidebar-accent: oklch(0.32 0.03 60);
|
||||
--sidebar-accent-foreground: oklch(0.94 0.02 80);
|
||||
--sidebar-border: oklch(1 0 0 / 12%);
|
||||
--sidebar-ring: oklch(0.72 0.13 55);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -127,4 +139,11 @@
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
font-family: var(--font-heading);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,23 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Space_Grotesk, Inter, IBM_Plex_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Providers } from "./providers";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
const spaceGrotesk = Space_Grotesk({
|
||||
variable: "--font-heading",
|
||||
subsets: ["latin"],
|
||||
weight: ["500", "600", "700"],
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-body",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
const plexMono = IBM_Plex_Mono({
|
||||
variable: "--font-data",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -22,7 +29,7 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
return (
|
||||
<html
|
||||
lang="de"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
className={`${spaceGrotesk.variable} ${inter.variable} ${plexMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">
|
||||
<Providers>{children}</Providers>
|
||||
|
||||
@@ -14,6 +14,8 @@ const badgeVariants = cva(
|
||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
warning:
|
||||
"bg-warning/15 text-warning-foreground focus-visible:ring-warning/30 [a]:hover:bg-warning/25",
|
||||
outline:
|
||||
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost:
|
||||
|
||||
@@ -12,7 +12,7 @@ function Card({
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl border border-border/60 bg-card py-(--card-spacing) text-sm text-card-foreground shadow-[0_1px_2px_-1px_rgba(33,20,10,0.06),0_2px_8px_-2px_rgba(33,20,10,0.06)] transition-shadow [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export type StatusTone = "default" | "secondary" | "warning" | "destructive" | "outline";
|
||||
|
||||
const WARNING_STATUSES = new Set([
|
||||
"WEAK",
|
||||
"CONSPICUOUS",
|
||||
"QUEENLESS",
|
||||
"LOW_STOCK",
|
||||
]);
|
||||
|
||||
const DESTRUCTIVE_STATUSES = new Set(["DISSOLVED", "DECEASED", "SOLD"]);
|
||||
|
||||
const NEUTRAL_STATUSES = new Set(["MERGED", "CONSUMED"]);
|
||||
|
||||
export function statusTone(status: string): StatusTone {
|
||||
if (WARNING_STATUSES.has(status)) return "warning";
|
||||
if (DESTRUCTIVE_STATUSES.has(status)) return "destructive";
|
||||
if (NEUTRAL_STATUSES.has(status)) return "outline";
|
||||
if (status === "ACTIVE") return "default";
|
||||
return "secondary";
|
||||
}
|
||||
Reference in new issue
Block a user