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]>
179 lines
6.9 KiB
TypeScript
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>
|
|
);
|
|
}
|