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,165 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import useEmblaCarousel from "embla-carousel-react";
|
|
3
|
+
import { useOpenAiGlobal } from "../use-openai-global";
|
|
4
|
+
import { Settings2, Star } from "lucide-react";
|
|
5
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
6
|
+
import { Button } from "@openai/apps-sdk-ui/components/Button";
|
|
7
|
+
import { Image } from "@openai/apps-sdk-ui/components/Image";
|
|
8
|
+
|
|
9
|
+
function PlaceListItem({ place, isSelected, onClick }) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
className={
|
|
13
|
+
"rounded-2xl px-3 select-none hover:bg-black/5 cursor-pointer" +
|
|
14
|
+
(isSelected ? " bg-black/5" : "")
|
|
15
|
+
}
|
|
16
|
+
>
|
|
17
|
+
<div
|
|
18
|
+
className={`border-b ${
|
|
19
|
+
isSelected ? "border-black/0" : "border-black/5"
|
|
20
|
+
} hover:border-black/0`}
|
|
21
|
+
>
|
|
22
|
+
<div
|
|
23
|
+
className="w-full text-left py-3 transition flex gap-3 items-center cursor-pointer"
|
|
24
|
+
onClick={onClick}
|
|
25
|
+
>
|
|
26
|
+
<Image
|
|
27
|
+
src={place.thumbnail}
|
|
28
|
+
alt={place.name}
|
|
29
|
+
className="h-16 w-16 rounded-lg object-cover flex-none"
|
|
30
|
+
/>
|
|
31
|
+
<div className="min-w-0 text-left">
|
|
32
|
+
<div className="font-medium truncate">{place.name}</div>
|
|
33
|
+
<div className="text-xs text-black/50 truncate">
|
|
34
|
+
{place.description}
|
|
35
|
+
</div>
|
|
36
|
+
<div className="text-xs mt-1 text-black/50 flex items-center gap-1">
|
|
37
|
+
<Star className="h-3 w-3" aria-hidden="true" />
|
|
38
|
+
{place.rating.toFixed(1)}
|
|
39
|
+
{place.price ? <span className="">· {place.price}</span> : null}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function Sidebar({ places, selectedId, onSelect }) {
|
|
49
|
+
const [emblaRef] = useEmblaCarousel({ dragFree: true, loop: false });
|
|
50
|
+
const displayMode = useOpenAiGlobal("displayMode");
|
|
51
|
+
const forceMobile = displayMode !== "fullscreen";
|
|
52
|
+
const scrollRef = React.useRef(null);
|
|
53
|
+
const [showBottomFade, setShowBottomFade] = React.useState(false);
|
|
54
|
+
|
|
55
|
+
const updateBottomFadeVisibility = React.useCallback(() => {
|
|
56
|
+
const el = scrollRef.current;
|
|
57
|
+
if (!el) return;
|
|
58
|
+
const atBottom =
|
|
59
|
+
Math.ceil(el.scrollTop + el.clientHeight) >= el.scrollHeight;
|
|
60
|
+
setShowBottomFade(!atBottom);
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
updateBottomFadeVisibility();
|
|
65
|
+
const el = scrollRef.current;
|
|
66
|
+
if (!el) return;
|
|
67
|
+
const onScroll = () => updateBottomFadeVisibility();
|
|
68
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
69
|
+
window.addEventListener("resize", updateBottomFadeVisibility);
|
|
70
|
+
return () => {
|
|
71
|
+
el.removeEventListener("scroll", onScroll);
|
|
72
|
+
window.removeEventListener("resize", updateBottomFadeVisibility);
|
|
73
|
+
};
|
|
74
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
75
|
+
}, [places]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
{/* Desktop/Tablet sidebar */}
|
|
80
|
+
<div
|
|
81
|
+
className={`${
|
|
82
|
+
forceMobile ? "hidden" : ""
|
|
83
|
+
} absolute inset-y-0 bottom-4 left-0 z-20 w-[340px] max-w-[75%] pointer-events-auto`}
|
|
84
|
+
>
|
|
85
|
+
<div
|
|
86
|
+
ref={scrollRef}
|
|
87
|
+
className="relative px-2 h-full overflow-y-auto bg-white text-black"
|
|
88
|
+
>
|
|
89
|
+
<div className="flex justify-between flex-row items-center px-3 sticky bg-white top-0 py-4 text-md font-medium">
|
|
90
|
+
{places.length} results
|
|
91
|
+
<Button
|
|
92
|
+
variant="ghost"
|
|
93
|
+
color="secondary"
|
|
94
|
+
size="sm"
|
|
95
|
+
uniform
|
|
96
|
+
aria-label="Filter"
|
|
97
|
+
>
|
|
98
|
+
<Settings2 className="h-5 w-5" aria-hidden="true" />
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
101
|
+
<div>
|
|
102
|
+
{places.map((place) => (
|
|
103
|
+
<PlaceListItem
|
|
104
|
+
key={place.id}
|
|
105
|
+
place={place}
|
|
106
|
+
isSelected={
|
|
107
|
+
displayMode === "fullscreen" && selectedId === place.id
|
|
108
|
+
}
|
|
109
|
+
onClick={() => onSelect(place)}
|
|
110
|
+
/>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<AnimatePresence>
|
|
115
|
+
{showBottomFade && (
|
|
116
|
+
<motion.div
|
|
117
|
+
className="pointer-events-none absolute inset-x-0 bottom-0 h-9 z-10"
|
|
118
|
+
initial={{ opacity: 0 }}
|
|
119
|
+
animate={{ opacity: 1 }}
|
|
120
|
+
exit={{ opacity: 0 }}
|
|
121
|
+
transition={{ duration: 0.2 }}
|
|
122
|
+
>
|
|
123
|
+
<div
|
|
124
|
+
className="w-full h-full bg-gradient-to-t border-b border-black/50 from-black/15 to-black/0"
|
|
125
|
+
style={{
|
|
126
|
+
WebkitMaskImage:
|
|
127
|
+
"linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.25) 25%, rgba(0,0,0,0.25) 75%, rgba(0,0,0,0) 100%)",
|
|
128
|
+
maskImage:
|
|
129
|
+
"linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.25) 25%, rgba(0,0,0,0.25) 75%, rgba(0,0,0,0) 100%)",
|
|
130
|
+
}}
|
|
131
|
+
aria-hidden
|
|
132
|
+
/>
|
|
133
|
+
</motion.div>
|
|
134
|
+
)}
|
|
135
|
+
</AnimatePresence>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Mobile bottom carousel */}
|
|
139
|
+
<div
|
|
140
|
+
className={`${
|
|
141
|
+
forceMobile ? "" : "hidden"
|
|
142
|
+
} absolute inset-x-0 bottom-0 z-20 pointer-events-auto`}
|
|
143
|
+
>
|
|
144
|
+
<div className="pt-2 text-black">
|
|
145
|
+
<div className="overflow-hidden" ref={emblaRef}>
|
|
146
|
+
<div className="px-3 py-3 flex gap-3">
|
|
147
|
+
{places.map((place) => (
|
|
148
|
+
<div className="ring ring-black/10 max-w-[330px] w-full shadow-xl rounded-2xl bg-white">
|
|
149
|
+
<PlaceListItem
|
|
150
|
+
key={place.id}
|
|
151
|
+
place={place}
|
|
152
|
+
isSelected={
|
|
153
|
+
displayMode === "fullscreen" && selectedId === place.id
|
|
154
|
+
}
|
|
155
|
+
onClick={() => onSelect(place)}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import mapboxgl from "mapbox-gl";
|
|
3
|
+
import "mapbox-gl/dist/mapbox-gl.css";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import markers from "./markers.json";
|
|
6
|
+
import { AnimatePresence } from "framer-motion";
|
|
7
|
+
import Inspector from "./Inspector";
|
|
8
|
+
import Sidebar from "./Sidebar";
|
|
9
|
+
import { useOpenAiGlobal } from "../use-openai-global";
|
|
10
|
+
import { useMaxHeight } from "../use-max-height";
|
|
11
|
+
import { Maximize2 } from "lucide-react";
|
|
12
|
+
import {
|
|
13
|
+
useNavigate,
|
|
14
|
+
useLocation,
|
|
15
|
+
Routes,
|
|
16
|
+
Route,
|
|
17
|
+
BrowserRouter,
|
|
18
|
+
Outlet,
|
|
19
|
+
} from "react-router-dom";
|
|
20
|
+
import { Button } from "@openai/apps-sdk-ui/components/Button";
|
|
21
|
+
|
|
22
|
+
mapboxgl.accessToken =
|
|
23
|
+
"pk.eyJ1IjoiZXJpY25pbmciLCJhIjoiY21icXlubWM1MDRiczJvb2xwM2p0amNyayJ9.n-3O6JI5nOp_Lw96ZO5vJQ";
|
|
24
|
+
|
|
25
|
+
function fitMapToMarkers(map, coords) {
|
|
26
|
+
if (!map || !coords.length) return;
|
|
27
|
+
if (coords.length === 1) {
|
|
28
|
+
map.flyTo({ center: coords[0], zoom: 12 });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const bounds = coords.reduce(
|
|
32
|
+
(b, c) => b.extend(c),
|
|
33
|
+
new mapboxgl.LngLatBounds(coords[0], coords[0])
|
|
34
|
+
);
|
|
35
|
+
map.fitBounds(bounds, { padding: 60, animate: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function App() {
|
|
39
|
+
const mapRef = useRef(null);
|
|
40
|
+
const mapObj = useRef(null);
|
|
41
|
+
const markerObjs = useRef([]);
|
|
42
|
+
const places = markers?.places || [];
|
|
43
|
+
const markerCoords = places.map((p) => p.coords);
|
|
44
|
+
const navigate = useNavigate();
|
|
45
|
+
const location = useLocation();
|
|
46
|
+
const selectedId = React.useMemo(() => {
|
|
47
|
+
const match = location?.pathname?.match(/(?:^|\/)place\/([^/]+)/);
|
|
48
|
+
return match && match[1] ? match[1] : null;
|
|
49
|
+
}, [location?.pathname]);
|
|
50
|
+
const selectedPlace = places.find((p) => p.id === selectedId) || null;
|
|
51
|
+
const [viewState, setViewState] = useState(() => ({
|
|
52
|
+
center: markerCoords.length > 0 ? markerCoords[0] : [0, 0],
|
|
53
|
+
zoom: markerCoords.length > 0 ? 12 : 2,
|
|
54
|
+
}));
|
|
55
|
+
const displayMode = useOpenAiGlobal("displayMode");
|
|
56
|
+
const allowInspector = displayMode === "fullscreen";
|
|
57
|
+
const maxHeight = useMaxHeight() ?? undefined;
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (mapObj.current) return;
|
|
61
|
+
mapObj.current = new mapboxgl.Map({
|
|
62
|
+
container: mapRef.current,
|
|
63
|
+
style: "mapbox://styles/mapbox/streets-v12",
|
|
64
|
+
center: markerCoords.length > 0 ? markerCoords[0] : [0, 0],
|
|
65
|
+
zoom: markerCoords.length > 0 ? 12 : 2,
|
|
66
|
+
attributionControl: false,
|
|
67
|
+
});
|
|
68
|
+
addAllMarkers(places);
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
fitMapToMarkers(mapObj.current, markerCoords);
|
|
71
|
+
}, 0);
|
|
72
|
+
// after first paint
|
|
73
|
+
requestAnimationFrame(() => mapObj.current.resize());
|
|
74
|
+
|
|
75
|
+
// or keep it in sync with window resizes
|
|
76
|
+
window.addEventListener("resize", mapObj.current.resize);
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
window.removeEventListener("resize", mapObj.current.resize);
|
|
80
|
+
mapObj.current.remove();
|
|
81
|
+
};
|
|
82
|
+
// eslint-disable-next-line
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!mapObj.current) return;
|
|
87
|
+
const handler = () => {
|
|
88
|
+
const c = mapObj.current.getCenter();
|
|
89
|
+
setViewState({ center: [c.lng, c.lat], zoom: mapObj.current.getZoom() });
|
|
90
|
+
};
|
|
91
|
+
mapObj.current.on("moveend", handler);
|
|
92
|
+
return () => {
|
|
93
|
+
mapObj.current.off("moveend", handler);
|
|
94
|
+
};
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
function addAllMarkers(placesList) {
|
|
98
|
+
markerObjs.current.forEach((m) => m.remove());
|
|
99
|
+
markerObjs.current = [];
|
|
100
|
+
placesList.forEach((place) => {
|
|
101
|
+
const marker = new mapboxgl.Marker({
|
|
102
|
+
color: "#F46C21",
|
|
103
|
+
})
|
|
104
|
+
.setLngLat(place.coords)
|
|
105
|
+
.addTo(mapObj.current);
|
|
106
|
+
const el = marker.getElement();
|
|
107
|
+
if (el) {
|
|
108
|
+
el.style.cursor = "pointer";
|
|
109
|
+
el.addEventListener("click", () => {
|
|
110
|
+
navigate(`/place/${place.id}`);
|
|
111
|
+
panTo(place.coords, { offsetForInspector: true });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
markerObjs.current.push(marker);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getInspectorOffsetPx() {
|
|
119
|
+
if (displayMode !== "fullscreen") return 0;
|
|
120
|
+
if (typeof window === "undefined") return 0;
|
|
121
|
+
const isXlUp =
|
|
122
|
+
window.matchMedia && window.matchMedia("(min-width: 1280px)").matches;
|
|
123
|
+
const el = document.querySelector(".pizzaz-inspector");
|
|
124
|
+
const w = el ? el.getBoundingClientRect().width : 360;
|
|
125
|
+
const half = Math.round(w / 2);
|
|
126
|
+
// xl: inspector on right → negative x offset; lg: inspector on left → positive x offset
|
|
127
|
+
return isXlUp ? -half : half;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function panTo(
|
|
131
|
+
coord,
|
|
132
|
+
{ offsetForInspector } = { offsetForInspector: false }
|
|
133
|
+
) {
|
|
134
|
+
if (!mapObj.current) return;
|
|
135
|
+
const inspectorOffset = offsetForInspector ? getInspectorOffsetPx() : 0;
|
|
136
|
+
const flyOpts = {
|
|
137
|
+
center: coord,
|
|
138
|
+
zoom: 14,
|
|
139
|
+
speed: 1.2,
|
|
140
|
+
curve: 1.6,
|
|
141
|
+
};
|
|
142
|
+
if (inspectorOffset) {
|
|
143
|
+
flyOpts.offset = [inspectorOffset, 0];
|
|
144
|
+
}
|
|
145
|
+
mapObj.current.flyTo(flyOpts);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!mapObj.current) return;
|
|
150
|
+
addAllMarkers(places);
|
|
151
|
+
}, [places]);
|
|
152
|
+
|
|
153
|
+
// Pan the map when the selected place changes via routing
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!mapObj.current || !selectedPlace) return;
|
|
156
|
+
panTo(selectedPlace.coords, { offsetForInspector: true });
|
|
157
|
+
}, [selectedId]);
|
|
158
|
+
|
|
159
|
+
// Ensure Mapbox resizes when container maxHeight/display mode changes
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (!mapObj.current) return;
|
|
162
|
+
mapObj.current.resize();
|
|
163
|
+
}, [maxHeight, displayMode]);
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (
|
|
167
|
+
typeof window !== "undefined" &&
|
|
168
|
+
window.oai &&
|
|
169
|
+
typeof window.oai.widget.setState === "function"
|
|
170
|
+
) {
|
|
171
|
+
window.oai.widget.setState({
|
|
172
|
+
center: viewState.center,
|
|
173
|
+
zoom: viewState.zoom,
|
|
174
|
+
markers: markerCoords,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}, [viewState, markerCoords]);
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<>
|
|
181
|
+
<div
|
|
182
|
+
style={{
|
|
183
|
+
maxHeight,
|
|
184
|
+
height: displayMode === "fullscreen" ? maxHeight - 40 : 480,
|
|
185
|
+
}}
|
|
186
|
+
className={
|
|
187
|
+
"relative antialiased w-full min-h-[480px] overflow-hidden " +
|
|
188
|
+
(displayMode === "fullscreen"
|
|
189
|
+
? "rounded-none border-0"
|
|
190
|
+
: "border border-black/10 dark:border-white/10 rounded-2xl sm:rounded-3xl")
|
|
191
|
+
}
|
|
192
|
+
>
|
|
193
|
+
<Outlet />
|
|
194
|
+
{displayMode !== "fullscreen" && (
|
|
195
|
+
<Button
|
|
196
|
+
aria-label="Enter fullscreen"
|
|
197
|
+
className="absolute top-4 right-4 z-30 shadow-lg pointer-events-auto bg-white text-black"
|
|
198
|
+
color="secondary"
|
|
199
|
+
size="sm"
|
|
200
|
+
variant="soft"
|
|
201
|
+
uniform
|
|
202
|
+
onClick={() => {
|
|
203
|
+
if (selectedId) {
|
|
204
|
+
navigate("..", { replace: true });
|
|
205
|
+
}
|
|
206
|
+
if (window?.webplus?.requestDisplayMode) {
|
|
207
|
+
window.webplus.requestDisplayMode({ mode: "fullscreen" });
|
|
208
|
+
}
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
<Maximize2
|
|
212
|
+
strokeWidth={1.5}
|
|
213
|
+
className="h-4.5 w-4.5"
|
|
214
|
+
aria-hidden="true"
|
|
215
|
+
/>
|
|
216
|
+
</Button>
|
|
217
|
+
)}
|
|
218
|
+
{/* Sidebar */}
|
|
219
|
+
<Sidebar
|
|
220
|
+
places={places}
|
|
221
|
+
selectedId={selectedId}
|
|
222
|
+
onSelect={(place) => {
|
|
223
|
+
navigate(`/place/${place.id}`);
|
|
224
|
+
panTo(place.coords, { offsetForInspector: true });
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
|
|
228
|
+
{/* Inspector (right) */}
|
|
229
|
+
<AnimatePresence>
|
|
230
|
+
{allowInspector && selectedPlace && (
|
|
231
|
+
<Inspector
|
|
232
|
+
key={selectedPlace.id}
|
|
233
|
+
place={selectedPlace}
|
|
234
|
+
onClose={() => navigate("..")}
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
237
|
+
</AnimatePresence>
|
|
238
|
+
|
|
239
|
+
{/* Map */}
|
|
240
|
+
<div
|
|
241
|
+
className={
|
|
242
|
+
"absolute inset-0 overflow-hidden" +
|
|
243
|
+
(displayMode === "fullscreen"
|
|
244
|
+
? " left-[340px] right-2 top-2 bottom-4 border border-black/10 rounded-3xl"
|
|
245
|
+
: "")
|
|
246
|
+
}
|
|
247
|
+
>
|
|
248
|
+
<div
|
|
249
|
+
ref={mapRef}
|
|
250
|
+
className="w-full h-full absolute bottom-0 left-0 right-0"
|
|
251
|
+
style={{
|
|
252
|
+
maxHeight,
|
|
253
|
+
height: displayMode === "fullscreen" ? maxHeight : undefined,
|
|
254
|
+
}}
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* Suggestion chips (bottom, fullscreen) */}
|
|
260
|
+
{displayMode === "fullscreen" && (
|
|
261
|
+
<div className="hidden antialiased md:flex absolute inset-x-0 bottom-2 z-30 justify-center pointer-events-none">
|
|
262
|
+
<div className="flex gap-3 pointer-events-auto">
|
|
263
|
+
{["Open now", "Top rated", "Vegetarian friendly"].map((label) => (
|
|
264
|
+
<Button
|
|
265
|
+
key={label}
|
|
266
|
+
color="secondary"
|
|
267
|
+
variant="soft"
|
|
268
|
+
size="sm"
|
|
269
|
+
className="font-base"
|
|
270
|
+
>
|
|
271
|
+
{label}
|
|
272
|
+
</Button>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function RouterRoot() {
|
|
282
|
+
return (
|
|
283
|
+
<Routes>
|
|
284
|
+
<Route path="*" element={<App />}>
|
|
285
|
+
<Route path="place/:placeId" element={<></>} />
|
|
286
|
+
</Route>
|
|
287
|
+
</Routes>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
createRoot(document.getElementById("pizzaz-root")).render(
|
|
292
|
+
<BrowserRouter>
|
|
293
|
+
<RouterRoot />
|
|
294
|
+
</BrowserRouter>
|
|
295
|
+
);
|