Files
Speisekarte/components/AppHeader.tsx
T
florian.wildeandClaude Sonnet 5 0c69a21a85 feat: interner Bereich für Mobilgeräte nutzbar
AppHeader.tsx: Hamburger-Menü unterhalb "sm" mit denselben Links
gestapelt (Vorschläge-Badge-Logik in gemeinsame renderLinks()-Funktion
ausgelagert), Panel schließt automatisch bei Navigation. Bisherige
einzeilige Navigation bleibt ab "sm" unverändert.

WeekEditor.tsx: Mobile Tag-für-Tag-Ansicht mit Tab-Leiste (Mo-Fr)
statt der 6-spaltigen Tabelle, die bei 375px unbenutzbar gequetscht
war. Zellinhalt (Eintragsliste + "hinzufügen"-Flow) in renderCell()
extrahiert und von Tabelle und Karten-Ansicht gemeinsam genutzt, damit
der Bearbeitungs-Zustand nicht dupliziert werden muss. Desktop-Tabelle
unverändert ab "sm".

DishManager/CodeListManager/UserManager/SectionManager/PasskeyManager:
Kopf- und Listenzeilen brechen jetzt bei Bedarf um (flex-wrap) statt
Buttons abzuschneiden, gleiches Muster wie bereits in
SuggestionReview.tsx vorhanden.

Version 1.9.5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 00:54:05 +02:00

216 lines
7.1 KiB
TypeScript

