Merge feature/v1.9.5: interner Bereich für Mobilgeräte nutzbar
This commit is contained in:
@@ -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/),
|
Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
|
||||||
Versionierung nach [Semantic Versioning](https://semver.org/lang/de/).
|
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
|
## [1.9.0] – 2026-07-20
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|||||||
+112
-22
@@ -3,7 +3,8 @@
|
|||||||
// Gemeinsamer Kopfbereich für alle internen Seiten. Statt zweier
|
// Gemeinsamer Kopfbereich für alle internen Seiten. Statt zweier
|
||||||
// getrennter Bereiche (Kantine/Verwaltung) gibt es eine Navigation:
|
// getrennter Bereiche (Kantine/Verwaltung) gibt es eine Navigation:
|
||||||
// Admins sehen zusätzlich die Verwaltungs-Links, der aktive Punkt
|
// 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 Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
@@ -13,6 +14,11 @@ import { formatMessage } from "@/lib/i18n/shared";
|
|||||||
import { useDict } from "@/components/i18n/LocaleProvider";
|
import { useDict } from "@/components/i18n/LocaleProvider";
|
||||||
import LanguageSwitcher from "@/components/i18n/LanguageSwitcher";
|
import LanguageSwitcher from "@/components/i18n/LanguageSwitcher";
|
||||||
|
|
||||||
|
interface NavLink {
|
||||||
|
href: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function AppHeader({
|
export default function AppHeader({
|
||||||
role,
|
role,
|
||||||
displayName,
|
displayName,
|
||||||
@@ -23,6 +29,7 @@ export default function AppHeader({
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const dict = useDict();
|
const dict = useDict();
|
||||||
const [suggestionCount, setSuggestionCount] = useState(0);
|
const [suggestionCount, setSuggestionCount] = useState(0);
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/dish-suggestions")
|
fetch("/api/dish-suggestions")
|
||||||
@@ -31,14 +38,19 @@ export default function AppHeader({
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [pathname]);
|
}, [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", label: dict.header.weekPlan },
|
||||||
{ href: "/intern/essenszeiten", label: dict.header.sections },
|
{ href: "/intern/essenszeiten", label: dict.header.sections },
|
||||||
{ href: "/intern/gerichte", label: dict.header.dishes },
|
{ href: "/intern/gerichte", label: dict.header.dishes },
|
||||||
{ href: "/intern/kennzeichnung", label: dict.header.labels },
|
{ href: "/intern/kennzeichnung", label: dict.header.labels },
|
||||||
{ href: "/intern/vorschlaege", label: dict.header.suggestions },
|
{ href: "/intern/vorschlaege", label: dict.header.suggestions },
|
||||||
];
|
];
|
||||||
const adminLinks = [
|
const adminLinks: NavLink[] = [
|
||||||
{ href: "/admin", label: dict.header.users },
|
{ href: "/admin", label: dict.header.users },
|
||||||
{ href: "/admin/design", label: dict.header.design },
|
{ href: "/admin/design", label: dict.header.design },
|
||||||
];
|
];
|
||||||
@@ -48,19 +60,10 @@ export default function AppHeader({
|
|||||||
pathname === href ? "bg-white/20 font-semibold" : "text-white/90"
|
pathname === href ? "bg-white/20 font-semibold" : "text-white/90"
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
return (
|
// Von Desktop- und Mobile-Navigation gemeinsam genutzt, damit die
|
||||||
<header className="bg-[var(--c-primary-dark)] text-white">
|
// Vorschläge-Badge-Logik nicht doppelt gepflegt werden muss.
|
||||||
<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">
|
function renderLinks(links: NavLink[]) {
|
||||||
<span className="font-bold text-base">{dict.header.canteen}</span>
|
return links.map((l) => (
|
||||||
|
|
||||||
<nav
|
|
||||||
className="flex flex-wrap items-center gap-x-1 gap-y-1"
|
|
||||||
aria-label={dict.header.menuGroup}
|
|
||||||
>
|
|
||||||
<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)}`}>
|
<Link key={l.href} href={l.href} className={`relative ${linkClass(l.href)}`}>
|
||||||
{l.label}
|
{l.label}
|
||||||
{l.href === "/intern/vorschlaege" && suggestionCount > 0 && (
|
{l.href === "/intern/vorschlaege" && suggestionCount > 0 && (
|
||||||
@@ -74,7 +77,98 @@ export default function AppHeader({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="bg-[var(--c-primary-dark)] text-white">
|
||||||
|
{/* 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
|
||||||
|
className="flex flex-wrap items-center gap-x-1 gap-y-1"
|
||||||
|
aria-label={dict.header.menuGroup}
|
||||||
|
>
|
||||||
|
<span className="mr-1 text-xs uppercase tracking-wider text-white/50">
|
||||||
|
{dict.header.menuGroup}
|
||||||
|
</span>
|
||||||
|
{renderLinks(menuLinks)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{role === "admin" && (
|
{role === "admin" && (
|
||||||
@@ -85,11 +179,7 @@ export default function AppHeader({
|
|||||||
<span className="mr-1 text-xs uppercase tracking-wider text-white/50">
|
<span className="mr-1 text-xs uppercase tracking-wider text-white/50">
|
||||||
{dict.header.adminGroup}
|
{dict.header.adminGroup}
|
||||||
</span>
|
</span>
|
||||||
{adminLinks.map((l) => (
|
{renderLinks(adminLinks)}
|
||||||
<Link key={l.href} href={l.href} className={linkClass(l.href)}>
|
|
||||||
{l.label}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export default function CodeListManager({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex flex-wrap items-center gap-3 mb-3">
|
||||||
<h2 className="text-lg font-bold text-[var(--c-primary)] mr-auto">
|
<h2 className="text-lg font-bold text-[var(--c-primary)] mr-auto">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -291,7 +291,7 @@ export default function CodeListManager({
|
|||||||
|
|
||||||
<ul className="divide-y divide-gray-100">
|
<ul className="divide-y divide-gray-100">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<li key={item.code} className="flex items-center gap-2 py-1.5 text-sm">
|
<li key={item.code} className="flex flex-wrap items-center gap-2 py-1.5 text-sm">
|
||||||
<span className="mr-auto">
|
<span className="mr-auto">
|
||||||
<span className="font-semibold">{item.code}</span> –{" "}
|
<span className="font-semibold">{item.code}</span> –{" "}
|
||||||
{localized(item, locale)}
|
{localized(item, locale)}
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ export default function DishManager({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl">
|
<div className="max-w-3xl">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||||
<h1 className="text-xl font-bold text-[var(--c-primary)] mr-auto">
|
<h1 className="text-xl font-bold text-[var(--c-primary)] mr-auto">
|
||||||
{dict.dishManager.title}
|
{dict.dishManager.title}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -444,7 +444,7 @@ export default function DishManager({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{dishes.length > 0 && (
|
{dishes.length > 0 && (
|
||||||
<div className="flex items-center gap-3 mb-2 text-sm">
|
<div className="flex flex-wrap items-center gap-3 mb-2 text-sm">
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -474,7 +474,7 @@ export default function DishManager({
|
|||||||
{visible.map((dish) => {
|
{visible.map((dish) => {
|
||||||
const codes = dishCodes(dish);
|
const codes = dishCodes(dish);
|
||||||
return (
|
return (
|
||||||
<li key={dish.id} className="flex items-center gap-3 px-4 py-2">
|
<li key={dish.id} className="flex flex-wrap items-center gap-3 px-4 py-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selected.has(dish.id)}
|
checked={selected.has(dish.id)}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export default function PasskeyManager() {
|
|||||||
|
|
||||||
<ul className="divide-y divide-gray-100">
|
<ul className="divide-y divide-gray-100">
|
||||||
{passkeys.map((pk) => (
|
{passkeys.map((pk) => (
|
||||||
<li key={pk.id} className="flex items-center gap-3 py-1.5 text-sm">
|
<li key={pk.id} className="flex flex-wrap items-center gap-3 py-1.5 text-sm">
|
||||||
<span className="mr-auto">
|
<span className="mr-auto">
|
||||||
<span className="font-semibold">{pk.name || "Passkey"}</span>{" "}
|
<span className="font-semibold">{pk.name || "Passkey"}</span>{" "}
|
||||||
<span className="text-gray-500">
|
<span className="text-gray-500">
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export default function SectionManager() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl">
|
<div className="max-w-3xl">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||||
<h1 className="text-xl font-bold text-[var(--c-primary)] mr-auto">
|
<h1 className="text-xl font-bold text-[var(--c-primary)] mr-auto">
|
||||||
{dict.sectionManager.title}
|
{dict.sectionManager.title}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -232,7 +232,7 @@ export default function SectionManager() {
|
|||||||
) : (
|
) : (
|
||||||
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-md bg-white">
|
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-md bg-white">
|
||||||
{sections.map((section, idx) => (
|
{sections.map((section, idx) => (
|
||||||
<li key={section.key} className="flex items-center gap-3 px-4 py-2">
|
<li key={section.key} className="flex flex-wrap items-center gap-3 px-4 py-2">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function UserManager({ meId }: { meId: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl">
|
<div className="max-w-3xl">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||||
<h1 className="text-xl font-bold text-[var(--c-primary)] mr-auto">
|
<h1 className="text-xl font-bold text-[var(--c-primary)] mr-auto">
|
||||||
{dict.userManager.title}
|
{dict.userManager.title}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -221,7 +221,7 @@ export default function UserManager({ meId }: { meId: string }) {
|
|||||||
|
|
||||||
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-md bg-white">
|
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-md bg-white">
|
||||||
{profiles.map((p) => (
|
{profiles.map((p) => (
|
||||||
<li key={p.id} className="flex items-center gap-3 px-4 py-2 text-sm">
|
<li key={p.id} className="flex flex-wrap items-center gap-3 px-4 py-2 text-sm">
|
||||||
<span className="mr-auto">
|
<span className="mr-auto">
|
||||||
<span className="font-semibold">{p.full_name || p.email}</span>{" "}
|
<span className="font-semibold">{p.full_name || p.email}</span>{" "}
|
||||||
<span className="text-gray-500">
|
<span className="text-gray-500">
|
||||||
|
|||||||
+117
-64
@@ -10,6 +10,7 @@ import {
|
|||||||
formatPrice,
|
formatPrice,
|
||||||
mondayOf,
|
mondayOf,
|
||||||
todayISO,
|
todayISO,
|
||||||
|
weekdayIndex,
|
||||||
} from "@/lib/menu";
|
} from "@/lib/menu";
|
||||||
import { formatMessage } from "@/lib/i18n/shared";
|
import { formatMessage } from "@/lib/i18n/shared";
|
||||||
import { localized } from "@/lib/types";
|
import { localized } from "@/lib/types";
|
||||||
@@ -22,8 +23,11 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
|||||||
const friday = addDays(monday, 4);
|
const friday = addDays(monday, 4);
|
||||||
const days = dict.weekdays.long.map((weekday, i) => ({
|
const days = dict.weekdays.long.map((weekday, i) => ({
|
||||||
weekday,
|
weekday,
|
||||||
|
short: dict.weekdays.short[i],
|
||||||
iso: addDays(monday, i),
|
iso: addDays(monday, i),
|
||||||
}));
|
}));
|
||||||
|
const isCurrentWeek = monday === mondayOf(todayISO());
|
||||||
|
const todayIdx = isCurrentWeek ? weekdayIndex(todayISO()) : -1;
|
||||||
|
|
||||||
const [sections, setSections] = useState<Section[]>([]);
|
const [sections, setSections] = useState<Section[]>([]);
|
||||||
const [dishes, setDishes] = useState<Dish[]>([]);
|
const [dishes, setDishes] = useState<Dish[]>([]);
|
||||||
@@ -36,6 +40,8 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
|||||||
);
|
);
|
||||||
const [selectedDish, setSelectedDish] = useState("");
|
const [selectedDish, setSelectedDish] = useState("");
|
||||||
const [dishSearch, setDishSearch] = useState("");
|
const [dishSearch, setDishSearch] = useState("");
|
||||||
|
// Mobil: ein Tag auf einmal statt aller fünf gleichzeitig
|
||||||
|
const [selectedDayIdx, setSelectedDayIdx] = useState(todayIdx >= 0 ? todayIdx : 0);
|
||||||
|
|
||||||
function startAdding(iso: string, section: string) {
|
function startAdding(iso: string, section: string) {
|
||||||
setAdding({ iso, section });
|
setAdding({ iso, section });
|
||||||
@@ -108,66 +114,20 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
|||||||
? dishes.filter((d) => localized(d, locale).toLowerCase().includes(query))
|
? dishes.filter((d) => localized(d, locale).toLowerCase().includes(query))
|
||||||
: dishes;
|
: dishes;
|
||||||
|
|
||||||
|
// Inhalt einer Tag/Essenszeit-Zelle: Eintragsliste + "hinzufügen"-Flow.
|
||||||
|
// Identisches Fragment für Desktop-Tabelle und Mobile-Karten, damit der
|
||||||
|
// Bearbeitungs-Zustand (adding/selectedDish/dishSearch) nicht dupliziert
|
||||||
|
// werden muss – nur eine Zelle kann gleichzeitig bearbeitet werden,
|
||||||
|
// unabhängig davon, welche Ansicht gerade rendert.
|
||||||
|
function renderCell(iso: string, sectionKey: string) {
|
||||||
|
const isAdding = adding?.iso === iso && adding?.section === sectionKey;
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<div className="flex flex-wrap items-center gap-3 mb-4">
|
{entriesFor(entries, iso, sectionKey).map((e) => {
|
||||||
<h1 className="text-xl font-bold text-[var(--c-primary)] mr-auto">
|
|
||||||
{dict.weekEditor.title}
|
|
||||||
</h1>
|
|
||||||
<Link href={`/intern?woche=${addDays(monday, -7)}`} className="btn btn-add">
|
|
||||||
{dict.publicMenu.prevWeek}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={`/intern?woche=${mondayOf(todayISO())}`}
|
|
||||||
className="btn btn-primary"
|
|
||||||
>
|
|
||||||
{dict.publicMenu.today}
|
|
||||||
</Link>
|
|
||||||
<Link href={`/intern?woche=${addDays(monday, 7)}`} className="btn btn-add">
|
|
||||||
{dict.publicMenu.nextWeek}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<p className="mb-3 text-sm text-[var(--c-del)]" role="alert">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{loading ? (
|
|
||||||
<p className="text-gray-500">{dict.weekEditor.loading}</p>
|
|
||||||
) : (
|
|
||||||
<table className="menu-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="section-head w-52" aria-hidden />
|
|
||||||
{days.map((d) => (
|
|
||||||
<th key={d.iso} className="day-head">
|
|
||||||
<span className="font-bold">{d.weekday}</span>
|
|
||||||
<br />
|
|
||||||
{formatDateLong(d.iso, dict.dateLocale)}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{sections.map((s) => (
|
|
||||||
<tr key={s.key}>
|
|
||||||
<th className="section-head">
|
|
||||||
<div>{localized(s, locale)}</div>
|
|
||||||
<div className="time">{s.time_range}</div>
|
|
||||||
</th>
|
|
||||||
{days.map((d) => {
|
|
||||||
const isAdding =
|
|
||||||
adding?.iso === d.iso && adding?.section === s.key;
|
|
||||||
return (
|
|
||||||
<td key={d.iso} className="cell">
|
|
||||||
{entriesFor(entries, d.iso, s.key).map((e) => {
|
|
||||||
const codes = dishCodes(e.dish);
|
const codes = dishCodes(e.dish);
|
||||||
return (
|
return (
|
||||||
<div key={e.id} className="dish">
|
<div key={e.id} className="dish">
|
||||||
<span className="font-bold">
|
<span className="font-bold">{localized(e.dish, locale)}</span>
|
||||||
{localized(e.dish, locale)}
|
|
||||||
</span>
|
|
||||||
{codes && <sup className="dish-codes">{codes}</sup>}
|
{codes && <sup className="dish-codes">{codes}</sup>}
|
||||||
{" – "}
|
{" – "}
|
||||||
{formatPrice(e.dish.price)}
|
{formatPrice(e.dish.price)}
|
||||||
@@ -245,11 +205,7 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
|||||||
>
|
>
|
||||||
{dict.common.add}
|
{dict.common.add}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button" className="btn btn-neutral" onClick={cancelAdding}>
|
||||||
type="button"
|
|
||||||
className="btn btn-neutral"
|
|
||||||
onClick={cancelAdding}
|
|
||||||
>
|
|
||||||
{dict.common.cancel}
|
{dict.common.cancel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,18 +214,115 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-add mt-1"
|
className="btn btn-add mt-1"
|
||||||
onClick={() => startAdding(d.iso, s.key)}
|
onClick={() => startAdding(iso, sectionKey)}
|
||||||
>
|
>
|
||||||
{dict.weekEditor.addMeal}
|
{dict.weekEditor.addMeal}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</td>
|
</>
|
||||||
);
|
);
|
||||||
})}
|
}
|
||||||
|
|
||||||
|
const selectedDay = days[selectedDayIdx];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||||
|
<h1 className="text-xl font-bold text-[var(--c-primary)] mr-auto">
|
||||||
|
{dict.weekEditor.title}
|
||||||
|
</h1>
|
||||||
|
<Link href={`/intern?woche=${addDays(monday, -7)}`} className="btn btn-add">
|
||||||
|
{dict.publicMenu.prevWeek}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={`/intern?woche=${mondayOf(todayISO())}`}
|
||||||
|
className="btn btn-primary"
|
||||||
|
>
|
||||||
|
{dict.publicMenu.today}
|
||||||
|
</Link>
|
||||||
|
<Link href={`/intern?woche=${addDays(monday, 7)}`} className="btn btn-add">
|
||||||
|
{dict.publicMenu.nextWeek}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="mb-3 text-sm text-[var(--c-del)]" role="alert">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-gray-500">{dict.weekEditor.loading}</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Mobil (< "sm"): ein Tag auf einmal, per Tabs gewählt */}
|
||||||
|
<div className="sm:hidden">
|
||||||
|
<div className="flex gap-1 mb-3">
|
||||||
|
{days.map((d, i) => (
|
||||||
|
<button
|
||||||
|
key={d.iso}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedDayIdx(i)}
|
||||||
|
className={`flex-1 rounded-md py-1.5 text-sm font-semibold border transition-colors ${
|
||||||
|
i === selectedDayIdx
|
||||||
|
? "bg-[var(--c-primary)] text-white border-[var(--c-primary)]"
|
||||||
|
: "bg-white text-gray-600 border-gray-200"
|
||||||
|
} ${i === todayIdx ? "ring-2 ring-[var(--c-accent)] ring-offset-1" : ""}`}
|
||||||
|
>
|
||||||
|
{d.short}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<h2 className="font-bold text-[var(--c-primary)] mb-2">
|
||||||
|
{selectedDay.weekday} · {formatDateLong(selectedDay.iso, dict.dateLocale)}
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{sections.map((s) => (
|
||||||
|
<div key={s.key} className="rounded-md border border-gray-200 overflow-hidden">
|
||||||
|
<div className="bg-[var(--c-primary)] text-[var(--c-header-text)] px-3 py-1.5 text-sm">
|
||||||
|
<span className="font-semibold">{localized(s, locale)}</span>{" "}
|
||||||
|
<span className="opacity-80">· {s.time_range}</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[var(--c-row-bg)] px-3 py-2 text-sm text-center">
|
||||||
|
{renderCell(selectedDay.iso, s.key)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ab "sm": klassische Wochentabelle */}
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<table className="menu-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="section-head w-52" aria-hidden />
|
||||||
|
{days.map((d) => (
|
||||||
|
<th key={d.iso} className="day-head">
|
||||||
|
<span className="font-bold">{d.weekday}</span>
|
||||||
|
<br />
|
||||||
|
{formatDateLong(d.iso, dict.dateLocale)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sections.map((s) => (
|
||||||
|
<tr key={s.key}>
|
||||||
|
<th className="section-head">
|
||||||
|
<div>{localized(s, locale)}</div>
|
||||||
|
<div className="time">{s.time_range}</div>
|
||||||
|
</th>
|
||||||
|
{days.map((d) => (
|
||||||
|
<td key={d.iso} className="cell">
|
||||||
|
{renderCell(d.iso, s.key)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<p className="mt-4 text-sm text-gray-500">
|
<p className="mt-4 text-sm text-gray-500">
|
||||||
{hintBefore}
|
{hintBefore}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const de = {
|
|||||||
openSuggestions: "{count} offene Rezeptvorschläge",
|
openSuggestions: "{count} offene Rezeptvorschläge",
|
||||||
viewMenu: "Speisekarte ansehen",
|
viewMenu: "Speisekarte ansehen",
|
||||||
myAccount: "Mein Konto",
|
myAccount: "Mein Konto",
|
||||||
|
menuToggle: "Menü öffnen/schließen",
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
title: "Anmeldung",
|
title: "Anmeldung",
|
||||||
@@ -333,6 +334,7 @@ const en: Dict = {
|
|||||||
openSuggestions: "{count} open dish suggestions",
|
openSuggestions: "{count} open dish suggestions",
|
||||||
viewMenu: "View menu",
|
viewMenu: "View menu",
|
||||||
myAccount: "My account",
|
myAccount: "My account",
|
||||||
|
menuToggle: "Open/close menu",
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
title: "Sign in",
|
title: "Sign in",
|
||||||
@@ -623,6 +625,7 @@ const pl: Dict = {
|
|||||||
openSuggestions: "{count} otwartych propozycji dań",
|
openSuggestions: "{count} otwartych propozycji dań",
|
||||||
viewMenu: "Zobacz jadłospis",
|
viewMenu: "Zobacz jadłospis",
|
||||||
myAccount: "Moje konto",
|
myAccount: "Moje konto",
|
||||||
|
menuToggle: "Otwórz/zamknij menu",
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
title: "Logowanie",
|
title: "Logowanie",
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "canteen-menu",
|
"name": "canteen-menu",
|
||||||
"version": "1.8.1",
|
"version": "1.9.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "canteen-menu",
|
"name": "canteen-menu",
|
||||||
"version": "1.8.1",
|
"version": "1.9.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^13.3.0",
|
"@simplewebauthn/browser": "^13.3.0",
|
||||||
"@simplewebauthn/server": "^13.3.2",
|
"@simplewebauthn/server": "^13.3.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "canteen-menu",
|
"name": "canteen-menu",
|
||||||
"version": "1.9.0",
|
"version": "1.9.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predev": "node --env-file-if-exists=.env.local db/migrate.mjs && node --env-file-if-exists=.env.local scripts/bootstrap-admin.mjs",
|
"predev": "node --env-file-if-exists=.env.local db/migrate.mjs && node --env-file-if-exists=.env.local scripts/bootstrap-admin.mjs",
|
||||||
|
|||||||
Reference in New Issue
Block a user