Files
Wabenhain/src/components/admin-shell.tsx
T
Nick SzaunigandClaude Opus 5.5 e6455b05e6 feat(ui): Admin-Seitenleiste mit Wabenmarkierung statt Seitenbalken
Aktive Seite mit Honig-Wabe statt farbigem border-left, feines Wabenraster
in der Seitenleiste (liegt per isolate/-z-10 unter dem Menue).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 13:28:40 +02:00

179 lines
6.9 KiB
TypeScript

"use client";
import type { ReactNode } from "react";
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { AnimatePresence, motion } from "motion/react";
import { Logomark } from "@/components/logomark";
import { HexMark, HoneycombPattern } from "@/components/honeycomb";
import { useI18n } from "@/i18n/i18n-provider";
import type { Dictionary } from "@/i18n/dictionary-type";
function getAdminNav(t: Dictionary) {
return [
{ href: "/admin", label: t.adminShell.navDashboard },
{ href: "/admin/bestellungen", label: t.adminShell.navOrders },
{ href: "/admin/retouren", label: t.adminShell.navReturns },
{ href: "/admin/produkte", label: t.adminShell.navProducts },
{ href: "/admin/ernten", label: t.adminShell.navHarvests },
{ href: "/admin/chargen", label: t.adminShell.navBatches },
{ href: "/admin/anfragen", label: t.adminShell.navBusinessInquiries },
{ href: "/admin/benutzer", label: t.adminShell.navUsers },
{ href: "/admin/versand", label: t.adminShell.navShipping },
{ href: "/admin/gutscheine", label: t.adminShell.navVouchers },
{ href: "/admin/bewertungen", label: t.adminShell.navReviews },
{ href: "/admin/inhalte", label: t.adminShell.navContent },
{ href: "/admin/audit-log", label: t.adminShell.navAuditLog },
];
}
function isActiveHref(pathname: string, href: string): boolean {
if (href === "/admin") return pathname === "/admin";
return pathname === href || pathname.startsWith(`${href}/`);
}
function NavLinks({
pathname,
adminNav,
onNavigate,
}: {
pathname: string;
adminNav: ReturnType<typeof getAdminNav>;
onNavigate?: () => void;
}) {
return (
<>
{adminNav.map((item) => {
const active = isActiveHref(pathname, item.href);
return (
<Link
key={item.href}
href={item.href}
onClick={onNavigate}
aria-current={active ? "page" : undefined}
className={`group flex items-center gap-3 px-5 py-2.5 text-sm transition-colors ${
active ? "bg-cream/10 text-cream" : "text-cream/70 hover:text-cream hover:bg-cream/5"
}`}
>
{/* Wabe statt farbigem Seitenbalken markiert die aktive Seite. */}
<HexMark
filled={active}
className={`h-2.5 w-2.5 flex-shrink-0 transition-[color,transform] duration-300 ${
active ? "text-honey-light" : "text-cream/25 group-hover:text-cream/60 group-hover:rotate-90"
}`}
/>
{item.label}
</Link>
);
})}
</>
);
}
function Wordmark() {
return (
<span className="flex items-center gap-2">
<Logomark className="h-7 w-7 flex-shrink-0" variant="inverted" />
<span className="tracking-[0.15em] text-xs whitespace-nowrap">WABENHAIN ADMIN</span>
</span>
);
}
interface AdminShellProps {
adminName: string;
logoutAction: () => void | Promise<void>;
children: ReactNode;
}
/**
* Sidebar-Layout fuer den Admin-Bereich statt der frueheren einzeiligen,
* horizontal scrollenden Navigation (11 Punkte passten auf typischen
* Bildschirmbreiten nicht nebeneinander). Ab lg persistente Sidebar links,
* darunter ein Vollbild-Overlay-Menue nach demselben Muster wie die mobile
* Hauptnavigation in site-chrome.tsx.
*/
export function AdminShell({ adminName, logoutAction, children }: AdminShellProps) {
const { t } = useI18n();
const adminNav = getAdminNav(t);
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
return (
<div className="min-h-screen bg-cream/40 lg:flex">
<aside className="relative isolate hidden overflow-hidden lg:flex lg:flex-col lg:w-60 lg:shrink-0 lg:sticky lg:top-0 lg:h-screen bg-forest text-cream">
<HoneycombPattern className="-z-10 text-honey-light/[0.06]" size={18} fade="bottom" />
<div className="flex items-center h-16 px-5 border-b border-cream/10">
<Wordmark />
</div>
<nav aria-label={t.adminShell.navAriaLabel} className="flex-1 overflow-y-auto py-4 flex flex-col gap-0.5">
<NavLinks pathname={pathname} adminNav={adminNav} />
</nav>
<div className="px-5 py-4 border-t border-cream/10">
<p className="text-xs text-cream/60 truncate">{t.adminShell.loggedInAs(adminName)}</p>
<form action={logoutAction} className="mt-2">
<button type="submit" className="text-xs text-cream/80 underline hover:text-cream">
{t.adminShell.logout}
</button>
</form>
</div>
</aside>
<div className="flex-1 min-w-0">
<header className="lg:hidden sticky top-0 z-30 bg-forest text-cream flex items-center justify-between h-16 px-5">
<Wordmark />
<button
aria-label={t.adminShell.openMenu}
className="w-10 h-10 flex items-center justify-center flex-shrink-0"
onClick={() => setMobileOpen(true)}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M3 6h18M3 12h18M3 18h18" />
</svg>
</button>
</header>
<main className="max-w-6xl mx-auto px-6 py-10 overflow-x-hidden">
{children}
</main>
</div>
<AnimatePresence>
{mobileOpen ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="lg:hidden fixed inset-0 z-50 bg-forest text-cream flex flex-col"
>
<div className="flex items-center justify-between h-16 px-5 border-b border-cream/10 flex-shrink-0">
<Wordmark />
<button
aria-label={t.adminShell.closeMenu}
className="w-10 h-10 flex items-center justify-center flex-shrink-0"
onClick={() => setMobileOpen(false)}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M6 6l12 12M18 6L6 18" />
</svg>
</button>
</div>
<nav aria-label={t.adminShell.mobileNavAriaLabel} className="flex-1 overflow-y-auto py-4">
<NavLinks pathname={pathname} adminNav={adminNav} onNavigate={() => setMobileOpen(false)} />
</nav>
<div className="px-5 py-4 border-t border-cream/10 flex-shrink-0">
<p className="text-xs text-cream/60">{t.adminShell.loggedInAs(adminName)}</p>
<form action={logoutAction} className="mt-2">
<button type="submit" className="text-xs text-cream/80 underline hover:text-cream">
{t.adminShell.logout}
</button>
</form>
</div>
</motion.div>
) : null}
</AnimatePresence>
</div>
);
}