diff --git a/app/globals.css b/app/globals.css index a2dc41e..4139480 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,110 @@ @import "tailwindcss"; +/* Farb-Variablen: Defaults entsprechen r-hofmann.de; werden zur Laufzeit + aus den Einstellungen (Supabase settings.colors) überschrieben. */ :root { - --background: #ffffff; - --foreground: #171717; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } + --c-primary: #003388; + --c-primary-dark: #0d315c; + --c-accent: #ff6900; + --c-header-text: #ffffff; + --c-row-bg: #e3e4f1; + --c-row-bg-alt: #f4f4fa; + --c-add: #6da54c; + --c-del: #e2371d; } body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + background: #ffffff; + color: #1a1a1a; + font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif; +} + +/* ---------- Buttons ---------- */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 9999px; + padding: 0.35rem 1rem; + font-size: 0.8rem; + font-weight: 600; + color: #ffffff; + cursor: pointer; + border: 1px solid rgb(0 0 0 / 0.15); + transition: filter 0.15s ease; + text-decoration: none; + white-space: nowrap; +} +.btn:hover { filter: brightness(1.08); } +.btn:disabled { opacity: 0.5; cursor: not-allowed; } +.btn-add { background: var(--c-add); } +.btn-del { background: var(--c-del); } +.btn-primary { background: var(--c-primary); } +.btn-neutral { background: #6b7280; } + +/* ---------- Formulare ---------- */ +.input { + width: 100%; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + padding: 0.4rem 0.6rem; + font-size: 0.9rem; + background: #ffffff; + color: #1a1a1a; +} +.input:focus { outline: 2px solid var(--c-primary); outline-offset: 0; } +.label { + display: block; + font-size: 0.8rem; + font-weight: 600; + margin-bottom: 0.15rem; + color: #374151; +} +.card { + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + padding: 1rem; + box-shadow: 0 1px 2px rgb(0 0 0 / 0.05); +} + +/* ---------- Speisekarten-Tabelle ---------- */ +.menu-table { + width: 100%; + border-collapse: separate; + border-spacing: 2px; + table-layout: fixed; +} +.menu-table th.day-head { + background: var(--c-primary); + color: var(--c-header-text); + padding: 0.5rem 0.5rem; + font-size: 0.9rem; + line-height: 1.3; +} +.menu-table th.section-head { + background: var(--c-primary); + color: var(--c-header-text); + text-align: left; + vertical-align: middle; + padding: 0.6rem 0.8rem; + font-size: 0.95rem; +} +.menu-table th.section-head .time { + font-weight: 400; + font-size: 0.8rem; +} +.menu-table td.cell { + background: var(--c-row-bg); + vertical-align: top; + text-align: center; + padding: 0.5rem; + font-size: 0.85rem; +} +.menu-table tr:nth-child(even) td.cell { background: var(--c-row-bg-alt); } +.menu-table td.cell .dish { margin-bottom: 0.35rem; } +.menu-table td.cell .dish sup { + font-size: 0.6rem; + color: #4b5563; + margin-left: 0.15rem; } diff --git a/app/layout.tsx b/app/layout.tsx index 976eb90..aa0b6b3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,33 +1,47 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Geist } from "next/font/google"; import "./globals.css"; +import { createClient } from "@/lib/supabase/server"; +import { COLOR_VARS, DEFAULT_COLORS } from "@/lib/types"; + +// Farben/Logo sollen Änderungen sofort widerspiegeln. +export const dynamic = "force-dynamic"; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Speisekarte", + description: "Speisekarte der Betriebskantine", }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const supabase = await createClient(); + const { data: settings } = await supabase + .from("settings") + .select("colors") + .eq("id", 1) + .maybeSingle(); + + const colors: Record = { + ...DEFAULT_COLORS, + ...((settings?.colors as Record) ?? {}), + }; + const style = Object.fromEntries( + Object.entries(COLOR_VARS).map(([key, cssVar]) => [cssVar, colors[key]]) + ) as React.CSSProperties; + return ( - - {children} + + + {children} + ); } diff --git a/app/page.tsx b/app/page.tsx index 3f36f7c..6bfa26b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,65 +1,148 @@ +import Link from "next/link"; import Image from "next/image"; +import { createClient } from "@/lib/supabase/server"; +import { + addDays, + formatDateLong, + formatPrice, + mondayOf, + WEEKDAYS, +} from "@/lib/menu"; +import { logoUrl } from "@/lib/types"; +import type { CodeItem, MenuEntry, Section } from "@/lib/types"; + +export default async function Home({ + searchParams, +}: { + searchParams: Promise<{ woche?: string }>; +}) { + const { woche } = await searchParams; + const monday = mondayOf(woche); + const friday = addDays(monday, 4); + 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)") + .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); + + const cell = (iso: string, sectionKey: string) => + entries.filter((e) => e.menu_date === iso && e.section_key === sectionKey); -export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

+
+ {/* Kopfbereich: Logo zentriert, Login-Link rechts */} +
+
+ {logo ? ( + // eslint-disable-next-line @next/next/no-img-element + Logo + ) : ( +

+ Speisekarte +

+ )}
-
+ + {/* Wochen-Navigation */} +
+ + ← Vorherige Woche + + + Nächste Woche → + +
+ + {/* 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

+
    + {allergens.map((a) => ( +
  • + {a.code} – {a.name} +
  • + ))} +
-
+
+

+ Zusatzstoffe +

+
    + {additives.map((a) => ( +
  • + {a.code} – {a.name} +
  • + ))} +
+
+
); }