Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
660490f1ac | ||
|
|
0c69a21a85 |
@@ -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
|
||||
|
||||
+114
-24
@@ -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) => (
|
||||
<Link key={l.href} href={l.href} className={`relative ${linkClass(l.href)}`}>
|
||||
{l.label}
|
||||
{l.href === "/intern/vorschlaege" && suggestionCount > 0 && (
|
||||
<span
|
||||
className="absolute -top-1.5 -right-1.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-[var(--c-del)] px-1 text-[10px] font-bold leading-none text-white"
|
||||
aria-label={formatMessage(dict.header.openSuggestions, {
|
||||
count: suggestionCount,
|
||||
})}
|
||||
>
|
||||
{suggestionCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="bg-[var(--c-primary-dark)] text-white">
|
||||
<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">
|
||||
{/* 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
|
||||
@@ -60,21 +168,7 @@ export default function AppHeader({
|
||||
<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)}`}>
|
||||
{l.label}
|
||||
{l.href === "/intern/vorschlaege" && suggestionCount > 0 && (
|
||||
<span
|
||||
className="absolute -top-1.5 -right-1.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-[var(--c-del)] px-1 text-[10px] font-bold leading-none text-white"
|
||||
aria-label={formatMessage(dict.header.openSuggestions, {
|
||||
count: suggestionCount,
|
||||
})}
|
||||
>
|
||||
{suggestionCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
{renderLinks(menuLinks)}
|
||||
</nav>
|
||||
|
||||
{role === "admin" && (
|
||||
@@ -85,11 +179,7 @@ export default function AppHeader({
|
||||
<span className="mr-1 text-xs uppercase tracking-wider text-white/50">
|
||||
{dict.header.adminGroup}
|
||||
</span>
|
||||
{adminLinks.map((l) => (
|
||||
<Link key={l.href} href={l.href} className={linkClass(l.href)}>
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
{renderLinks(adminLinks)}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ export default function CodeListManager({
|
||||
|
||||
return (
|
||||
<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">
|
||||
{title}
|
||||
</h2>
|
||||
@@ -291,7 +291,7 @@ export default function CodeListManager({
|
||||
|
||||
<ul className="divide-y divide-gray-100">
|
||||
{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="font-semibold">{item.code}</span> –{" "}
|
||||
{localized(item, locale)}
|
||||
|
||||
@@ -284,7 +284,7 @@ export default function DishManager({
|
||||
|
||||
return (
|
||||
<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">
|
||||
{dict.dishManager.title}
|
||||
</h1>
|
||||
@@ -444,7 +444,7 @@ export default function DishManager({
|
||||
/>
|
||||
)}
|
||||
{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">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -474,7 +474,7 @@ export default function DishManager({
|
||||
{visible.map((dish) => {
|
||||
const codes = dishCodes(dish);
|
||||
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
|
||||
type="checkbox"
|
||||
checked={selected.has(dish.id)}
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function PasskeyManager() {
|
||||
|
||||
<ul className="divide-y divide-gray-100">
|
||||
{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="font-semibold">{pk.name || "Passkey"}</span>{" "}
|
||||
<span className="text-gray-500">
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function SectionManager() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
{dict.sectionManager.title}
|
||||
</h1>
|
||||
@@ -232,7 +232,7 @@ export default function SectionManager() {
|
||||
) : (
|
||||
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-md bg-white">
|
||||
{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">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function UserManager({ meId }: { meId: string }) {
|
||||
|
||||
return (
|
||||
<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">
|
||||
{dict.userManager.title}
|
||||
</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">
|
||||
{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="font-semibold">{p.full_name || p.email}</span>{" "}
|
||||
<span className="text-gray-500">
|
||||
|
||||
+186
-133
@@ -10,6 +10,7 @@ import {
|
||||
formatPrice,
|
||||
mondayOf,
|
||||
todayISO,
|
||||
weekdayIndex,
|
||||
} from "@/lib/menu";
|
||||
import { formatMessage } from "@/lib/i18n/shared";
|
||||
import { localized } from "@/lib/types";
|
||||
@@ -22,8 +23,11 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
||||
const friday = addDays(monday, 4);
|
||||
const days = dict.weekdays.long.map((weekday, i) => ({
|
||||
weekday,
|
||||
short: dict.weekdays.short[i],
|
||||
iso: addDays(monday, i),
|
||||
}));
|
||||
const isCurrentWeek = monday === mondayOf(todayISO());
|
||||
const todayIdx = isCurrentWeek ? weekdayIndex(todayISO()) : -1;
|
||||
|
||||
const [sections, setSections] = useState<Section[]>([]);
|
||||
const [dishes, setDishes] = useState<Dish[]>([]);
|
||||
@@ -36,6 +40,8 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
||||
);
|
||||
const [selectedDish, setSelectedDish] = 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) {
|
||||
setAdding({ iso, section });
|
||||
@@ -108,6 +114,117 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
||||
? dishes.filter((d) => localized(d, locale).toLowerCase().includes(query))
|
||||
: 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 (
|
||||
<>
|
||||
{entriesFor(entries, iso, sectionKey).map((e) => {
|
||||
const codes = dishCodes(e.dish);
|
||||
return (
|
||||
<div key={e.id} className="dish">
|
||||
<span className="font-bold">{localized(e.dish, locale)}</span>
|
||||
{codes && <sup className="dish-codes">{codes}</sup>}
|
||||
{" – "}
|
||||
{formatPrice(e.dish.price)}
|
||||
<div className="mt-1">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-del"
|
||||
onClick={() => removeEntry(e.id)}
|
||||
>
|
||||
{dict.common.delete}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{isAdding ? (
|
||||
<div className="mt-2 space-y-1 text-left">
|
||||
{selectedDishObj ? (
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<span className="mr-auto">
|
||||
{localized(selectedDishObj, locale)} (
|
||||
{formatPrice(selectedDishObj.price)})
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="text-gray-400 hover:text-[var(--c-primary)]"
|
||||
onClick={() => {
|
||||
setSelectedDish("");
|
||||
setDishSearch("");
|
||||
}}
|
||||
aria-label={dict.weekEditor.changeDish}
|
||||
title={dict.weekEditor.changeDish}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
type="search"
|
||||
className="input"
|
||||
autoFocus
|
||||
placeholder={dict.dishManager.searchPlaceholder}
|
||||
value={dishSearch}
|
||||
onChange={(e) => setDishSearch(e.target.value)}
|
||||
/>
|
||||
<ul className="max-h-32 overflow-y-auto border border-gray-200 rounded-md bg-white divide-y divide-gray-100">
|
||||
{filteredDishes.map((dish) => (
|
||||
<li key={dish.id}>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full text-left px-2 py-1 text-sm hover:bg-[var(--c-row-bg-alt)]"
|
||||
onClick={() => setSelectedDish(dish.id)}
|
||||
>
|
||||
{localized(dish, locale)} ({formatPrice(dish.price)})
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{filteredDishes.length === 0 && (
|
||||
<li className="px-2 py-1 text-sm text-gray-500">
|
||||
{formatMessage(dict.dishManager.noResults, {
|
||||
query: dishSearch,
|
||||
})}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
<div className="flex gap-1 justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-add"
|
||||
onClick={addEntry}
|
||||
disabled={!selectedDish}
|
||||
>
|
||||
{dict.common.add}
|
||||
</button>
|
||||
<button type="button" className="btn btn-neutral" onClick={cancelAdding}>
|
||||
{dict.common.cancel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-add mt-1"
|
||||
onClick={() => startAdding(iso, sectionKey)}
|
||||
>
|
||||
{dict.weekEditor.addMeal}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedDay = days[selectedDayIdx];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||
@@ -136,140 +253,76 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
||||
{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>
|
||||
<>
|
||||
{/* 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>
|
||||
))}
|
||||
</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);
|
||||
return (
|
||||
<div key={e.id} className="dish">
|
||||
<span className="font-bold">
|
||||
{localized(e.dish, locale)}
|
||||
</span>
|
||||
{codes && <sup className="dish-codes">{codes}</sup>}
|
||||
{" – "}
|
||||
{formatPrice(e.dish.price)}
|
||||
<div className="mt-1">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-del"
|
||||
onClick={() => removeEntry(e.id)}
|
||||
>
|
||||
{dict.common.delete}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{isAdding ? (
|
||||
<div className="mt-2 space-y-1 text-left">
|
||||
{selectedDishObj ? (
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<span className="mr-auto">
|
||||
{localized(selectedDishObj, locale)} (
|
||||
{formatPrice(selectedDishObj.price)})
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="text-gray-400 hover:text-[var(--c-primary)]"
|
||||
onClick={() => {
|
||||
setSelectedDish("");
|
||||
setDishSearch("");
|
||||
}}
|
||||
aria-label={dict.weekEditor.changeDish}
|
||||
title={dict.weekEditor.changeDish}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
type="search"
|
||||
className="input"
|
||||
autoFocus
|
||||
placeholder={dict.dishManager.searchPlaceholder}
|
||||
value={dishSearch}
|
||||
onChange={(e) => setDishSearch(e.target.value)}
|
||||
/>
|
||||
<ul className="max-h-32 overflow-y-auto border border-gray-200 rounded-md bg-white divide-y divide-gray-100">
|
||||
{filteredDishes.map((dish) => (
|
||||
<li key={dish.id}>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full text-left px-2 py-1 text-sm hover:bg-[var(--c-row-bg-alt)]"
|
||||
onClick={() => setSelectedDish(dish.id)}
|
||||
>
|
||||
{localized(dish, locale)} ({formatPrice(dish.price)})
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{filteredDishes.length === 0 && (
|
||||
<li className="px-2 py-1 text-sm text-gray-500">
|
||||
{formatMessage(dict.dishManager.noResults, {
|
||||
query: dishSearch,
|
||||
})}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
<div className="flex gap-1 justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-add"
|
||||
onClick={addEntry}
|
||||
disabled={!selectedDish}
|
||||
>
|
||||
{dict.common.add}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-neutral"
|
||||
onClick={cancelAdding}
|
||||
>
|
||||
{dict.common.cancel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-add mt-1"
|
||||
onClick={() => startAdding(d.iso, s.key)}
|
||||
>
|
||||
{dict.weekEditor.addMeal}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<p className="mt-4 text-sm text-gray-500">
|
||||
{hintBefore}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user