Skip to content

Commit f9a4a46

Browse files
committed
Admin edit UX: split overlay for dishes, side panel for categories
Dishes: bottom-sheet modal becomes a split overlay. Form on the left, live preview (collapsed list card + expanded detail) on the right. Mobile (<768px) hides the preview and keeps the bottom-sheet form. Categories: centered modal becomes a right-anchored side panel that stays inline with the list. Mobile drops to a bottom sheet. No preview pane (no rich content to preview). The MenuItemPreview component is admin-only and re-implements the public look — admin and public are not coupled. Both must be kept in visual sync by hand if the public design changes.
1 parent b8b0832 commit f9a4a46

12 files changed

Lines changed: 259 additions & 15 deletions

File tree

web/messages/de.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@
420420
"settings.publishing.publishMenu": "Publish menu",
421421
"settings.publishing.working": "Working...",
422422
"settings.saveAll": "Save changes",
423-
"settings.savingAll": "Saving..."
423+
"settings.savingAll": "Saving...",
424+
"entries.preview.cardTitle": "Vorschau — Listenansicht",
425+
"entries.preview.expandedTitle": "Vorschau — Detailansicht"
424426
}
425427
}

web/messages/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@
420420
"settings.publishing.publishMenu": "Publish menu",
421421
"settings.publishing.working": "Working...",
422422
"settings.saveAll": "Save changes",
423-
"settings.savingAll": "Saving..."
423+
"settings.savingAll": "Saving...",
424+
"entries.preview.cardTitle": "Preview — list view",
425+
"entries.preview.expandedTitle": "Preview — detail view"
424426
}
425427
}

web/messages/es.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@
420420
"settings.publishing.publishMenu": "Publish menu",
421421
"settings.publishing.working": "Working...",
422422
"settings.saveAll": "Save changes",
423-
"settings.savingAll": "Saving..."
423+
"settings.savingAll": "Saving...",
424+
"entries.preview.cardTitle": "Vista previa — lista",
425+
"entries.preview.expandedTitle": "Vista previa — detalle"
424426
}
425427
}

web/messages/fr.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@
420420
"settings.publishing.publishMenu": "Publish menu",
421421
"settings.publishing.working": "Working...",
422422
"settings.saveAll": "Save changes",
423-
"settings.savingAll": "Saving..."
423+
"settings.savingAll": "Saving...",
424+
"entries.preview.cardTitle": "Aperçu — vue liste",
425+
"entries.preview.expandedTitle": "Aperçu — vue détail"
424426
}
425427
}

web/messages/it.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@
420420
"settings.publishing.publishMenu": "Pubblica menu",
421421
"settings.publishing.working": "In corso...",
422422
"settings.saveAll": "Salva Modifiche",
423-
"settings.savingAll": "Salvataggio..."
423+
"settings.savingAll": "Salvataggio...",
424+
"entries.preview.cardTitle": "Anteprima — vista lista",
425+
"entries.preview.expandedTitle": "Anteprima — vista dettaglio"
424426
}
425427
}

web/messages/nl.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@
420420
"settings.publishing.publishMenu": "Publish menu",
421421
"settings.publishing.working": "Working...",
422422
"settings.saveAll": "Save changes",
423-
"settings.savingAll": "Saving..."
423+
"settings.savingAll": "Saving...",
424+
"entries.preview.cardTitle": "Voorbeeld — lijstweergave",
425+
"entries.preview.expandedTitle": "Voorbeeld — detailweergave"
424426
}
425427
}

web/messages/pt.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@
420420
"settings.publishing.publishMenu": "Publish menu",
421421
"settings.publishing.working": "Working...",
422422
"settings.saveAll": "Save changes",
423-
"settings.savingAll": "Saving..."
423+
"settings.savingAll": "Saving...",
424+
"entries.preview.cardTitle": "Pré-visualização — lista",
425+
"entries.preview.expandedTitle": "Pré-visualização — detalhe"
424426
}
425427
}

web/messages/ru.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@
420420
"settings.publishing.publishMenu": "Publish menu",
421421
"settings.publishing.working": "Working...",
422422
"settings.saveAll": "Save changes",
423-
"settings.savingAll": "Saving..."
423+
"settings.savingAll": "Saving...",
424+
"entries.preview.cardTitle": "Превью — в списке",
425+
"entries.preview.expandedTitle": "Превью — детально"
424426
}
425427
}

