feat(frontend): Linksseitige Navigation, Seitenanimation, ADHS-freundliche Gruppierung
Ersetzt die horizontale Kopfzeilen-Navigation durch eine feste Sidebar links: gruppiert nach Betrieb / Arbeit / Ernte & Lager / Verwaltung mit Icon+Label je Eintrag, um kognitive Last zu reduzieren. Aktiver Zustand klar farblich hervorgehoben. Mobile Ansicht (Bottom-Nav + FAB) bleibt unveraendert bestehen. Sanfte, kurze Seitenuebergangsanimation (PageTransition, respektiert prefers-reduced-motion ueber motion-safe:) fuer Orientierung ohne Ablenkung. Ausserdem: wiederkehrenden Anzeigefehler in Status-Filtern behoben (Select zeigte den rohen Sentinel-Wert "ALL" statt "Alle Status" an) auf Voelker-, Honigchargen-, Aufgaben-, Koeniginnen- und Bienenstand-Listen. Live in Desktop-, Tablet- und Mobilbreite verifiziert, kein horizontales Overflow.
This commit is contained in:
1 parent
fb2b09a929
commit
ecb3afe1f5
9 files changed
+181
-86
No files matched your search
@@ -48,7 +48,7 @@ export default function ApiariesPage() {
|
||||
onValueChange={(value) => setStatus(value === "ALL" ? "" : ((value ?? "") as ApiaryStatus))}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Status" />
|
||||
<SelectValue placeholder="Status">{status || "Alle Status"}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">Alle Status</SelectItem>
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function ColoniesPage() {
|
||||
onValueChange={(value) => setStatus(value === "ALL" ? "" : ((value ?? "") as ColonyStatus))}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Status" />
|
||||
<SelectValue placeholder="Status">{status || "Alle Status"}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">Alle Status</SelectItem>
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function HoneyBatchesPage() {
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Status" />
|
||||
<SelectValue placeholder="Status">{status || "Alle Status"}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">Alle Status</SelectItem>
|
||||
|
||||
@@ -2,61 +2,18 @@
|
||||
|
||||
import { ReactNode, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { Hexagon, MoreHorizontal } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Hexagon } from "lucide-react";
|
||||
import { useAuth } from "@/lib/auth/auth-context";
|
||||
import { NotificationBell } from "@/components/notification-bell";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Sidebar } from "./sidebar";
|
||||
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: "Kalender", href: "/calendar" },
|
||||
{ label: "Honigchargen", href: "/honey-batches" },
|
||||
{ label: "Lager", href: "/storage" },
|
||||
{ label: "Suche", href: "/search" },
|
||||
];
|
||||
|
||||
const MORE_LINKS = [
|
||||
{ label: "Produkte", href: "/products" },
|
||||
{ label: "Abfüllungen", href: "/bottling/new" },
|
||||
{ label: "Inventar", href: "/equipment" },
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
import { PageTransition } from "./page-transition";
|
||||
|
||||
export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { user, isLoading, logout } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -70,41 +27,17 @@ 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 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">
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar user={user} />
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<header className="flex items-center justify-between gap-3 border-b bg-card px-4 py-2.5 sm:justify-end sm:px-6">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="flex items-center gap-1.5 font-heading font-semibold sm:hidden"
|
||||
>
|
||||
<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} />
|
||||
))}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="sm" className="gap-1 text-muted-foreground" />}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
Mehr
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{MORE_LINKS.map((item) => (
|
||||
<DropdownMenuItem key={item.href} render={<Link href={item.href} />}>
|
||||
{item.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{user.role === "ADMIN"
|
||||
? ADMIN_LINKS.map((item) => (
|
||||
<DropdownMenuItem key={item.href} render={<Link href={item.href} />}>
|
||||
{item.label}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
: null}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</nav>
|
||||
<div className="flex shrink-0 items-center gap-2 text-sm">
|
||||
<NotificationBell />
|
||||
<span className="hidden max-w-40 truncate text-muted-foreground md:inline">
|
||||
@@ -121,7 +54,10 @@ export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex-1 pb-16 sm:pb-0">{children}</div>
|
||||
<div className="flex-1 overflow-y-auto pb-16 sm:pb-0">
|
||||
<PageTransition>{children}</PageTransition>
|
||||
</div>
|
||||
</div>
|
||||
<BottomNav />
|
||||
<QuickAddMenu />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function PageTransition({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<div key={pathname} className="motion-safe:animate-page-in">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export default function QueensPage() {
|
||||
onValueChange={(value) => setStatus(value === "ALL" ? "" : ((value ?? "") as QueenStatus))}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Status" />
|
||||
<SelectValue placeholder="Status">{status || "Alle Status"}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">Alle Status</SelectItem>
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
CalendarDays,
|
||||
Crown,
|
||||
Droplet,
|
||||
FileClock,
|
||||
Hexagon,
|
||||
History,
|
||||
LayoutDashboard,
|
||||
Layers,
|
||||
ListTodo,
|
||||
type LucideIcon,
|
||||
MapPin,
|
||||
Package,
|
||||
Search,
|
||||
ShoppingBag,
|
||||
Users,
|
||||
Warehouse,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { AuthUser } from "@/lib/auth/auth-context";
|
||||
|
||||
interface NavItem {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
|
||||
interface NavGroup {
|
||||
label: string | null;
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
const NAV_GROUPS: NavGroup[] = [
|
||||
{
|
||||
label: null,
|
||||
items: [
|
||||
{ label: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
|
||||
{ label: "Suche", href: "/search", icon: Search },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Betrieb",
|
||||
items: [
|
||||
{ label: "Bienenstände", href: "/apiaries", icon: MapPin },
|
||||
{ label: "Völker", href: "/colonies", icon: Layers },
|
||||
{ label: "Königinnen", href: "/queens", icon: Crown },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Arbeit",
|
||||
items: [
|
||||
{ label: "Aufgaben", href: "/tasks", icon: ListTodo },
|
||||
{ label: "Kalender", href: "/calendar", icon: CalendarDays },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Ernte & Lager",
|
||||
items: [
|
||||
{ label: "Honigchargen", href: "/honey-batches", icon: Droplet },
|
||||
{ label: "Abfüllungen", href: "/bottling/new", icon: Package },
|
||||
{ label: "Produkte", href: "/products", icon: ShoppingBag },
|
||||
{ label: "Lager", href: "/storage", icon: Warehouse },
|
||||
{ label: "Inventar", href: "/equipment", icon: Wrench },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const ADMIN_GROUP: NavGroup = {
|
||||
label: "Verwaltung",
|
||||
items: [
|
||||
{ label: "Benutzer", href: "/settings/users", icon: Users },
|
||||
{ label: "Audit-Log", href: "/settings/audit-log", icon: History },
|
||||
],
|
||||
};
|
||||
|
||||
function SidebarLink({ item, pathname }: { item: NavItem; pathname: string }) {
|
||||
const Icon = item.icon;
|
||||
const isActive = pathname.startsWith(item.href);
|
||||
return (
|
||||
<Link
|
||||
href={item.href}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-150",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4.5 shrink-0" />
|
||||
<span className="truncate">{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar({ user }: { user: AuthUser }) {
|
||||
const pathname = usePathname();
|
||||
const groups = user.role === "ADMIN" ? [...NAV_GROUPS, ADMIN_GROUP] : NAV_GROUPS;
|
||||
|
||||
return (
|
||||
<aside className="hidden w-60 shrink-0 flex-col gap-1 overflow-y-auto border-r bg-card px-3 py-4 sm:flex">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="mb-4 flex items-center gap-2 px-2 font-heading text-lg font-semibold"
|
||||
>
|
||||
<Hexagon className="size-5.5 fill-primary/15 text-primary" strokeWidth={2.25} />
|
||||
ApiCore
|
||||
</Link>
|
||||
<nav className="flex flex-1 flex-col gap-4">
|
||||
{groups.map((group, index) => (
|
||||
<div key={group.label ?? `group-${index}`} className="flex flex-col gap-0.5">
|
||||
{group.label ? (
|
||||
<p className="px-3 pb-1 text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||
{group.label}
|
||||
</p>
|
||||
) : null}
|
||||
{group.items.map((item) => (
|
||||
<SidebarLink key={item.href} item={item} pathname={pathname} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
<div className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground">
|
||||
<FileClock className="size-3.5 shrink-0" />
|
||||
<span className="truncate">v1.x</span>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export default function TasksPage() {
|
||||
onValueChange={(value) => setStatus(value === "ALL" ? "" : ((value ?? "") as TaskStatus))}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Status" />
|
||||
<SelectValue placeholder="Status">{status || "Alle Status"}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">Alle Status</SelectItem>
|
||||
@@ -87,7 +87,7 @@ export default function TasksPage() {
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Priorität" />
|
||||
<SelectValue placeholder="Priorität">{priority || "Alle Prioritäten"}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">Alle Prioritäten</SelectItem>
|
||||
|
||||
@@ -50,6 +50,18 @@
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
--animate-page-in: page-in 260ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes page-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
Reference in new issue
Block a user