pizzaz-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +3 -0
- package/README.md +139 -0
- package/build-all.mts +188 -0
- package/docs/DEPLOYMENT_GUIDE.md +226 -0
- package/package.json +41 -0
- package/render.yaml +12 -0
- package/server/server.ts +400 -0
- package/src/index.css +39 -0
- package/src/media-queries.ts +15 -0
- package/src/pizzaz/Inspector.jsx +109 -0
- package/src/pizzaz/Sidebar.jsx +165 -0
- package/src/pizzaz/index.jsx +295 -0
- package/src/pizzaz/map.css +707 -0
- package/src/pizzaz/markers.json +104 -0
- package/src/pizzaz-albums/AlbumCard.jsx +45 -0
- package/src/pizzaz-albums/FilmStrip.jsx +30 -0
- package/src/pizzaz-albums/FullscreenViewer.jsx +43 -0
- package/src/pizzaz-albums/albums.json +112 -0
- package/src/pizzaz-albums/index.jsx +153 -0
- package/src/pizzaz-carousel/PlaceCard.jsx +40 -0
- package/src/pizzaz-carousel/index.jsx +121 -0
- package/src/pizzaz-list/index.jsx +115 -0
- package/src/pizzaz-shop/index.tsx +1482 -0
- package/src/types.ts +103 -0
- package/src/use-display-mode.ts +6 -0
- package/src/use-max-height.ts +5 -0
- package/src/use-openai-global.ts +37 -0
- package/src/use-widget-props.ts +14 -0
- package/src/use-widget-state.ts +46 -0
- package/tailwind.config.ts +7 -0
- package/tsconfig.app.json +24 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +13 -0
- package/vite-env.d.ts +1 -0
- package/vite.config.mts +232 -0
|
@@ -0,0 +1,1482 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
|
|
3
|
+
import { Minus, Plus, ShoppingCart } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
type MouseEvent as ReactMouseEvent,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from "react";
|
|
13
|
+
import { createRoot } from "react-dom/client";
|
|
14
|
+
import { BrowserRouter, useLocation, useNavigate } from "react-router-dom";
|
|
15
|
+
import { useDisplayMode } from "../use-display-mode";
|
|
16
|
+
import { useMaxHeight } from "../use-max-height";
|
|
17
|
+
import { useOpenAiGlobal } from "../use-openai-global";
|
|
18
|
+
import { useWidgetProps } from "../use-widget-props";
|
|
19
|
+
import { useWidgetState } from "../use-widget-state";
|
|
20
|
+
|
|
21
|
+
import { Button } from "@openai/apps-sdk-ui/components/Button";
|
|
22
|
+
import { Image } from "@openai/apps-sdk-ui/components/Image";
|
|
23
|
+
|
|
24
|
+
type NutritionFact = {
|
|
25
|
+
label: string;
|
|
26
|
+
value: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type CartItem = {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
price: number;
|
|
33
|
+
description: string;
|
|
34
|
+
shortDescription?: string;
|
|
35
|
+
detailSummary?: string;
|
|
36
|
+
nutritionFacts?: NutritionFact[];
|
|
37
|
+
highlights?: string[];
|
|
38
|
+
tags?: string[];
|
|
39
|
+
quantity: number;
|
|
40
|
+
image: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type PizzazCartWidgetState = {
|
|
44
|
+
state?: "checkout" | null;
|
|
45
|
+
cartItems?: CartItem[];
|
|
46
|
+
selectedCartItemId?: string | null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type PizzazCartWidgetProps = {
|
|
50
|
+
cartItems?: CartItem[];
|
|
51
|
+
widgetState?: Partial<PizzazCartWidgetState> | null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const SERVICE_FEE = 3;
|
|
55
|
+
const DELIVERY_FEE = 2.99;
|
|
56
|
+
const TAX_FEE = 3.4;
|
|
57
|
+
const CONTINUE_TO_PAYMENT_EVENT = "pizzaz-shop:continue-to-payment";
|
|
58
|
+
|
|
59
|
+
const FILTERS: Array<{
|
|
60
|
+
id: "all" | "vegetarian" | "vegan" | "size" | "spicy";
|
|
61
|
+
label: string;
|
|
62
|
+
tag?: string;
|
|
63
|
+
}> = [
|
|
64
|
+
{ id: "all", label: "All" },
|
|
65
|
+
{ id: "vegetarian", label: "Vegetarian", tag: "vegetarian" },
|
|
66
|
+
{ id: "vegan", label: "Vegan", tag: "vegan" },
|
|
67
|
+
{ id: "size", label: "Size", tag: "size" },
|
|
68
|
+
{ id: "spicy", label: "Spicy", tag: "spicy" },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const INITIAL_CART_ITEMS: CartItem[] = [
|
|
72
|
+
{
|
|
73
|
+
id: "marys-chicken",
|
|
74
|
+
name: "Mary's Chicken",
|
|
75
|
+
price: 19.48,
|
|
76
|
+
description:
|
|
77
|
+
"Tender organic chicken breasts trimmed for easy cooking. Raised without antibiotics and air chilled for exceptional flavor.",
|
|
78
|
+
shortDescription: "Organic chicken breasts",
|
|
79
|
+
detailSummary: "4 lbs • $3.99/lb",
|
|
80
|
+
nutritionFacts: [
|
|
81
|
+
{ label: "Protein", value: "8g" },
|
|
82
|
+
{ label: "Fat", value: "9g" },
|
|
83
|
+
{ label: "Sugar", value: "12g" },
|
|
84
|
+
{ label: "Calories", value: "160" },
|
|
85
|
+
],
|
|
86
|
+
highlights: [
|
|
87
|
+
"No antibiotics or added hormones.",
|
|
88
|
+
"Air chilled and never frozen for peak flavor.",
|
|
89
|
+
"Raised in the USA on a vegetarian diet.",
|
|
90
|
+
],
|
|
91
|
+
quantity: 2,
|
|
92
|
+
image: "https://persistent.oaistatic.com/pizzaz-cart-xl/chicken.png",
|
|
93
|
+
tags: ["size"],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "avocados",
|
|
97
|
+
name: "Avocados",
|
|
98
|
+
price: 1,
|
|
99
|
+
description:
|
|
100
|
+
"Creamy Hass avocados picked at peak ripeness. Ideal for smashing into guacamole or topping tacos.",
|
|
101
|
+
shortDescription: "Creamy Hass avocados",
|
|
102
|
+
detailSummary: "3 ct • $1.00/ea",
|
|
103
|
+
nutritionFacts: [
|
|
104
|
+
{ label: "Fiber", value: "7g" },
|
|
105
|
+
{ label: "Fat", value: "15g" },
|
|
106
|
+
{ label: "Potassium", value: "485mg" },
|
|
107
|
+
{ label: "Calories", value: "160" },
|
|
108
|
+
],
|
|
109
|
+
highlights: [
|
|
110
|
+
"Perfectly ripe and ready for slicing.",
|
|
111
|
+
"Rich in healthy fats and naturally creamy.",
|
|
112
|
+
],
|
|
113
|
+
quantity: 2,
|
|
114
|
+
image: "https://persistent.oaistatic.com/pizzaz-cart-xl/avocado.png",
|
|
115
|
+
tags: ["vegan"],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "hojicha-pizza",
|
|
119
|
+
name: "Hojicha Pizza",
|
|
120
|
+
price: 15.5,
|
|
121
|
+
description:
|
|
122
|
+
"Wood-fired crust layered with smoky hojicha tea sauce and melted mozzarella with a drizzle of honey for an adventurous slice.",
|
|
123
|
+
shortDescription: "Smoky hojicha sauce & honey",
|
|
124
|
+
detailSummary: '12" pie • Serves 2',
|
|
125
|
+
nutritionFacts: [
|
|
126
|
+
{ label: "Protein", value: "14g" },
|
|
127
|
+
{ label: "Fat", value: "18g" },
|
|
128
|
+
{ label: "Sugar", value: "9g" },
|
|
129
|
+
{ label: "Calories", value: "320" },
|
|
130
|
+
],
|
|
131
|
+
highlights: [
|
|
132
|
+
"Smoky roasted hojicha glaze with honey drizzle.",
|
|
133
|
+
"Stone-fired crust with a delicate char.",
|
|
134
|
+
],
|
|
135
|
+
quantity: 2,
|
|
136
|
+
image: "https://persistent.oaistatic.com/pizzaz-cart-xl/hojicha-pizza.png",
|
|
137
|
+
tags: ["vegetarian", "size", "spicy"],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "chicken-pizza",
|
|
141
|
+
name: "Chicken Pizza",
|
|
142
|
+
price: 7,
|
|
143
|
+
description:
|
|
144
|
+
"Classic thin-crust pizza topped with roasted chicken, caramelized onions, and herb pesto.",
|
|
145
|
+
shortDescription: "Roasted chicken & pesto",
|
|
146
|
+
detailSummary: '10" personal • Serves 1',
|
|
147
|
+
nutritionFacts: [
|
|
148
|
+
{ label: "Protein", value: "20g" },
|
|
149
|
+
{ label: "Fat", value: "11g" },
|
|
150
|
+
{ label: "Carbs", value: "36g" },
|
|
151
|
+
{ label: "Calories", value: "290" },
|
|
152
|
+
],
|
|
153
|
+
highlights: [
|
|
154
|
+
"Roasted chicken with caramelized onions.",
|
|
155
|
+
"Fresh basil pesto and mozzarella.",
|
|
156
|
+
],
|
|
157
|
+
quantity: 1,
|
|
158
|
+
image: "https://persistent.oaistatic.com/pizzaz-cart-xl/chicken-pizza.png",
|
|
159
|
+
tags: ["size"],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "matcha-pizza",
|
|
163
|
+
name: "Matcha Pizza",
|
|
164
|
+
price: 5,
|
|
165
|
+
description:
|
|
166
|
+
"Crisp dough spread with velvety matcha cream and mascarpone. Earthy green tea notes balance gentle sweetness.",
|
|
167
|
+
shortDescription: "Velvety matcha cream",
|
|
168
|
+
detailSummary: '8" dessert • Serves 2',
|
|
169
|
+
nutritionFacts: [
|
|
170
|
+
{ label: "Protein", value: "6g" },
|
|
171
|
+
{ label: "Fat", value: "10g" },
|
|
172
|
+
{ label: "Sugar", value: "14g" },
|
|
173
|
+
{ label: "Calories", value: "240" },
|
|
174
|
+
],
|
|
175
|
+
highlights: [
|
|
176
|
+
"Stone-baked crust with delicate crunch.",
|
|
177
|
+
"Matcha mascarpone with white chocolate drizzle.",
|
|
178
|
+
],
|
|
179
|
+
quantity: 1,
|
|
180
|
+
image: "https://persistent.oaistatic.com/pizzaz-cart-xl/matcha-pizza.png",
|
|
181
|
+
tags: ["vegetarian"],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "pesto-pizza",
|
|
185
|
+
name: "Pesto Pizza",
|
|
186
|
+
price: 12.5,
|
|
187
|
+
description:
|
|
188
|
+
"Hand-tossed crust brushed with bright basil pesto, layered with fresh mozzarella, and finished with roasted cherry tomatoes.",
|
|
189
|
+
shortDescription: "Basil pesto & tomatoes",
|
|
190
|
+
detailSummary: '12" pie • Serves 2',
|
|
191
|
+
nutritionFacts: [
|
|
192
|
+
{ label: "Protein", value: "16g" },
|
|
193
|
+
{ label: "Fat", value: "14g" },
|
|
194
|
+
{ label: "Carbs", value: "28g" },
|
|
195
|
+
{ label: "Calories", value: "310" },
|
|
196
|
+
],
|
|
197
|
+
highlights: [
|
|
198
|
+
"House-made pesto with sweet basil and pine nuts.",
|
|
199
|
+
"Roasted cherry tomatoes for a pop of acidity.",
|
|
200
|
+
],
|
|
201
|
+
quantity: 1,
|
|
202
|
+
image: "https://persistent.oaistatic.com/pizzaz-cart-xl/matcha-pizza.png",
|
|
203
|
+
tags: ["vegetarian", "size"],
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
const cloneCartItem = (item: CartItem): CartItem => ({
|
|
208
|
+
...item,
|
|
209
|
+
nutritionFacts: item.nutritionFacts?.map((fact) => ({ ...fact })),
|
|
210
|
+
highlights: item.highlights ? [...item.highlights] : undefined,
|
|
211
|
+
tags: item.tags ? [...item.tags] : undefined,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const createDefaultCartItems = (): CartItem[] =>
|
|
215
|
+
INITIAL_CART_ITEMS.map((item) => cloneCartItem(item));
|
|
216
|
+
|
|
217
|
+
const createDefaultWidgetState = (): PizzazCartWidgetState => ({
|
|
218
|
+
state: null,
|
|
219
|
+
cartItems: createDefaultCartItems(),
|
|
220
|
+
selectedCartItemId: null,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const nutritionFactsEqual = (
|
|
224
|
+
a?: NutritionFact[],
|
|
225
|
+
b?: NutritionFact[]
|
|
226
|
+
): boolean => {
|
|
227
|
+
if (!a?.length && !b?.length) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
if (!a || !b || a.length !== b.length) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
return a.every((fact, index) => {
|
|
234
|
+
const other = b[index];
|
|
235
|
+
if (!other) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
return fact.label === other.label && fact.value === other.value;
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const highlightsEqual = (a?: string[], b?: string[]): boolean => {
|
|
243
|
+
if (!a?.length && !b?.length) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
if (!a || !b || a.length !== b.length) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
return a.every((highlight, index) => highlight === b[index]);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const cartItemsEqual = (a: CartItem[], b: CartItem[]): boolean => {
|
|
253
|
+
if (a.length !== b.length) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
257
|
+
const left = a[i];
|
|
258
|
+
const right = b[i];
|
|
259
|
+
if (!right) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
if (
|
|
263
|
+
left.id !== right.id ||
|
|
264
|
+
left.quantity !== right.quantity ||
|
|
265
|
+
left.name !== right.name ||
|
|
266
|
+
left.price !== right.price ||
|
|
267
|
+
left.description !== right.description ||
|
|
268
|
+
left.shortDescription !== right.shortDescription ||
|
|
269
|
+
left.detailSummary !== right.detailSummary ||
|
|
270
|
+
!nutritionFactsEqual(left.nutritionFacts, right.nutritionFacts) ||
|
|
271
|
+
!highlightsEqual(left.highlights, right.highlights) ||
|
|
272
|
+
!highlightsEqual(left.tags, right.tags) ||
|
|
273
|
+
left.image !== right.image
|
|
274
|
+
) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
type SelectedCartItemPanelProps = {
|
|
282
|
+
item: CartItem;
|
|
283
|
+
onAdjustQuantity: (id: string, delta: number) => void;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
function SelectedCartItemPanel({
|
|
287
|
+
item,
|
|
288
|
+
onAdjustQuantity,
|
|
289
|
+
}: SelectedCartItemPanelProps) {
|
|
290
|
+
const nutritionFacts = Array.isArray(item.nutritionFacts)
|
|
291
|
+
? item.nutritionFacts
|
|
292
|
+
: [];
|
|
293
|
+
const highlights = Array.isArray(item.highlights) ? item.highlights : [];
|
|
294
|
+
|
|
295
|
+
const hasNutritionFacts = nutritionFacts.length > 0;
|
|
296
|
+
const hasHighlights = highlights.length > 0;
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<div className="space-y-4">
|
|
300
|
+
<div className="overflow-hidden rounded-none border-b border-black/5 bg-white">
|
|
301
|
+
<div className="relative flex items-center justify-center overflow-hidden">
|
|
302
|
+
<Image
|
|
303
|
+
src={item.image}
|
|
304
|
+
alt={item.name}
|
|
305
|
+
className="max-h-[320px] w-[80%] object-cover"
|
|
306
|
+
/>
|
|
307
|
+
<div className="absolute inset-0 bg-black/[0.025]" />
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div className="flex flex-col gap-3 px-5 pb-5">
|
|
312
|
+
<div className="flex items-start justify-between gap-4">
|
|
313
|
+
<div className="space-y-0">
|
|
314
|
+
<p className="text-xl font-medium text-black">
|
|
315
|
+
${item.price.toFixed(2)}
|
|
316
|
+
</p>
|
|
317
|
+
<h2 className="text-base text-black">{item.name}</h2>
|
|
318
|
+
</div>
|
|
319
|
+
<div className="flex items-center rounded-full bg-black/[0.04] px-1 py-1 text-black">
|
|
320
|
+
<Button
|
|
321
|
+
type="button"
|
|
322
|
+
variant="ghost"
|
|
323
|
+
color="secondary"
|
|
324
|
+
size="xs"
|
|
325
|
+
uniform
|
|
326
|
+
aria-label={`Decrease quantity of ${item.name}`}
|
|
327
|
+
onClick={() => onAdjustQuantity(item.id, -1)}
|
|
328
|
+
>
|
|
329
|
+
<Minus
|
|
330
|
+
strokeWidth={2}
|
|
331
|
+
className="h-3.5 w-3.5"
|
|
332
|
+
aria-hidden="true"
|
|
333
|
+
/>
|
|
334
|
+
</Button>
|
|
335
|
+
<span className="mx-2 min-w-[10px] text-center text-base font-medium">
|
|
336
|
+
{item.quantity}
|
|
337
|
+
</span>
|
|
338
|
+
<Button
|
|
339
|
+
type="button"
|
|
340
|
+
variant="ghost"
|
|
341
|
+
color="secondary"
|
|
342
|
+
size="xs"
|
|
343
|
+
uniform
|
|
344
|
+
aria-label={`Increase quantity of ${item.name}`}
|
|
345
|
+
onClick={() => onAdjustQuantity(item.id, 1)}
|
|
346
|
+
>
|
|
347
|
+
<Plus
|
|
348
|
+
strokeWidth={2}
|
|
349
|
+
className="h-3.5 w-3.5"
|
|
350
|
+
aria-hidden="true"
|
|
351
|
+
/>
|
|
352
|
+
</Button>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<p className="text-sm text-black/60">{item.description}</p>
|
|
357
|
+
|
|
358
|
+
{item.detailSummary ? (
|
|
359
|
+
<p className="text-sm font-medium text-black">{item.detailSummary}</p>
|
|
360
|
+
) : null}
|
|
361
|
+
|
|
362
|
+
{hasNutritionFacts ? (
|
|
363
|
+
<div className="grid grid-cols-3 gap-3 rounded-3xl border border-black/[0.05] px-4 py-2 text-center sm:grid-cols-4">
|
|
364
|
+
{nutritionFacts.map((fact) => (
|
|
365
|
+
<div key={`${item.id}-${fact.label}`} className="space-y-0.5">
|
|
366
|
+
<p className="text-base font-medium text-black">{fact.value}</p>
|
|
367
|
+
<p className="text-xs text-black/60">{fact.label}</p>
|
|
368
|
+
</div>
|
|
369
|
+
))}
|
|
370
|
+
</div>
|
|
371
|
+
) : null}
|
|
372
|
+
|
|
373
|
+
{hasHighlights ? (
|
|
374
|
+
<div className="space-y-1 text-sm text-black/60">
|
|
375
|
+
{highlights.map((highlight, index) => (
|
|
376
|
+
<p key={`${item.id}-highlight-${index}`}>{highlight}</p>
|
|
377
|
+
))}
|
|
378
|
+
</div>
|
|
379
|
+
) : null}
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
type CheckoutDetailsPanelProps = {
|
|
386
|
+
shouldShowCheckoutOnly: boolean;
|
|
387
|
+
subtotal: number;
|
|
388
|
+
total: number;
|
|
389
|
+
onContinueToPayment?: () => void;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
function CheckoutDetailsPanel({
|
|
393
|
+
shouldShowCheckoutOnly,
|
|
394
|
+
subtotal,
|
|
395
|
+
total,
|
|
396
|
+
onContinueToPayment,
|
|
397
|
+
}: CheckoutDetailsPanelProps) {
|
|
398
|
+
return (
|
|
399
|
+
<>
|
|
400
|
+
{!shouldShowCheckoutOnly && (
|
|
401
|
+
<header className="hidden space-y-4 sm:block">
|
|
402
|
+
<h2 className="text-xl text-black">Checkout details</h2>
|
|
403
|
+
</header>
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
<section className="space-y-4 border-t border-black/5 pt-3">
|
|
407
|
+
<div className="space-y-0">
|
|
408
|
+
<h3 className="text-sm font-medium">Delivery address</h3>
|
|
409
|
+
</div>
|
|
410
|
+
<div className="space-y-0">
|
|
411
|
+
<p className="text-base text-sm font-medium text-slate-900">
|
|
412
|
+
1234 Main St, San Francisco, CA
|
|
413
|
+
</p>
|
|
414
|
+
<p className="text-xs text-black/50">
|
|
415
|
+
Leave at door - Delivery instructions
|
|
416
|
+
</p>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<div className="mt-1 flex flex-row items-center gap-3">
|
|
420
|
+
<div className="flex flex-1 items-center justify-between rounded-xl border border-black/35 bg-white px-4 py-2.5 shadow-sm">
|
|
421
|
+
<div>
|
|
422
|
+
<p className="text-sm font-medium text-slate-900">Fast</p>
|
|
423
|
+
<p className="line-clamp-1 text-xs text-black/50">
|
|
424
|
+
50 min - 2 hr 10 min
|
|
425
|
+
</p>
|
|
426
|
+
</div>
|
|
427
|
+
<span className="text-sm font-semibold text-[#047857]">Free</span>
|
|
428
|
+
</div>
|
|
429
|
+
<div className="flex flex-1 items-center justify-between rounded-xl border border-black/10 px-4 py-2.5">
|
|
430
|
+
<div>
|
|
431
|
+
<p className="text-sm font-medium text-slate-900">Priority</p>
|
|
432
|
+
<p className="line-clamp-1 text-xs text-black/50">35 min</p>
|
|
433
|
+
</div>
|
|
434
|
+
<span className="text-sm font-semibold text-[#047857]">Free</span>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</section>
|
|
438
|
+
|
|
439
|
+
<section className="space-y-4 border-t border-black/5 pt-3">
|
|
440
|
+
<div>
|
|
441
|
+
<h3 className="text-sm font-medium text-black">Delivery tip</h3>
|
|
442
|
+
<p className="text-xs text-black/50">100% goes to the shopper</p>
|
|
443
|
+
</div>
|
|
444
|
+
<div className="flex items-center gap-3 text-sm">
|
|
445
|
+
<Button
|
|
446
|
+
type="button"
|
|
447
|
+
variant="soft"
|
|
448
|
+
color="secondary"
|
|
449
|
+
size="sm"
|
|
450
|
+
className="flex-1"
|
|
451
|
+
>
|
|
452
|
+
5%
|
|
453
|
+
</Button>
|
|
454
|
+
<Button
|
|
455
|
+
type="button"
|
|
456
|
+
variant="solid"
|
|
457
|
+
color="primary"
|
|
458
|
+
size="sm"
|
|
459
|
+
className="flex-1"
|
|
460
|
+
>
|
|
461
|
+
10%
|
|
462
|
+
</Button>
|
|
463
|
+
<Button
|
|
464
|
+
type="button"
|
|
465
|
+
variant="soft"
|
|
466
|
+
color="secondary"
|
|
467
|
+
size="sm"
|
|
468
|
+
className="flex-1"
|
|
469
|
+
>
|
|
470
|
+
15%
|
|
471
|
+
</Button>
|
|
472
|
+
<Button
|
|
473
|
+
type="button"
|
|
474
|
+
variant="soft"
|
|
475
|
+
color="secondary"
|
|
476
|
+
size="sm"
|
|
477
|
+
className="flex-1"
|
|
478
|
+
>
|
|
479
|
+
Other
|
|
480
|
+
</Button>
|
|
481
|
+
</div>
|
|
482
|
+
</section>
|
|
483
|
+
|
|
484
|
+
<section className="space-y-1 border-t border-black/5 pt-3 text-center">
|
|
485
|
+
<div className="flex items-center justify-between">
|
|
486
|
+
<span className="text-sm text-black/70">Subtotal</span>
|
|
487
|
+
<span className="text-md text-black">${subtotal.toFixed(2)}</span>
|
|
488
|
+
</div>
|
|
489
|
+
<div className="flex items-center justify-between">
|
|
490
|
+
<span className="text-sm text-black/70">Total</span>
|
|
491
|
+
<span className="text-md font-medium text-black">
|
|
492
|
+
${total.toFixed(2)}
|
|
493
|
+
</span>
|
|
494
|
+
</div>
|
|
495
|
+
<p className="mt-3 mb-4 border-b border-black/5 text-xs text-black/50"></p>
|
|
496
|
+
<Button
|
|
497
|
+
type="button"
|
|
498
|
+
color="primary"
|
|
499
|
+
variant="solid"
|
|
500
|
+
size="md"
|
|
501
|
+
className="mx-auto w-full max-w-xs"
|
|
502
|
+
block
|
|
503
|
+
onClick={onContinueToPayment}
|
|
504
|
+
>
|
|
505
|
+
Continue to payment
|
|
506
|
+
</Button>
|
|
507
|
+
</section>
|
|
508
|
+
</>
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function App() {
|
|
513
|
+
const maxHeight = useMaxHeight() ?? undefined;
|
|
514
|
+
const displayMode = useDisplayMode();
|
|
515
|
+
const isFullscreen = displayMode === "fullscreen";
|
|
516
|
+
const widgetProps = useWidgetProps<PizzazCartWidgetProps>(() => ({}));
|
|
517
|
+
const [widgetState, setWidgetState] = useWidgetState<PizzazCartWidgetState>(
|
|
518
|
+
createDefaultWidgetState
|
|
519
|
+
);
|
|
520
|
+
const navigate = useNavigate();
|
|
521
|
+
const location = useLocation();
|
|
522
|
+
const isCheckoutRoute = useMemo(() => {
|
|
523
|
+
const pathname = location?.pathname ?? "";
|
|
524
|
+
if (!pathname) {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return pathname === "/checkout" || pathname.endsWith("/checkout");
|
|
529
|
+
}, [location?.pathname]);
|
|
530
|
+
|
|
531
|
+
const defaultCartItems = useMemo(() => createDefaultCartItems(), []);
|
|
532
|
+
const cartGridRef = useRef<HTMLDivElement | null>(null);
|
|
533
|
+
const [gridColumnCount, setGridColumnCount] = useState(1);
|
|
534
|
+
|
|
535
|
+
const mergeWithDefaultItems = useCallback(
|
|
536
|
+
(items: CartItem[]): CartItem[] => {
|
|
537
|
+
const existingIds = new Set(items.map((item) => item.id));
|
|
538
|
+
const merged = items.map((item) => {
|
|
539
|
+
const defaultItem = defaultCartItems.find(
|
|
540
|
+
(candidate) => candidate.id === item.id
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
if (!defaultItem) {
|
|
544
|
+
return cloneCartItem(item);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const enriched: CartItem = {
|
|
548
|
+
...cloneCartItem(defaultItem),
|
|
549
|
+
...item,
|
|
550
|
+
tags: item.tags ? [...item.tags] : defaultItem.tags,
|
|
551
|
+
nutritionFacts:
|
|
552
|
+
item.nutritionFacts ??
|
|
553
|
+
defaultItem.nutritionFacts?.map((fact) => ({ ...fact })),
|
|
554
|
+
highlights:
|
|
555
|
+
item.highlights != null
|
|
556
|
+
? [...item.highlights]
|
|
557
|
+
: defaultItem.highlights
|
|
558
|
+
? [...defaultItem.highlights]
|
|
559
|
+
: undefined,
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
return cloneCartItem(enriched);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
defaultCartItems.forEach((defaultItem) => {
|
|
566
|
+
if (!existingIds.has(defaultItem.id)) {
|
|
567
|
+
merged.push(cloneCartItem(defaultItem));
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
return merged;
|
|
572
|
+
},
|
|
573
|
+
[defaultCartItems]
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
const resolvedCartItems = useMemo(() => {
|
|
577
|
+
if (Array.isArray(widgetState?.cartItems) && widgetState.cartItems.length) {
|
|
578
|
+
return mergeWithDefaultItems(widgetState.cartItems);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (
|
|
582
|
+
Array.isArray(widgetProps?.widgetState?.cartItems) &&
|
|
583
|
+
widgetProps.widgetState.cartItems.length
|
|
584
|
+
) {
|
|
585
|
+
return mergeWithDefaultItems(widgetProps.widgetState.cartItems);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (Array.isArray(widgetProps?.cartItems) && widgetProps.cartItems.length) {
|
|
589
|
+
return mergeWithDefaultItems(widgetProps.cartItems);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return mergeWithDefaultItems(defaultCartItems);
|
|
593
|
+
}, [
|
|
594
|
+
defaultCartItems,
|
|
595
|
+
mergeWithDefaultItems,
|
|
596
|
+
widgetProps?.cartItems,
|
|
597
|
+
widgetProps?.widgetState?.cartItems,
|
|
598
|
+
widgetState,
|
|
599
|
+
]);
|
|
600
|
+
|
|
601
|
+
const [cartItems, setCartItems] = useState<CartItem[]>(resolvedCartItems);
|
|
602
|
+
|
|
603
|
+
useEffect(() => {
|
|
604
|
+
setCartItems((previous) =>
|
|
605
|
+
cartItemsEqual(previous, resolvedCartItems) ? previous : resolvedCartItems
|
|
606
|
+
);
|
|
607
|
+
}, [resolvedCartItems]);
|
|
608
|
+
|
|
609
|
+
const resolvedSelectedCartItemId =
|
|
610
|
+
widgetState?.selectedCartItemId ??
|
|
611
|
+
widgetProps?.widgetState?.selectedCartItemId ??
|
|
612
|
+
null;
|
|
613
|
+
|
|
614
|
+
const [selectedCartItemId, setSelectedCartItemId] = useState<string | null>(
|
|
615
|
+
resolvedSelectedCartItemId
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
useEffect(() => {
|
|
619
|
+
setSelectedCartItemId((prev) =>
|
|
620
|
+
prev === resolvedSelectedCartItemId ? prev : resolvedSelectedCartItemId
|
|
621
|
+
);
|
|
622
|
+
}, [resolvedSelectedCartItemId]);
|
|
623
|
+
|
|
624
|
+
const view = useOpenAiGlobal("view");
|
|
625
|
+
const viewParams = view?.params;
|
|
626
|
+
const isModalView = view?.mode === "modal";
|
|
627
|
+
const checkoutFromState =
|
|
628
|
+
(widgetState?.state ?? widgetProps?.widgetState?.state) === "checkout";
|
|
629
|
+
const modalParams =
|
|
630
|
+
viewParams && typeof viewParams === "object"
|
|
631
|
+
? (viewParams as {
|
|
632
|
+
state?: unknown;
|
|
633
|
+
cartItems?: unknown;
|
|
634
|
+
subtotal?: unknown;
|
|
635
|
+
total?: unknown;
|
|
636
|
+
totalItems?: unknown;
|
|
637
|
+
})
|
|
638
|
+
: null;
|
|
639
|
+
|
|
640
|
+
const modalState =
|
|
641
|
+
modalParams && typeof modalParams.state === "string"
|
|
642
|
+
? (modalParams.state as string)
|
|
643
|
+
: null;
|
|
644
|
+
|
|
645
|
+
const isCartModalView = isModalView && modalState === "cart";
|
|
646
|
+
const shouldShowCheckoutOnly =
|
|
647
|
+
isCheckoutRoute || (isModalView && !isCartModalView);
|
|
648
|
+
const wasModalViewRef = useRef(isModalView);
|
|
649
|
+
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
if (!viewParams || typeof viewParams !== "object") {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const paramsWithSelection = viewParams as {
|
|
656
|
+
selectedCartItemId?: unknown;
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const selectedIdFromParams = paramsWithSelection.selectedCartItemId;
|
|
660
|
+
|
|
661
|
+
if (
|
|
662
|
+
typeof selectedIdFromParams === "string" &&
|
|
663
|
+
selectedIdFromParams !== selectedCartItemId
|
|
664
|
+
) {
|
|
665
|
+
setSelectedCartItemId(selectedIdFromParams);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (selectedIdFromParams === null && selectedCartItemId !== null) {
|
|
670
|
+
setSelectedCartItemId(null);
|
|
671
|
+
}
|
|
672
|
+
}, [selectedCartItemId, viewParams]);
|
|
673
|
+
|
|
674
|
+
const [hoveredCartItemId, setHoveredCartItemId] = useState<string | null>(
|
|
675
|
+
null
|
|
676
|
+
);
|
|
677
|
+
const [activeFilters, setActiveFilters] = useState<string[]>([]);
|
|
678
|
+
|
|
679
|
+
const updateWidgetState = useCallback(
|
|
680
|
+
(partial: Partial<PizzazCartWidgetState>) => {
|
|
681
|
+
setWidgetState((previous) => ({
|
|
682
|
+
...createDefaultWidgetState(),
|
|
683
|
+
...(previous ?? {}),
|
|
684
|
+
...partial,
|
|
685
|
+
}));
|
|
686
|
+
},
|
|
687
|
+
[setWidgetState]
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
useEffect(() => {
|
|
691
|
+
if (!Array.isArray(widgetState?.cartItems)) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const merged = mergeWithDefaultItems(widgetState.cartItems);
|
|
696
|
+
|
|
697
|
+
if (!cartItemsEqual(widgetState.cartItems, merged)) {
|
|
698
|
+
updateWidgetState({ cartItems: merged });
|
|
699
|
+
}
|
|
700
|
+
}, [mergeWithDefaultItems, updateWidgetState, widgetState?.cartItems]);
|
|
701
|
+
|
|
702
|
+
useEffect(() => {
|
|
703
|
+
if (wasModalViewRef.current && !isModalView && checkoutFromState) {
|
|
704
|
+
updateWidgetState({ state: null });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
wasModalViewRef.current = isModalView;
|
|
708
|
+
}, [checkoutFromState, isModalView, updateWidgetState]);
|
|
709
|
+
|
|
710
|
+
const adjustQuantity = useCallback(
|
|
711
|
+
(id: string, delta: number) => {
|
|
712
|
+
setCartItems((previousItems) => {
|
|
713
|
+
const updatedItems = previousItems.map((item) =>
|
|
714
|
+
item.id === id
|
|
715
|
+
? { ...item, quantity: Math.max(0, item.quantity + delta) }
|
|
716
|
+
: item
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
if (!cartItemsEqual(previousItems, updatedItems)) {
|
|
720
|
+
updateWidgetState({ cartItems: updatedItems });
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return updatedItems;
|
|
724
|
+
});
|
|
725
|
+
},
|
|
726
|
+
[updateWidgetState]
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
useEffect(() => {
|
|
730
|
+
if (!shouldShowCheckoutOnly) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
setHoveredCartItemId(null);
|
|
735
|
+
}, [shouldShowCheckoutOnly]);
|
|
736
|
+
|
|
737
|
+
const manualCheckoutTriggerRef = useRef(false);
|
|
738
|
+
|
|
739
|
+
const requestModalWithAnchor = useCallback(
|
|
740
|
+
({
|
|
741
|
+
title,
|
|
742
|
+
params,
|
|
743
|
+
anchorElement,
|
|
744
|
+
}: {
|
|
745
|
+
title: string;
|
|
746
|
+
params: Record<string, unknown>;
|
|
747
|
+
anchorElement?: HTMLElement | null;
|
|
748
|
+
}) => {
|
|
749
|
+
if (isModalView) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const anchorRect = anchorElement?.getBoundingClientRect();
|
|
754
|
+
const anchor =
|
|
755
|
+
anchorRect == null
|
|
756
|
+
? undefined
|
|
757
|
+
: {
|
|
758
|
+
top: anchorRect.top,
|
|
759
|
+
left: anchorRect.left,
|
|
760
|
+
width: anchorRect.width,
|
|
761
|
+
height: anchorRect.height,
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
void (async () => {
|
|
765
|
+
try {
|
|
766
|
+
await window?.openai?.requestModal?.({
|
|
767
|
+
title,
|
|
768
|
+
params,
|
|
769
|
+
...(anchor ? { anchor } : {}),
|
|
770
|
+
});
|
|
771
|
+
} catch (error) {
|
|
772
|
+
console.error("Failed to open checkout modal", error);
|
|
773
|
+
}
|
|
774
|
+
})();
|
|
775
|
+
},
|
|
776
|
+
[isModalView]
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
const openCheckoutModal = useCallback(
|
|
780
|
+
(anchorElement?: HTMLElement | null) => {
|
|
781
|
+
requestModalWithAnchor({
|
|
782
|
+
title: "Checkout",
|
|
783
|
+
params: { state: "checkout" },
|
|
784
|
+
anchorElement,
|
|
785
|
+
});
|
|
786
|
+
},
|
|
787
|
+
[requestModalWithAnchor]
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
const openCartItemModal = useCallback(
|
|
791
|
+
({
|
|
792
|
+
selectedId,
|
|
793
|
+
selectedName,
|
|
794
|
+
anchorElement,
|
|
795
|
+
}: {
|
|
796
|
+
selectedId: string;
|
|
797
|
+
selectedName: string | null;
|
|
798
|
+
anchorElement?: HTMLElement | null;
|
|
799
|
+
}) => {
|
|
800
|
+
requestModalWithAnchor({
|
|
801
|
+
title: selectedName ?? selectedId,
|
|
802
|
+
params: { state: "checkout", selectedCartItemId: selectedId },
|
|
803
|
+
anchorElement,
|
|
804
|
+
});
|
|
805
|
+
},
|
|
806
|
+
[requestModalWithAnchor]
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
const handleCartItemSelect = useCallback(
|
|
810
|
+
(id: string, anchorElement?: HTMLElement | null) => {
|
|
811
|
+
const itemName = cartItems.find((item) => item.id === id)?.name ?? null;
|
|
812
|
+
manualCheckoutTriggerRef.current = true;
|
|
813
|
+
setSelectedCartItemId(id);
|
|
814
|
+
updateWidgetState({ selectedCartItemId: id, state: "checkout" });
|
|
815
|
+
openCartItemModal({
|
|
816
|
+
selectedId: id,
|
|
817
|
+
selectedName: itemName,
|
|
818
|
+
anchorElement,
|
|
819
|
+
});
|
|
820
|
+
},
|
|
821
|
+
[cartItems, openCartItemModal, updateWidgetState]
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
const subtotal = useMemo(
|
|
825
|
+
() =>
|
|
826
|
+
cartItems.reduce(
|
|
827
|
+
(total, item) => total + item.price * Math.max(0, item.quantity),
|
|
828
|
+
0
|
|
829
|
+
),
|
|
830
|
+
[cartItems]
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
const total = subtotal + SERVICE_FEE + DELIVERY_FEE + TAX_FEE;
|
|
834
|
+
|
|
835
|
+
const totalItems = useMemo(
|
|
836
|
+
() =>
|
|
837
|
+
cartItems.reduce((total, item) => total + Math.max(0, item.quantity), 0),
|
|
838
|
+
[cartItems]
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
const visibleCartItems = useMemo(() => {
|
|
842
|
+
if (!activeFilters.length) {
|
|
843
|
+
return cartItems;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return cartItems.filter((item) => {
|
|
847
|
+
const tags = item.tags ?? [];
|
|
848
|
+
|
|
849
|
+
return activeFilters.every((filterId) => {
|
|
850
|
+
const filterMeta = FILTERS.find((filter) => filter.id === filterId);
|
|
851
|
+
if (!filterMeta?.tag) {
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
return tags.includes(filterMeta.tag);
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
}, [activeFilters, cartItems]);
|
|
858
|
+
|
|
859
|
+
const updateItemColumnPlacement = useCallback(() => {
|
|
860
|
+
const gridNode = cartGridRef.current;
|
|
861
|
+
|
|
862
|
+
const width = gridNode?.offsetWidth ?? 0;
|
|
863
|
+
|
|
864
|
+
let baseColumnCount = 1;
|
|
865
|
+
if (width >= 768) {
|
|
866
|
+
baseColumnCount = 3;
|
|
867
|
+
} else if (width >= 640) {
|
|
868
|
+
baseColumnCount = 2;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const columnCount = isFullscreen
|
|
872
|
+
? Math.max(baseColumnCount, 3)
|
|
873
|
+
: baseColumnCount;
|
|
874
|
+
|
|
875
|
+
if (gridNode) {
|
|
876
|
+
gridNode.style.gridTemplateColumns = `repeat(${columnCount}, minmax(0, 1fr))`;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
setGridColumnCount(columnCount);
|
|
880
|
+
}, [isFullscreen]);
|
|
881
|
+
|
|
882
|
+
const handleFilterToggle = useCallback(
|
|
883
|
+
(id: string) => {
|
|
884
|
+
setActiveFilters((previous) => {
|
|
885
|
+
if (id === "all") {
|
|
886
|
+
return [];
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const isActive = previous.includes(id);
|
|
890
|
+
if (isActive) {
|
|
891
|
+
return [];
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return [id];
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
requestAnimationFrame(() => {
|
|
898
|
+
updateItemColumnPlacement();
|
|
899
|
+
});
|
|
900
|
+
},
|
|
901
|
+
[updateItemColumnPlacement]
|
|
902
|
+
);
|
|
903
|
+
|
|
904
|
+
useEffect(() => {
|
|
905
|
+
const node = cartGridRef.current;
|
|
906
|
+
|
|
907
|
+
if (!node) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const observer =
|
|
912
|
+
typeof ResizeObserver !== "undefined"
|
|
913
|
+
? new ResizeObserver(() => {
|
|
914
|
+
requestAnimationFrame(updateItemColumnPlacement);
|
|
915
|
+
})
|
|
916
|
+
: null;
|
|
917
|
+
|
|
918
|
+
observer?.observe(node);
|
|
919
|
+
window.addEventListener("resize", updateItemColumnPlacement);
|
|
920
|
+
|
|
921
|
+
return () => {
|
|
922
|
+
observer?.disconnect();
|
|
923
|
+
window.removeEventListener("resize", updateItemColumnPlacement);
|
|
924
|
+
};
|
|
925
|
+
}, [updateItemColumnPlacement]);
|
|
926
|
+
|
|
927
|
+
const openCartModal = useCallback(
|
|
928
|
+
(anchorElement?: HTMLElement | null) => {
|
|
929
|
+
if (isModalView || shouldShowCheckoutOnly) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
requestModalWithAnchor({
|
|
934
|
+
title: "Cart",
|
|
935
|
+
params: {
|
|
936
|
+
state: "cart",
|
|
937
|
+
cartItems,
|
|
938
|
+
subtotal,
|
|
939
|
+
total,
|
|
940
|
+
totalItems,
|
|
941
|
+
},
|
|
942
|
+
anchorElement,
|
|
943
|
+
});
|
|
944
|
+
},
|
|
945
|
+
[
|
|
946
|
+
cartItems,
|
|
947
|
+
isModalView,
|
|
948
|
+
requestModalWithAnchor,
|
|
949
|
+
shouldShowCheckoutOnly,
|
|
950
|
+
subtotal,
|
|
951
|
+
total,
|
|
952
|
+
totalItems,
|
|
953
|
+
]
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
type CartSummaryItem = {
|
|
957
|
+
id: string;
|
|
958
|
+
name: string;
|
|
959
|
+
price: number;
|
|
960
|
+
quantity: number;
|
|
961
|
+
image?: string;
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const cartSummaryItems: CartSummaryItem[] = useMemo(() => {
|
|
965
|
+
if (!isCartModalView) {
|
|
966
|
+
return [];
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const items = Array.isArray(modalParams?.cartItems)
|
|
970
|
+
? modalParams?.cartItems
|
|
971
|
+
: null;
|
|
972
|
+
|
|
973
|
+
if (!items) {
|
|
974
|
+
return cartItems.map((item) => ({
|
|
975
|
+
id: item.id,
|
|
976
|
+
name: item.name,
|
|
977
|
+
price: item.price,
|
|
978
|
+
quantity: Math.max(0, item.quantity),
|
|
979
|
+
image: item.image,
|
|
980
|
+
}));
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const sanitized = items
|
|
984
|
+
.map((raw, index) => {
|
|
985
|
+
if (!raw || typeof raw !== "object") {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
const candidate = raw as Record<string, unknown>;
|
|
989
|
+
const id =
|
|
990
|
+
typeof candidate.id === "string" ? candidate.id : `cart-${index}`;
|
|
991
|
+
const name =
|
|
992
|
+
typeof candidate.name === "string" ? candidate.name : "Item";
|
|
993
|
+
const priceValue = Number(candidate.price);
|
|
994
|
+
const quantityValue = Number(candidate.quantity);
|
|
995
|
+
const price = Number.isFinite(priceValue) ? priceValue : 0;
|
|
996
|
+
const quantity = Number.isFinite(quantityValue)
|
|
997
|
+
? Math.max(0, quantityValue)
|
|
998
|
+
: 0;
|
|
999
|
+
const image =
|
|
1000
|
+
typeof candidate.image === "string" ? candidate.image : undefined;
|
|
1001
|
+
|
|
1002
|
+
return {
|
|
1003
|
+
id,
|
|
1004
|
+
name,
|
|
1005
|
+
price,
|
|
1006
|
+
quantity,
|
|
1007
|
+
image,
|
|
1008
|
+
} as CartSummaryItem;
|
|
1009
|
+
})
|
|
1010
|
+
.filter(Boolean) as CartSummaryItem[];
|
|
1011
|
+
|
|
1012
|
+
if (sanitized.length === 0) {
|
|
1013
|
+
return cartItems.map((item) => ({
|
|
1014
|
+
id: item.id,
|
|
1015
|
+
name: item.name,
|
|
1016
|
+
price: item.price,
|
|
1017
|
+
quantity: Math.max(0, item.quantity),
|
|
1018
|
+
image: item.image,
|
|
1019
|
+
}));
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return sanitized;
|
|
1023
|
+
}, [cartItems, isCartModalView, modalParams?.cartItems]);
|
|
1024
|
+
|
|
1025
|
+
const cartSummarySubtotal = useMemo(() => {
|
|
1026
|
+
if (!isCartModalView) {
|
|
1027
|
+
return subtotal;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const candidate = Number(modalParams?.subtotal);
|
|
1031
|
+
return Number.isFinite(candidate) ? candidate : subtotal;
|
|
1032
|
+
}, [isCartModalView, modalParams?.subtotal, subtotal]);
|
|
1033
|
+
|
|
1034
|
+
const cartSummaryTotal = useMemo(() => {
|
|
1035
|
+
if (!isCartModalView) {
|
|
1036
|
+
return total;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const candidate = Number(modalParams?.total);
|
|
1040
|
+
return Number.isFinite(candidate) ? candidate : total;
|
|
1041
|
+
}, [isCartModalView, modalParams?.total, total]);
|
|
1042
|
+
|
|
1043
|
+
const cartSummaryTotalItems = useMemo(() => {
|
|
1044
|
+
if (!isCartModalView) {
|
|
1045
|
+
return totalItems;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const candidate = Number(modalParams?.totalItems);
|
|
1049
|
+
return Number.isFinite(candidate) ? candidate : totalItems;
|
|
1050
|
+
}, [isCartModalView, modalParams?.totalItems, totalItems]);
|
|
1051
|
+
|
|
1052
|
+
const handleContinueToPayment = useCallback(
|
|
1053
|
+
(event?: ReactMouseEvent<HTMLElement>) => {
|
|
1054
|
+
const anchorElement = event?.currentTarget ?? null;
|
|
1055
|
+
|
|
1056
|
+
if (typeof window !== "undefined") {
|
|
1057
|
+
const detail = {
|
|
1058
|
+
subtotal: isCartModalView ? cartSummarySubtotal : subtotal,
|
|
1059
|
+
total: isCartModalView ? cartSummaryTotal : total,
|
|
1060
|
+
totalItems: isCartModalView ? cartSummaryTotalItems : totalItems,
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
try {
|
|
1064
|
+
window.dispatchEvent(
|
|
1065
|
+
new CustomEvent(CONTINUE_TO_PAYMENT_EVENT, { detail })
|
|
1066
|
+
);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
console.error("Failed to dispatch checkout navigation event", error);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (isCartModalView) {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
manualCheckoutTriggerRef.current = true;
|
|
1077
|
+
updateWidgetState({ state: "checkout" });
|
|
1078
|
+
const shouldNavigateToCheckout = isCartModalView || !isCheckoutRoute;
|
|
1079
|
+
|
|
1080
|
+
if (shouldNavigateToCheckout) {
|
|
1081
|
+
navigate("/checkout");
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
openCheckoutModal(anchorElement);
|
|
1086
|
+
},
|
|
1087
|
+
[
|
|
1088
|
+
cartSummarySubtotal,
|
|
1089
|
+
cartSummaryTotal,
|
|
1090
|
+
cartSummaryTotalItems,
|
|
1091
|
+
isCartModalView,
|
|
1092
|
+
isCheckoutRoute,
|
|
1093
|
+
navigate,
|
|
1094
|
+
openCheckoutModal,
|
|
1095
|
+
subtotal,
|
|
1096
|
+
total,
|
|
1097
|
+
totalItems,
|
|
1098
|
+
updateWidgetState,
|
|
1099
|
+
]
|
|
1100
|
+
);
|
|
1101
|
+
|
|
1102
|
+
const handleSeeAll = useCallback(async () => {
|
|
1103
|
+
if (typeof window === "undefined") {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
try {
|
|
1108
|
+
await window?.openai?.requestDisplayMode?.({ mode: "fullscreen" });
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
console.error("Failed to request fullscreen display mode", error);
|
|
1111
|
+
}
|
|
1112
|
+
}, []);
|
|
1113
|
+
|
|
1114
|
+
useLayoutEffect(() => {
|
|
1115
|
+
const raf = requestAnimationFrame(updateItemColumnPlacement);
|
|
1116
|
+
|
|
1117
|
+
return () => {
|
|
1118
|
+
cancelAnimationFrame(raf);
|
|
1119
|
+
};
|
|
1120
|
+
}, [updateItemColumnPlacement, visibleCartItems]);
|
|
1121
|
+
|
|
1122
|
+
const selectedCartItem = useMemo(() => {
|
|
1123
|
+
if (selectedCartItemId == null) {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
return cartItems.find((item) => item.id === selectedCartItemId) ?? null;
|
|
1127
|
+
}, [cartItems, selectedCartItemId]);
|
|
1128
|
+
|
|
1129
|
+
const selectedCartItemName = selectedCartItem?.name ?? null;
|
|
1130
|
+
const shouldShowSelectedCartItemPanel =
|
|
1131
|
+
selectedCartItem != null && !isFullscreen;
|
|
1132
|
+
|
|
1133
|
+
useEffect(() => {
|
|
1134
|
+
if (isCheckoutRoute) {
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (!checkoutFromState) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if (manualCheckoutTriggerRef.current) {
|
|
1143
|
+
manualCheckoutTriggerRef.current = false;
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (selectedCartItemId) {
|
|
1148
|
+
openCartItemModal({
|
|
1149
|
+
selectedId: selectedCartItemId,
|
|
1150
|
+
selectedName: selectedCartItemName,
|
|
1151
|
+
});
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
openCheckoutModal();
|
|
1156
|
+
}, [
|
|
1157
|
+
isCheckoutRoute,
|
|
1158
|
+
checkoutFromState,
|
|
1159
|
+
openCartItemModal,
|
|
1160
|
+
openCheckoutModal,
|
|
1161
|
+
selectedCartItemId,
|
|
1162
|
+
selectedCartItemName,
|
|
1163
|
+
]);
|
|
1164
|
+
|
|
1165
|
+
const cartPanel = (
|
|
1166
|
+
<section>
|
|
1167
|
+
{!shouldShowCheckoutOnly && (
|
|
1168
|
+
<header className="mb-4 flex flex-col gap-3 border-b border-black/5 px-0 pb-3 sm:flex-row sm:items-center sm:justify-between">
|
|
1169
|
+
{!isFullscreen ? (
|
|
1170
|
+
<div className="flex items-center gap-3">
|
|
1171
|
+
<Button
|
|
1172
|
+
type="button"
|
|
1173
|
+
onClick={(event) =>
|
|
1174
|
+
openCartModal(event.currentTarget as HTMLElement)
|
|
1175
|
+
}
|
|
1176
|
+
aria-haspopup="dialog"
|
|
1177
|
+
variant="outline"
|
|
1178
|
+
color="secondary"
|
|
1179
|
+
size="sm"
|
|
1180
|
+
>
|
|
1181
|
+
<ShoppingCart className="h-4 w-4" aria-hidden="true" />
|
|
1182
|
+
<span>Cart</span>
|
|
1183
|
+
</Button>
|
|
1184
|
+
</div>
|
|
1185
|
+
) : (
|
|
1186
|
+
<div className="text-lg text-black/70">Results</div>
|
|
1187
|
+
)}
|
|
1188
|
+
<nav className="flex flex-wrap items-center gap-2">
|
|
1189
|
+
{FILTERS.map((filter) => {
|
|
1190
|
+
const isActive =
|
|
1191
|
+
filter.id === "all"
|
|
1192
|
+
? activeFilters.length === 0
|
|
1193
|
+
: activeFilters.includes(filter.id);
|
|
1194
|
+
|
|
1195
|
+
return (
|
|
1196
|
+
<Button
|
|
1197
|
+
key={filter.id}
|
|
1198
|
+
type="button"
|
|
1199
|
+
onClick={() => handleFilterToggle(filter.id)}
|
|
1200
|
+
aria-pressed={isActive}
|
|
1201
|
+
variant={isActive ? "solid" : "outline"}
|
|
1202
|
+
color="primary"
|
|
1203
|
+
size="sm"
|
|
1204
|
+
>
|
|
1205
|
+
{filter.label}
|
|
1206
|
+
</Button>
|
|
1207
|
+
);
|
|
1208
|
+
})}
|
|
1209
|
+
</nav>
|
|
1210
|
+
</header>
|
|
1211
|
+
)}
|
|
1212
|
+
|
|
1213
|
+
<LayoutGroup id="pizzas-grid">
|
|
1214
|
+
<div
|
|
1215
|
+
ref={cartGridRef}
|
|
1216
|
+
className={clsx(
|
|
1217
|
+
"mt-4 grid gap-[1.5px]",
|
|
1218
|
+
isFullscreen ? "grid-cols-3" : "sm:grid-cols-2 md:grid-cols-3"
|
|
1219
|
+
)}
|
|
1220
|
+
>
|
|
1221
|
+
<AnimatePresence initial={false} mode="popLayout">
|
|
1222
|
+
{visibleCartItems.map((item, index) => {
|
|
1223
|
+
const isHovered = hoveredCartItemId === item.id;
|
|
1224
|
+
const shortDescription =
|
|
1225
|
+
item.shortDescription ?? item.description.split(".")[0];
|
|
1226
|
+
const columnCount = Math.max(gridColumnCount, 1);
|
|
1227
|
+
const rowStartIndex =
|
|
1228
|
+
Math.floor(index / columnCount) * columnCount;
|
|
1229
|
+
const itemsRemaining = visibleCartItems.length - rowStartIndex;
|
|
1230
|
+
const rowSize = Math.min(columnCount, itemsRemaining);
|
|
1231
|
+
const positionInRow = index - rowStartIndex;
|
|
1232
|
+
|
|
1233
|
+
const isSingle = rowSize === 1;
|
|
1234
|
+
const isLeft = positionInRow === 0;
|
|
1235
|
+
const isRight = positionInRow === rowSize - 1;
|
|
1236
|
+
|
|
1237
|
+
return (
|
|
1238
|
+
<motion.article
|
|
1239
|
+
layout
|
|
1240
|
+
layoutId={item.id}
|
|
1241
|
+
key={item.id}
|
|
1242
|
+
initial={{ opacity: 0, scale: 0.98 }}
|
|
1243
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
1244
|
+
exit={{ opacity: 0, scale: 0.98 }}
|
|
1245
|
+
transition={{
|
|
1246
|
+
type: "spring",
|
|
1247
|
+
stiffness: 260,
|
|
1248
|
+
damping: 26,
|
|
1249
|
+
mass: 0.8,
|
|
1250
|
+
}}
|
|
1251
|
+
onClick={(event) =>
|
|
1252
|
+
handleCartItemSelect(
|
|
1253
|
+
item.id,
|
|
1254
|
+
event.currentTarget as HTMLElement
|
|
1255
|
+
)
|
|
1256
|
+
}
|
|
1257
|
+
onMouseEnter={() => setHoveredCartItemId(item.id)}
|
|
1258
|
+
onMouseLeave={() => setHoveredCartItemId(null)}
|
|
1259
|
+
className={clsx(
|
|
1260
|
+
"group mb-4 flex cursor-pointer flex-col overflow-hidden border border-transparent bg-white transition-colors",
|
|
1261
|
+
isHovered && "border-[#0f766e]"
|
|
1262
|
+
)}
|
|
1263
|
+
>
|
|
1264
|
+
<div
|
|
1265
|
+
className={clsx(
|
|
1266
|
+
"relative overflow-hidden",
|
|
1267
|
+
isSingle && "rounded-3xl",
|
|
1268
|
+
!isSingle && isLeft && "rounded-l-3xl",
|
|
1269
|
+
!isSingle && isRight && "rounded-r-3xl",
|
|
1270
|
+
!isSingle && !isLeft && !isRight && "rounded-none"
|
|
1271
|
+
)}
|
|
1272
|
+
>
|
|
1273
|
+
<Image
|
|
1274
|
+
src={item.image}
|
|
1275
|
+
alt={item.name}
|
|
1276
|
+
className="h-60 w-full object-cover transition-transform duration-200"
|
|
1277
|
+
/>
|
|
1278
|
+
|
|
1279
|
+
<div className="absolute inset-0 bg-black/[0.05]" />
|
|
1280
|
+
</div>
|
|
1281
|
+
<div className="flex flex-1 flex-col gap-3 pe-6 pt-3 pb-4 text-left">
|
|
1282
|
+
<div className="space-y-0.5">
|
|
1283
|
+
<p className="text-base font-semibold text-slate-900">
|
|
1284
|
+
{item.name}
|
|
1285
|
+
</p>
|
|
1286
|
+
<p className="text-sm text-black/60">
|
|
1287
|
+
${item.price.toFixed(2)}
|
|
1288
|
+
</p>
|
|
1289
|
+
</div>
|
|
1290
|
+
{shortDescription ? (
|
|
1291
|
+
<p
|
|
1292
|
+
className="text-sm leading-snug text-black/50"
|
|
1293
|
+
title={shortDescription}
|
|
1294
|
+
>
|
|
1295
|
+
{shortDescription}
|
|
1296
|
+
</p>
|
|
1297
|
+
) : null}
|
|
1298
|
+
<div className="flex items-center justify-between">
|
|
1299
|
+
<div className="flex items-center rounded-full bg-black/[0.04] px-1.5 py-1 text-black">
|
|
1300
|
+
<button
|
|
1301
|
+
type="button"
|
|
1302
|
+
className="flex h-6 w-6 items-center justify-center rounded-full opacity-50 transition-colors hover:bg-slate-200 hover:opacity-100"
|
|
1303
|
+
aria-label={`Decrease quantity of ${item.name}`}
|
|
1304
|
+
onClick={(event) => {
|
|
1305
|
+
event.stopPropagation();
|
|
1306
|
+
adjustQuantity(item.id, -1);
|
|
1307
|
+
}}
|
|
1308
|
+
>
|
|
1309
|
+
<Minus
|
|
1310
|
+
strokeWidth={2.5}
|
|
1311
|
+
className="h-3 w-3"
|
|
1312
|
+
aria-hidden="true"
|
|
1313
|
+
/>
|
|
1314
|
+
</button>
|
|
1315
|
+
<span className="min-w-[20px] px-1 text-center text-sm font-medium">
|
|
1316
|
+
{item.quantity}
|
|
1317
|
+
</span>
|
|
1318
|
+
<button
|
|
1319
|
+
type="button"
|
|
1320
|
+
className="flex h-6 w-6 items-center justify-center rounded-full opacity-50 transition-colors hover:bg-slate-200 hover:opacity-100"
|
|
1321
|
+
aria-label={`Increase quantity of ${item.name}`}
|
|
1322
|
+
onClick={(event) => {
|
|
1323
|
+
event.stopPropagation();
|
|
1324
|
+
adjustQuantity(item.id, 1);
|
|
1325
|
+
}}
|
|
1326
|
+
>
|
|
1327
|
+
<Plus
|
|
1328
|
+
strokeWidth={2.5}
|
|
1329
|
+
className="h-3 w-3"
|
|
1330
|
+
aria-hidden="true"
|
|
1331
|
+
/>
|
|
1332
|
+
</button>
|
|
1333
|
+
</div>
|
|
1334
|
+
</div>
|
|
1335
|
+
</div>
|
|
1336
|
+
</motion.article>
|
|
1337
|
+
);
|
|
1338
|
+
})}
|
|
1339
|
+
</AnimatePresence>
|
|
1340
|
+
</div>
|
|
1341
|
+
</LayoutGroup>
|
|
1342
|
+
</section>
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
if (isCartModalView && !isCheckoutRoute) {
|
|
1346
|
+
return (
|
|
1347
|
+
<div className="flex w-full flex-col gap-6 px-4">
|
|
1348
|
+
<div className="divide-y divide-black/5">
|
|
1349
|
+
{cartSummaryItems.length ? (
|
|
1350
|
+
cartSummaryItems.map((item) => (
|
|
1351
|
+
<div
|
|
1352
|
+
key={`modal-${item.id}`}
|
|
1353
|
+
className="flex items-center gap-3 py-2"
|
|
1354
|
+
>
|
|
1355
|
+
<div className="relative h-10 w-10 overflow-hidden rounded-xl bg-white">
|
|
1356
|
+
{item.image ? (
|
|
1357
|
+
<Image
|
|
1358
|
+
src={item.image}
|
|
1359
|
+
alt={item.name}
|
|
1360
|
+
className="h-full w-full object-cover"
|
|
1361
|
+
/>
|
|
1362
|
+
) : null}
|
|
1363
|
+
<div className="absolute inset-0 bg-black/[0.05]" />
|
|
1364
|
+
</div>
|
|
1365
|
+
<div className="flex min-w-0 flex-1 items-center justify-between gap-3">
|
|
1366
|
+
<div className="min-w-0">
|
|
1367
|
+
<p className="truncate font-medium text-slate-900">
|
|
1368
|
+
{item.name}
|
|
1369
|
+
</p>
|
|
1370
|
+
<p className="text-xs text-black/50">
|
|
1371
|
+
${item.price.toFixed(2)} • Qty{" "}
|
|
1372
|
+
{Math.max(0, item.quantity)}
|
|
1373
|
+
</p>
|
|
1374
|
+
</div>
|
|
1375
|
+
<span className="text-sm font-medium text-black">
|
|
1376
|
+
${(item.price * Math.max(0, item.quantity)).toFixed(2)}
|
|
1377
|
+
</span>
|
|
1378
|
+
</div>
|
|
1379
|
+
</div>
|
|
1380
|
+
))
|
|
1381
|
+
) : (
|
|
1382
|
+
<p className="rounded-2xl border border-dashed border-black/20 bg-white/90 p-6 text-center text-sm text-black/50">
|
|
1383
|
+
Your cart is empty.
|
|
1384
|
+
</p>
|
|
1385
|
+
)}
|
|
1386
|
+
</div>
|
|
1387
|
+
|
|
1388
|
+
<div className="space-y-0.5">
|
|
1389
|
+
<div className="flex items-center justify-between text-sm font-medium text-black">
|
|
1390
|
+
<span>Subtotal</span>
|
|
1391
|
+
<span>${cartSummarySubtotal.toFixed(2)}</span>
|
|
1392
|
+
</div>
|
|
1393
|
+
<div className="flex items-center justify-between text-sm text-black/60">
|
|
1394
|
+
<span>Total</span>
|
|
1395
|
+
<span>${cartSummaryTotal.toFixed(2)}</span>
|
|
1396
|
+
</div>
|
|
1397
|
+
</div>
|
|
1398
|
+
<button
|
|
1399
|
+
type="button"
|
|
1400
|
+
onClick={handleContinueToPayment}
|
|
1401
|
+
className="mx-auto mb-4 w-full rounded-full bg-[#FF5100] px-6 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-[#ff6a26] disabled:cursor-not-allowed disabled:bg-black/20"
|
|
1402
|
+
disabled={cartSummaryTotalItems === 0}
|
|
1403
|
+
>
|
|
1404
|
+
Continue to checkout
|
|
1405
|
+
</button>
|
|
1406
|
+
</div>
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const checkoutPanel = (
|
|
1411
|
+
<div
|
|
1412
|
+
className={
|
|
1413
|
+
shouldShowCheckoutOnly
|
|
1414
|
+
? "space-y-4"
|
|
1415
|
+
: "space-y-4 overflow-hidden border-black/[0.075] pt-4 md:rounded-3xl md:border md:px-5 md:pb-5 md:shadow-[0px_6px_14px_rgba(0,0,0,0.06)]"
|
|
1416
|
+
}
|
|
1417
|
+
>
|
|
1418
|
+
{shouldShowSelectedCartItemPanel ? (
|
|
1419
|
+
<SelectedCartItemPanel
|
|
1420
|
+
item={selectedCartItem}
|
|
1421
|
+
onAdjustQuantity={adjustQuantity}
|
|
1422
|
+
/>
|
|
1423
|
+
) : (
|
|
1424
|
+
<CheckoutDetailsPanel
|
|
1425
|
+
shouldShowCheckoutOnly={shouldShowCheckoutOnly}
|
|
1426
|
+
subtotal={subtotal}
|
|
1427
|
+
total={total}
|
|
1428
|
+
onContinueToPayment={handleContinueToPayment}
|
|
1429
|
+
/>
|
|
1430
|
+
)}
|
|
1431
|
+
</div>
|
|
1432
|
+
);
|
|
1433
|
+
|
|
1434
|
+
return (
|
|
1435
|
+
<div
|
|
1436
|
+
className={clsx(
|
|
1437
|
+
`flex items-center justify-center overflow-hidden`,
|
|
1438
|
+
isModalView ? "px-0 pb-4" : ""
|
|
1439
|
+
)}
|
|
1440
|
+
style={{
|
|
1441
|
+
maxHeight,
|
|
1442
|
+
height: displayMode === "fullscreen" ? maxHeight : undefined,
|
|
1443
|
+
overflow: "hidden",
|
|
1444
|
+
scrollbarGutter: "0px",
|
|
1445
|
+
scrollbarWidth: "none",
|
|
1446
|
+
msOverflowStyle: "none",
|
|
1447
|
+
}}
|
|
1448
|
+
>
|
|
1449
|
+
<main
|
|
1450
|
+
className={`w-full overflow-hidden ${isFullscreen ? "max-w-7xl" : ""}`}
|
|
1451
|
+
>
|
|
1452
|
+
{shouldShowCheckoutOnly ? (
|
|
1453
|
+
checkoutPanel
|
|
1454
|
+
) : isFullscreen ? (
|
|
1455
|
+
<div className="mt-8 grid gap-0 overflow-hidden md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_360px] md:gap-8">
|
|
1456
|
+
<div className="md:col-span-2">{cartPanel}</div>
|
|
1457
|
+
<div>{checkoutPanel}</div>
|
|
1458
|
+
</div>
|
|
1459
|
+
) : (
|
|
1460
|
+
cartPanel
|
|
1461
|
+
)}
|
|
1462
|
+
{!isFullscreen && !shouldShowCheckoutOnly && (
|
|
1463
|
+
<div className="flex justify-center">
|
|
1464
|
+
<button
|
|
1465
|
+
type="button"
|
|
1466
|
+
onClick={handleSeeAll}
|
|
1467
|
+
className="rounded-full border border-black/10 px-4 py-2 text-sm font-medium text-black/70 transition-colors hover:border-black/40 hover:text-black"
|
|
1468
|
+
>
|
|
1469
|
+
See all items
|
|
1470
|
+
</button>
|
|
1471
|
+
</div>
|
|
1472
|
+
)}
|
|
1473
|
+
</main>
|
|
1474
|
+
</div>
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
createRoot(document.getElementById("pizzaz-shop-root")!).render(
|
|
1479
|
+
<BrowserRouter>
|
|
1480
|
+
<App />
|
|
1481
|
+
</BrowserRouter>
|
|
1482
|
+
);
|