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>
This commit is contained in:
+114
-24
@@ -3,7 +3,8 @@
|
||||
// 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.
|
||||
// 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";
|
||||
@@ -13,6 +14,11 @@ 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,
|
||||
@@ -23,6 +29,7 @@ export default function AppHeader({
|
||||
const pathname = usePathname();
|
||||
const dict = useDict();
|
||||
const [suggestionCount, setSuggestionCount] = useState(0);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/dish-suggestions")
|
||||
@@ -31,14 +38,19 @@ export default function AppHeader({
|
||||
.catch(() => {});
|
||||
}, [pathname]);
|
||||
|
||||
const menuLinks = [
|
||||
// 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 = [
|
||||
const adminLinks: NavLink[] = [
|
||||
{ href: "/admin", label: dict.header.users },
|
||||
{ href: "/admin/design", label: dict.header.design },
|
||||
];
|
||||
@@ -48,9 +60,105 @@ export default function AppHeader({
|
||||
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">
|
||||
<div className="max-w-[1600px] mx-auto w-full px-4 py-3 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm">
|
||||
{/* 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
|
||||
@@ -60,21 +168,7 @@ export default function AppHeader({
|
||||
<span className="mr-1 text-xs uppercase tracking-wider text-white/50">
|
||||
{dict.header.menuGroup}
|
||||
</span>
|
||||
{menuLinks.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>
|
||||
))}
|
||||
{renderLinks(menuLinks)}
|
||||
</nav>
|
||||
|
||||
{role === "admin" && (
|
||||
@@ -85,11 +179,7 @@ export default function AppHeader({
|
||||
<span className="mr-1 text-xs uppercase tracking-wider text-white/50">
|
||||
{dict.header.adminGroup}
|
||||
</span>
|
||||
{adminLinks.map((l) => (
|
||||
<Link key={l.href} href={l.href} className={linkClass(l.href)}>
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
{renderLinks(adminLinks)}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user