"use client";
// Gemeinsamer Kopfbereich für alle internen Seiten. Statt zweier
// getrennter Bereiche (Kantine/Verwaltung) gibt es eine Navigation:
// Admins sehen zusätzlich die Verwaltungs-Links, der aktive Punkt
// wird hervorgehoben. Ab "sm" die klassische einzeilige Navigation,
// darunter ein Hamburger-Menü mit denselben Links gestapelt.
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import type { Role } from "@/lib/types";
import { formatMessage } from "@/lib/i18n/shared";
import { useDict } from "@/components/i18n/LocaleProvider";
import LanguageSwitcher from "@/components/i18n/LanguageSwitcher";
interface NavLink {
href: string;
label: string;
}
export default function AppHeader({
role,
displayName,
}: {
role: Role;
displayName: string;
}) {
const pathname = usePathname();
const dict = useDict();
const [suggestionCount, setSuggestionCount] = useState(0);
const [mobileOpen, setMobileOpen] = useState(false);
useEffect(() => {
fetch("/api/dish-suggestions")
.then((res) => (res.ok ? res.json() : []))
.then((data) => setSuggestionCount(Array.isArray(data) ? data.length : 0))
.catch(() => {});
}, [pathname]);
// Hamburger-Panel bei jeder Navigation automatisch schließen.
useEffect(() => {
setMobileOpen(false);
}, [pathname]);
const menuLinks: NavLink[] = [
{ href: "/intern", label: dict.header.weekPlan },
{ href: "/intern/essenszeiten", label: dict.header.sections },
{ href: "/intern/gerichte", label: dict.header.dishes },
{ href: "/intern/kennzeichnung", label: dict.header.labels },
{ href: "/intern/vorschlaege", label: dict.header.suggestions },
];
const adminLinks: NavLink[] = [
{ href: "/admin", label: dict.header.users },
{ href: "/admin/design", label: dict.header.design },
];
const linkClass = (href: string) =>
`rounded px-2 py-0.5 transition-colors hover:bg-white/10 ${
pathname === href ? "bg-white/20 font-semibold" : "text-white/90"
}`;
// Von Desktop- und Mobile-Navigation gemeinsam genutzt, damit die
// Vorschläge-Badge-Logik nicht doppelt gepflegt werden muss.
function renderLinks(links: NavLink[]) {
return links.map((l) => (
<Link key={l.href} href={l.href} className={`relative ${linkClass(l.href)}`}>
{l.label}
{l.href === "/intern/vorschlaege" && suggestionCount > 0 && (
<span
className="absolute -top-1.5 -right-1.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-[var(--c-del)] px-1 text-[10px] font-bold leading-none text-white"
aria-label={formatMessage(dict.header.openSuggestions, {
count: suggestionCount,
})}
>
{suggestionCount}
</span>
)}
</Link>
));
}
return (
<header className="bg-[var(--c-primary-dark)] text-white">
{/* Mobil (< "sm"): kompakte Kopfzeile mit Hamburger-Toggle */}
<div className="sm:hidden flex items-center justify-between px-4 py-3">
<span className="font-bold text-base">{dict.header.canteen}</span>
<button
type="button"
className="p-1 -m-1"
onClick={() => setMobileOpen((v) => !v)}
aria-label={dict.header.menuToggle}
aria-expanded={mobileOpen}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
aria-hidden="true"
>
{mobileOpen ? (
<path d="M6 6l12 12M18 6L6 18" />
) : (
<path d="M4 7h16M4 12h16M4 17h16" />
)}
</svg>
</button>
</div>
{mobileOpen && (
<div className="sm:hidden px-4 pb-4 flex flex-col gap-4 text-sm border-t border-white/10">
<nav className="flex flex-col gap-1" aria-label={dict.header.menuGroup}>
<span className="mt-3 text-xs uppercase tracking-wider text-white/50">
{dict.header.menuGroup}
</span>
{renderLinks(menuLinks)}
</nav>
{role === "admin" && (
<nav className="flex flex-col gap-1" aria-label={dict.header.adminGroup}>
<span className="text-xs uppercase tracking-wider text-white/50">
{dict.header.adminGroup}
</span>
{renderLinks(adminLinks)}
</nav>
)}
<Link
href="/"
className="w-fit rounded px-2 py-0.5 text-white/70 hover:bg-white/10"
>
{dict.header.viewMenu}
</Link>
<div className="flex items-center gap-3 border-t border-white/10 pt-3">
<LanguageSwitcher />
<Link
href="/konto"
className={`rounded px-2 py-0.5 hover:bg-white/10 ${
pathname === "/konto" ? "bg-white/20 font-semibold" : "text-white/80"
}`}
>
{displayName}
</Link>
</div>
<form action="/logout" method="post">
<button type="submit" className="btn btn-del w-full">
{dict.common.logout}
</button>
</form>
</div>
)}
{/* Ab "sm": bisherige einzeilige Navigation */}
<div className="hidden sm:flex max-w-[1600px] mx-auto w-full px-4 py-3 flex-wrap items-center gap-x-6 gap-y-2 text-sm">
<span className="font-bold text-base">{dict.header.canteen}</span>
<nav
className="flex flex-wrap items-center gap-x-1 gap-y-1"
aria-label={dict.header.menuGroup}
>
<span className="mr-1 text-xs uppercase tracking-wider text-white/50">
{dict.header.menuGroup}
</span>
{renderLinks(menuLinks)}
</nav>
{role === "admin" && (
<nav
className="flex flex-wrap items-center gap-x-1 gap-y-1"
aria-label={dict.header.adminGroup}
>
<span className="mr-1 text-xs uppercase tracking-wider text-white/50">
{dict.header.adminGroup}
</span>
{renderLinks(adminLinks)}
</nav>
)}
<Link
href="/"
className="rounded px-2 py-0.5 text-white/70 hover:bg-white/10"
>
{dict.header.viewMenu}
</Link>
<div className="ml-auto flex items-center gap-3">
<LanguageSwitcher />
<Link
href="/konto"
title={dict.header.myAccount}
className={`rounded px-2 py-0.5 hover:bg-white/10 ${
pathname === "/konto"
? "bg-white/20 font-semibold"
: "text-white/80"
}`}
>
{displayName}
</Link>
<form action="/logout" method="post">
<button type="submit" className="btn btn-del">
{dict.common.logout}
</button>
</form>
</div>
</div>
</header>
);
}