From 0c69a21a85f0362918b53312af01c1b8c271822c Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 20 Jul 2026 00:54:05 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20interner=20Bereich=20f=C3=BCr=20Mobilge?= =?UTF-8?q?r=C3=A4te=20nutzbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 17 ++ components/AppHeader.tsx | 138 +++++++++++--- components/CodeListManager.tsx | 4 +- components/DishManager.tsx | 6 +- components/PasskeyManager.tsx | 2 +- components/SectionManager.tsx | 4 +- components/UserManager.tsx | 4 +- components/WeekEditor.tsx | 319 +++++++++++++++++++-------------- lib/i18n/dictionaries.ts | 3 + package-lock.json | 4 +- package.json | 2 +- 11 files changed, 333 insertions(+), 170 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05227e9..af94ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ Alle nennenswerten Änderungen an diesem Projekt werden hier dokumentiert. Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), Versionierung nach [Semantic Versioning](https://semver.org/lang/de/). +## [1.9.5] – 2026-07-20 + +### Hinzugefügt +- Interner Bereich (Wochenplan, Essenszeiten, Gerichte, Allergene & + Zusatzstoffe, Vorschläge, Benutzer, Design, Mein Konto) für + Mobilgeräte nutzbar gemacht. `AppHeader.tsx` bekommt unterhalb der + `sm`-Breakpoint-Grenze ein Hamburger-Menü mit denselben Links + gestapelt statt der bisherigen, bei schmalen Bildschirmen unübersichtlich + umbrechenden einzeiligen Navigation. Der Wochenplan (`WeekEditor.tsx`) + zeigt auf Mobilgeräten eine Tag-für-Tag-Ansicht mit Tab-Leiste + (Mo–Fr) statt der für 375px viel zu schmalen 6-spaltigen Tabelle – + Gericht hinzufügen/löschen funktioniert unverändert, da beide + Ansichten denselben Bearbeitungs-Zustand teilen. In DishManager, + CodeListManager, UserManager, SectionManager und PasskeyManager + brechen Kopf- und Listenzeilen jetzt bei Bedarf um, statt Buttons + abzuschneiden. + ## [1.9.0] – 2026-07-20 ### Hinzugefügt diff --git a/components/AppHeader.tsx b/components/AppHeader.tsx index 0070ff4..eb298a2 100644 --- a/components/AppHeader.tsx +++ b/components/AppHeader.tsx @@ -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) => ( + + {l.label} + {l.href === "/intern/vorschlaege" && suggestionCount > 0 && ( + + {suggestionCount} + + )} + + )); + } + return (
-
+ {/* Mobil (< "sm"): kompakte Kopfzeile mit Hamburger-Toggle */} +
+ {dict.header.canteen} + +
+ + {mobileOpen && ( +
+ + + {role === "admin" && ( + + )} + + + {dict.header.viewMenu} + + +
+ + + {displayName} + +
+ +
+ +
+
+ )} + + {/* Ab "sm": bisherige einzeilige Navigation */} +
{dict.header.canteen} {role === "admin" && ( @@ -85,11 +179,7 @@ export default function AppHeader({ {dict.header.adminGroup} - {adminLinks.map((l) => ( - - {l.label} - - ))} + {renderLinks(adminLinks)} )} diff --git a/components/CodeListManager.tsx b/components/CodeListManager.tsx index 1619cbc..a4b6c25 100644 --- a/components/CodeListManager.tsx +++ b/components/CodeListManager.tsx @@ -181,7 +181,7 @@ export default function CodeListManager({ return (
-
+

{title}

@@ -291,7 +291,7 @@ export default function CodeListManager({
    {items.map((item) => ( -
  • +
  • {item.code} –{" "} {localized(item, locale)} diff --git a/components/DishManager.tsx b/components/DishManager.tsx index 6a42e0b..5f9632f 100644 --- a/components/DishManager.tsx +++ b/components/DishManager.tsx @@ -284,7 +284,7 @@ export default function DishManager({ return (
    -
    +

    {dict.dishManager.title}

    @@ -444,7 +444,7 @@ export default function DishManager({ /> )} {dishes.length > 0 && ( -
    +
  • +
  • {passkeys.map((pk) => ( -
  • +
  • {pk.name || "Passkey"}{" "} diff --git a/components/SectionManager.tsx b/components/SectionManager.tsx index 6ae91ac..32f88e8 100644 --- a/components/SectionManager.tsx +++ b/components/SectionManager.tsx @@ -124,7 +124,7 @@ export default function SectionManager() { return (
    -
    +

    {dict.sectionManager.title}

    @@ -232,7 +232,7 @@ export default function SectionManager() { ) : (
      {sections.map((section, idx) => ( -
    • +
    • +
      +
    + ); + })} + {isAdding ? ( +
    + {selectedDishObj ? ( +
    + + {localized(selectedDishObj, locale)} ( + {formatPrice(selectedDishObj.price)}) + + +
    + ) : ( + <> + setDishSearch(e.target.value)} + /> +
      + {filteredDishes.map((dish) => ( +
    • + +
    • + ))} + {filteredDishes.length === 0 && ( +
    • + {formatMessage(dict.dishManager.noResults, { + query: dishSearch, + })} +
    • + )} +
    + + )} +
    + + +
    +
    + ) : ( + + )} + + ); + } + + const selectedDay = days[selectedDayIdx]; + return (
    @@ -136,140 +253,76 @@ export default function WeekEditor({ monday }: { monday: string }) { {loading ? (

    {dict.weekEditor.loading}

    ) : ( - - - - + <> + {/* Mobil (< "sm"): ein Tag auf einmal, per Tabs gewählt */} +
    +
    + {days.map((d, i) => ( + ))} -
    - - - {sections.map((s) => ( - - - {days.map((d) => { - const isAdding = - adding?.iso === d.iso && adding?.section === s.key; - return ( - - ); - })} - - ))} - -
    - {days.map((d) => ( - - {d.weekday} -
    - {formatDateLong(d.iso, dict.dateLocale)} -
    -
    {localized(s, locale)}
    -
    {s.time_range}
    -
    - {entriesFor(entries, d.iso, s.key).map((e) => { - const codes = dishCodes(e.dish); - return ( -
    - - {localized(e.dish, locale)} - - {codes && {codes}} - {" – "} - {formatPrice(e.dish.price)} -
    - -
    -
    - ); - })} - {isAdding ? ( -
    - {selectedDishObj ? ( -
    - - {localized(selectedDishObj, locale)} ( - {formatPrice(selectedDishObj.price)}) - - -
    - ) : ( - <> - setDishSearch(e.target.value)} - /> -
      - {filteredDishes.map((dish) => ( -
    • - -
    • - ))} - {filteredDishes.length === 0 && ( -
    • - {formatMessage(dict.dishManager.noResults, { - query: dishSearch, - })} -
    • - )} -
    - - )} -
    - - -
    -
    - ) : ( - - )} -
    +
    +

    + {selectedDay.weekday} · {formatDateLong(selectedDay.iso, dict.dateLocale)} +

    +
    + {sections.map((s) => ( +
    +
    + {localized(s, locale)}{" "} + · {s.time_range} +
    +
    + {renderCell(selectedDay.iso, s.key)} +
    +
    + ))} +
    +
    + + {/* Ab "sm": klassische Wochentabelle */} +
    + + + + + ))} + + + + {sections.map((s) => ( + + + {days.map((d) => ( + + ))} + + ))} + +
    + {days.map((d) => ( + + {d.weekday} +
    + {formatDateLong(d.iso, dict.dateLocale)} +
    +
    {localized(s, locale)}
    +
    {s.time_range}
    +
    + {renderCell(d.iso, s.key)} +
    +
    + )}

    {hintBefore} diff --git a/lib/i18n/dictionaries.ts b/lib/i18n/dictionaries.ts index 5672ac2..05e0505 100644 --- a/lib/i18n/dictionaries.ts +++ b/lib/i18n/dictionaries.ts @@ -40,6 +40,7 @@ const de = { openSuggestions: "{count} offene Rezeptvorschläge", viewMenu: "Speisekarte ansehen", myAccount: "Mein Konto", + menuToggle: "Menü öffnen/schließen", }, login: { title: "Anmeldung", @@ -333,6 +334,7 @@ const en: Dict = { openSuggestions: "{count} open dish suggestions", viewMenu: "View menu", myAccount: "My account", + menuToggle: "Open/close menu", }, login: { title: "Sign in", @@ -623,6 +625,7 @@ const pl: Dict = { openSuggestions: "{count} otwartych propozycji dań", viewMenu: "Zobacz jadłospis", myAccount: "Moje konto", + menuToggle: "Otwórz/zamknij menu", }, login: { title: "Logowanie", diff --git a/package-lock.json b/package-lock.json index d889565..e40b434 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "canteen-menu", - "version": "1.8.1", + "version": "1.9.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "canteen-menu", - "version": "1.8.1", + "version": "1.9.5", "dependencies": { "@simplewebauthn/browser": "^13.3.0", "@simplewebauthn/server": "^13.3.2", diff --git a/package.json b/package.json index f124cd4..071a4fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "canteen-menu", - "version": "1.9.0", + "version": "1.9.5", "private": true, "scripts": { "predev": "node --env-file-if-exists=.env.local db/migrate.mjs && node --env-file-if-exists=.env.local scripts/bootstrap-admin.mjs",