Files
Speisekarte/app/page.tsx
T
florian.wilde 1db89a54bf Öffentliche Speisekarte: Heute-Button, mobile Tages-/Wochenansicht
- "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
2026-07-18 00:46:39 +02:00

165 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from "next/link";
import { createClient } from "@/lib/supabase/server";
import {
addDays,
dishCodes,
entriesFor,
formatDateLong,
formatPrice,
mondayOf,
todayISO,
WEEKDAYS,
} from "@/lib/menu";
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,
}: {
searchParams: Promise<{ woche?: string }>;
}) {
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),
}));
const supabase = await createClient();
const [sectionsRes, entriesRes, allergensRes, additivesRes, settingsRes] =
await Promise.all([
supabase.from("sections").select("*").order("sort"),
supabase
.from("menu_entries")
.select(
"id, menu_date, section_key, position, dish:dishes(id, name, price, allergens, additives)"
)
.gte("menu_date", monday)
.lte("menu_date", friday)
.order("position")
.order("created_at"),
supabase.from("allergens").select("*").order("sort"),
supabase.from("additives").select("*").order("sort"),
supabase.from("settings").select("logo_path").eq("id", 1).maybeSingle(),
]);
const sections = (sectionsRes.data ?? []) as Section[];
const entries = (entriesRes.data ?? []) as unknown as MenuEntry[];
const allergens = (allergensRes.data ?? []) as CodeItem[];
const additives = (additivesRes.data ?? []) as CodeItem[];
const logo = logoUrl(settingsRes.data?.logo_path);
return (
<div className="flex-1 px-4 py-6 max-w-[1600px] mx-auto w-full">
{/* Kopfbereich: Logo zentriert, Login-Link rechts */}
<header className="relative mb-6">
<div className="flex justify-center">
{logo ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={logo} alt="Logo" className="h-20 object-contain" />
) : (
<h1 className="text-4xl font-extrabold tracking-tight text-[var(--c-primary)]">
{SITE_TITLE}
</h1>
)}
</div>
<Link
href="/login"
className="absolute right-0 top-0 text-sm text-gray-500 hover:text-[var(--c-primary)]"
>
Anmelden
</Link>
</header>
{/* Mobil: Tag (Standard) oder Woche umschaltbar */}
<PublicMobileMenu monday={monday} sections={sections} entries={entries} />
{/* Ab "sm": klassische Wochentabelle */}
<div className="hidden sm:block">
<div className="flex gap-3 mb-4">
<Link href={`/?woche=${addDays(monday, -7)}`} className="btn btn-add">
← Vorherige Woche
</Link>
<Link href={`/?woche=${currentWeek}`} className="btn btn-primary">
Heute
</Link>
<Link href={`/?woche=${addDays(monday, 7)}`} className="btn btn-add">
Nächste Woche →
</Link>
</div>
<div className="overflow-x-auto">
<table className="menu-table">
<thead>
<tr>
<th className="section-head w-56" aria-hidden />
{days.map((d) => (
<th key={d.iso} className="day-head">
<span className="font-bold">{d.weekday}</span>
<br />
{formatDateLong(d.iso)}
</th>
))}
</tr>
</thead>
<tbody>
{sections.map((s) => (
<tr key={s.key}>
<th className="section-head">
<div>{s.label}</div>
<div className="time">{s.time_range}</div>
</th>
{days.map((d) => (
<td key={d.iso} className="cell">
{entriesFor(entries, d.iso, s.key).map((e) => {
const codes = dishCodes(e.dish);
return (
<div key={e.id} className="dish">
<span className="font-bold">{e.dish.name}</span>
{codes && <sup className="dish-codes">{codes}</sup>}
{" – "}
{formatPrice(e.dish.price)}
</div>
);
})}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Allergene & Zusatzstoffe: Legende zu den Kürzeln bei den Gerichten */}
<section className="mt-8 grid gap-6 sm:grid-cols-2 text-sm text-gray-700">
<div>
<h2 className="font-bold text-[var(--c-primary)] mb-2">Allergene</h2>
<ul className="space-y-0.5">
{allergens.map((a) => (
<li key={a.code}>
<span className="font-semibold">{a.code}</span> – {a.name}
</li>
))}
</ul>
</div>
<div>
<h2 className="font-bold text-[var(--c-primary)] mb-2">
Zusatzstoffe
</h2>
<ul className="space-y-0.5">
{additives.map((a) => (
<li key={a.code}>
<span className="font-semibold">{a.code}</span> – {a.name}
</li>
))}
</ul>
</div>
</section>
</div>
);
}