From 1db89a54bfee1086f343a2c6570afbbabcc80fa4 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 18 Jul 2026 00:46:39 +0200 Subject: [PATCH] =?UTF-8?q?=C3=96ffentliche=20Speisekarte:=20Heute-Button,?= =?UTF-8?q?=20mobile=20Tages-/Wochenansicht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Heute" springt direkt zur aktuellen Woche - Unter dem sm-Breakpoint umschaltbar zwischen Tag (Standard) und Woche statt der Tabelle; Desktop bleibt vollständig serverseitig gerendert, nur der mobile Umschalter ist eine Client-Komponente --- app/page.tsx | 122 +++++++++++++---------- components/PublicMobileMenu.tsx | 169 ++++++++++++++++++++++++++++++++ lib/menu.ts | 28 ++++++ 3 files changed, 266 insertions(+), 53 deletions(-) create mode 100644 components/PublicMobileMenu.tsx diff --git a/app/page.tsx b/app/page.tsx index 6bfa26b..a3fd595 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,15 +1,18 @@ import Link from "next/link"; -import Image from "next/image"; import { createClient } from "@/lib/supabase/server"; import { addDays, + dishCodes, + entriesFor, formatDateLong, formatPrice, mondayOf, + todayISO, WEEKDAYS, } from "@/lib/menu"; -import { logoUrl } from "@/lib/types"; +import { logoUrl, SITE_TITLE } from "@/lib/types"; import type { CodeItem, MenuEntry, Section } from "@/lib/types"; +import PublicMobileMenu from "@/components/PublicMobileMenu"; export default async function Home({ searchParams, @@ -19,6 +22,7 @@ export default async function Home({ const { woche } = await searchParams; const monday = mondayOf(woche); const friday = addDays(monday, 4); + const currentWeek = mondayOf(todayISO()); const days = WEEKDAYS.map((weekday, i) => ({ weekday, iso: addDays(monday, i), @@ -30,7 +34,9 @@ export default async function Home({ supabase.from("sections").select("*").order("sort"), supabase .from("menu_entries") - .select("id, menu_date, section_key, position, dish:dishes(id, name, price)") + .select( + "id, menu_date, section_key, position, dish:dishes(id, name, price, allergens, additives)" + ) .gte("menu_date", monday) .lte("menu_date", friday) .order("position") @@ -46,9 +52,6 @@ export default async function Home({ const additives = (additivesRes.data ?? []) as CodeItem[]; const logo = logoUrl(settingsRes.data?.logo_path); - const cell = (iso: string, sectionKey: string) => - entries.filter((e) => e.menu_date === iso && e.section_key === sectionKey); - return (
{/* Kopfbereich: Logo zentriert, Login-Link rechts */} @@ -59,7 +62,7 @@ export default async function Home({ Logo ) : (

- Speisekarte + {SITE_TITLE}

)}
@@ -71,54 +74,67 @@ export default async function Home({ - {/* Wochen-Navigation */} -
- - ← Vorherige Woche - - - Nächste Woche → - + {/* Mobil: Tag (Standard) oder Woche umschaltbar */} + + + {/* Ab "sm": klassische Wochentabelle */} +
+
+ + ← Vorherige Woche + + + Heute + + + Nächste Woche → + +
+ +
+ + + + + ))} + + + + {sections.map((s) => ( + + + {days.map((d) => ( + + ))} + + ))} + +
+ {days.map((d) => ( + + {d.weekday} +
+ {formatDateLong(d.iso)} +
+
{s.label}
+
{s.time_range}
+
+ {entriesFor(entries, d.iso, s.key).map((e) => { + const codes = dishCodes(e.dish); + return ( +
+ {e.dish.name} + {codes && {codes}} + {" – "} + {formatPrice(e.dish.price)} +
+ ); + })} +
+
- {/* Speisekarte */} - - - - - ))} - - - - {sections.map((s) => ( - - - {days.map((d) => ( - - ))} - - ))} - -
- {days.map((d) => ( - - {d.weekday} -
- {formatDateLong(d.iso)} -
-
{s.label}
-
{s.time_range}
-
- {cell(d.iso, s.key).map((e) => ( -
- {e.dish.name} - {" – "} - {formatPrice(e.dish.price)} -
- ))} -
- - {/* Allergene & Zusatzstoffe (gelten für die komplette Speisekarte) */} + {/* Allergene & Zusatzstoffe: Legende zu den Kürzeln bei den Gerichten */}

Allergene

diff --git a/components/PublicMobileMenu.tsx b/components/PublicMobileMenu.tsx new file mode 100644 index 0000000..6161c88 --- /dev/null +++ b/components/PublicMobileMenu.tsx @@ -0,0 +1,169 @@ +"use client"; + +// Mobile Ansicht der öffentlichen Speisekarte: Tag (Standard) oder Woche, +// umschaltbar ohne Seiten-Reload. Ab dem "sm"-Breakpoint übernimmt die +// normale Wochentabelle (siehe app/page.tsx), diese Komponente blendet +// sich dort per CSS aus. + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { + addDays, + dishCodes, + entriesFor, + formatDateLong, + formatPrice, + mondayOf, + todayISO, + weekdayIndex, + WEEKDAYS, + WEEKDAYS_SHORT, +} from "@/lib/menu"; +import type { MenuEntry, Section } from "@/lib/types"; + +export default function PublicMobileMenu({ + monday, + sections, + entries, +}: { + monday: string; + sections: Section[]; + entries: MenuEntry[]; +}) { + const router = useRouter(); + const days = WEEKDAYS.map((weekday, i) => ({ + weekday, + short: WEEKDAYS_SHORT[i], + iso: addDays(monday, i), + })); + const isCurrentWeek = monday === mondayOf(todayISO()); + const todayIdx = isCurrentWeek ? weekdayIndex(todayISO()) : -1; + + const [tab, setTab] = useState<"tag" | "woche">("tag"); + const [selectedIdx, setSelectedIdx] = useState(todayIdx >= 0 ? todayIdx : 0); + + function goToday() { + const currentMonday = mondayOf(todayISO()); + if (currentMonday === monday) { + const idx = weekdayIndex(todayISO()); + setSelectedIdx(idx >= 0 ? idx : 0); + setTab("tag"); + } else { + router.push(`/?woche=${currentMonday}`); + } + } + + function renderDay(day: { weekday: string; iso: string }) { + return ( +
+

+ {day.weekday} · {formatDateLong(day.iso)} +

+
+ {sections.map((s) => { + const items = entriesFor(entries, day.iso, s.key); + return ( +
+
+ {s.label}{" "} + · {s.time_range} +
+
+ {items.length === 0 ? ( +

–

+ ) : ( + items.map((e) => { + const codes = dishCodes(e.dish); + return ( +

+ {e.dish.name} + {codes && {codes}}{" "} + – {formatPrice(e.dish.price)} +

+ ); + }) + )} +
+
+ ); + })} +
+
+ ); + } + + return ( +
+
+ + ← Woche + + + + Woche → + +
+ +
+ + +
+ + {tab === "tag" ? ( + <> +
+ {days.map((d, i) => ( + + ))} +
+ {renderDay(days[selectedIdx])} + + ) : ( +
{days.map((d) => renderDay(d))}
+ )} +
+ ); +} diff --git a/lib/menu.ts b/lib/menu.ts index 6f6edeb..c417dbb 100644 --- a/lib/menu.ts +++ b/lib/menu.ts @@ -2,6 +2,8 @@ // Alle Berechnungen laufen über UTC, damit Server und Browser // unabhängig von der Zeitzone dieselbe Woche ermitteln. +import type { MenuEntry } from "./types"; + const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/; function toDate(iso: string): Date { @@ -31,7 +33,19 @@ export function addDays(iso: string, days: number): string { return toISO(d); } +/** Heutiges Datum als YYYY-MM-DD (UTC). */ +export function todayISO(): string { + return toISO(new Date()); +} + +/** Index (0=Montag…4=Freitag) von `iso` innerhalb seiner Woche, sonst -1 (Wochenende). */ +export function weekdayIndex(iso: string): number { + const day = (toDate(iso).getUTCDay() + 6) % 7; + return day <= 4 ? day : -1; +} + export const WEEKDAYS = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"]; +export const WEEKDAYS_SHORT = ["Mo", "Di", "Mi", "Do", "Fr"]; /** z. B. "13. Juli 2026" */ export function formatDateLong(iso: string): string { @@ -47,3 +61,17 @@ export function formatDateLong(iso: string): string { export function formatPrice(price: number | string): string { return Number(price).toFixed(2).replace(".", ",") + "€"; } + +/** Allergen-/Zusatzstoff-Kürzel eines Gerichts, z. B. "A,G,2" */ +export function dishCodes(dish: { allergens: string[]; additives: string[] }): string { + return [...(dish.allergens ?? []), ...(dish.additives ?? [])].join(","); +} + +/** Einträge eines Tages/Bereichs aus der geladenen Wochenliste herausfiltern. */ +export function entriesFor( + entries: MenuEntry[], + iso: string, + sectionKey: string +): MenuEntry[] { + return entries.filter((e) => e.menu_date === iso && e.section_key === sectionKey); +}