web/messages/vec.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@
420420
"settings.publishing.publishMenu": "Publish menu",
421421
"settings.publishing.working": "Working...",
422422
"settings.saveAll": "Save changes",
423-
"settings.savingAll": "Saving..."
423+
"settings.savingAll": "Saving...",
424+
"entries.preview.cardTitle": "Anteprima — vista lista",
425+
"entries.preview.expandedTitle": "Anteprima — vista detajo"
424426
}
425427
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"use client";
2+
3+
import Image from "next/image";
4+
5+
/**
6+
* Decoupled visual preview of a menu item.
7+
*
8+
* Mirrors the look of the public menu's MenuItem + MenuItemDetail components
9+
* but does NOT import them — so the admin preview never breaks the public
10+
* site, and the public site never breaks the admin. If the public visuals
11+
* change, this file should be updated by hand to stay in sync.
12+
*
13+
* Takes a plain data object: pre-resolved primary-locale name/description,
14+
* no store access, no router, no i18n hook.
15+
*/
16+
17+
interface PreviewItemData {
18+
name: string;
19+
description?: string;
20+
price: number;
21+
priceUnit?: string;
22+
image?: string | null;
23+
allergens?: string[];
24+
outOfStock?: boolean;
25+
frozen?: boolean;
26+
}
27+
28+
const ALLERGEN_SHORT: Record<string, string> = {
29+
"Anidride-Solforosa-e-Solfiti": "SO2",
30+
Arachidi: "AR",
31+
Crostacei: "CR",
32+
"Frutta-a-Guscio": "FG",
33+
Glutine: "GL",
34+
"Latte-e-Derivati": "LA",
35+
Lupini: "LU",
36+
Molluschi: "MO",
37+
Pesce: "PE",
38+
Sedano: "SE",
39+
Senape: "SN",
40+
Sesamo: "SS",
41+
Soia: "SO",
42+
Uova: "UO",
43+
};
44+
45+
function formatPrice(price: number, unit?: string): string {
46+
const formatted = `€ ${price.toFixed(2).replace(".", ",")}`;
47+
return unit ? `${formatted}/${unit}` : formatted;
48+
}
49+
50+
function sanitizeRichText(html: string): string {
51+
if (!html) return "";
52+
let safe = html
53+
.replace(/&/g, "&amp;")
54+
.replace(/</g, "&lt;")
55+
.replace(/>/g, "&gt;");
56+
safe = safe
57+
.replace(/&lt;(\/?)b&gt;/gi, "<$1b>")
58+
.replace(/&lt;(\/?)i&gt;/gi, "<$1i>")
59+
.replace(/&lt;(\/?)u&gt;/gi, "<$1u>");
60+
return safe;
61+
}
62+
63+
/** The list-card collapsed view, as seen in the menu list. */
64+
export function MenuItemCardPreview({ item }: { item: PreviewItemData }) {
65+
const hasDescription = !!item.description?.trim();
66+
return (
67+
<article
68+
className={`relative border-b-2 border-gray-100 bg-white ${
69+
item.outOfStock ? "opacity-60" : ""
70+
}`}
71+
>
72+
<div className="px-4 py-4">
73+
<div className="flex gap-3">
74+
<div className="flex-1 min-w-0">
75+
<h3
76+
className={`text-base font-medium text-gray-900 ${
77+
item.outOfStock ? "line-through" : ""
78+
}`}
79+
>
80+
{item.name || <span className="text-gray-400 italic">Untitled</span>}
81+
</h3>
82+
{hasDescription && (
83+
<p className="mt-1 line-clamp-2 text-sm text-gray-600">
84+
{item.description}
85+
</p>
86+
)}
87+
{item.allergens && item.allergens.length > 0 && (
88+
<div className="mt-2 flex flex-wrap gap-1">
89+
{item.allergens.map((allergen) => (
90+
<span
91+
key={allergen}
92+
className="inline-flex h-5 items-center rounded bg-amber-50 px-1.5 text-[10px] font-medium text-amber-700"
93+
title={allergen.replace(/-/g, " ")}
94+
>
95+
{ALLERGEN_SHORT[allergen] || allergen.slice(0, 2).toUpperCase()}
96+
</span>
97+
))}
98+
</div>
99+
)}
100+
<p className="mt-2 font-medium tracking-wide text-gray-700">
101+
{formatPrice(item.price, item.priceUnit)}
102+
</p>
103+
{item.outOfStock && (
104+
<span className="mt-2 inline-block rounded bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">
105+
Out of Stock
106+
</span>
107+
)}
108+
{item.frozen && (
109+
<span className="mt-1 inline-block text-xs text-gray-400">
110+
* Contains frozen ingredients
111+
</span>
112+
)}
113+
</div>
114+
{item.image && (
115+
<div className="flex-shrink-0">
116+
<div className="relative h-16 w-16 overflow-hidden rounded-full bg-gray-200">
117+
<Image
118+
src={item.image}
119+
alt={item.name}
120+
fill
121+
className="object-cover"
122+
sizes="64px"
123+
unoptimized
124+
/>
125+
</div>
126+
</div>
127+
)}
128+
</div>
129+
</div>
130+
</article>
131+
);
132+
}
133+
134+
/** The expanded detail view, as seen when a customer taps a card. */
135+
export function MenuItemExpandedPreview({ item }: { item: PreviewItemData }) {
136+
return (
137+
<div className="bg-white rounded-3xl shadow-lg overflow-hidden">
138+
{item.image && (
139+
<div className="relative w-full aspect-[4/3] bg-gray-200">
140+
<Image
141+
src={item.image}
142+
alt={item.name}
143+
fill
144+
className="object-cover"
145+
unoptimized
146+
/>
147+
</div>
148+
)}
149+
<div className="p-6">
150+
<div className={item.image ? "flex justify-between items-start gap-4 mb-3" : "mb-3"}>
151+
<h2 className="text-2xl font-bold text-gray-800 flex-1">
152+
{item.name || <span className="text-gray-400 italic">Untitled</span>}
153+
</h2>
154+
<span className="text-2xl font-bold text-primary whitespace-nowrap">
155+
{formatPrice(item.price, item.priceUnit)}
156+
</span>
157+
</div>
158+
{item.allergens && item.allergens.length > 0 && (
159+
<div className="mb-4 flex flex-wrap gap-1.5">
160+
{item.allergens.map((allergen) => (
161+
<span
162+
key={allergen}
163+
className="inline-flex items-center rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700"
164+
title={allergen.replace(/-/g, " ")}
165+
>
166+
{ALLERGEN_SHORT[allergen] || allergen}
167+
</span>
168+
))}
169+
</div>
170+
)}
171+
{item.description && (
172+
<p
173+
className="text-gray-600 leading-relaxed"
174+
dangerouslySetInnerHTML={{ __html: sanitizeRichText(item.description) }}
175+
/>
176+
)}
177+
{item.frozen && (
178+
<p className="mt-4 text-sm text-gray-500 italic">
179+
* Contains frozen ingredients
180+
</p>
181+
)}
182+
</div>
183+
</div>
184+
);
185+
}

0 commit comments

Comments
 (0)