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,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"places": [
|
|
3
|
+
{
|
|
4
|
+
"id": "nova-slice-lab",
|
|
5
|
+
"name": "Nova Slice Lab",
|
|
6
|
+
"coords": [-122.4098, 37.8001],
|
|
7
|
+
"description": "Award‑winning Neapolitan pies in North Beach.",
|
|
8
|
+
"city": "North Beach",
|
|
9
|
+
"rating": 4.8,
|
|
10
|
+
"price": "$$$",
|
|
11
|
+
"thumbnail": "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "midnight-marinara",
|
|
15
|
+
"name": "Midnight Marinara",
|
|
16
|
+
"coords": [-122.4093, 37.7990],
|
|
17
|
+
"description": "Focaccia‑style squares, late‑night favorite.",
|
|
18
|
+
"city": "North Beach",
|
|
19
|
+
"rating": 4.6,
|
|
20
|
+
"price": "$",
|
|
21
|
+
"thumbnail": "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "cinder-oven-co",
|
|
25
|
+
"name": "Cinder Oven Co.",
|
|
26
|
+
"coords": [-122.4255, 37.7613],
|
|
27
|
+
"description": "Thin‑crust classics on 18th Street.",
|
|
28
|
+
"city": "Mission",
|
|
29
|
+
"rating": 4.5,
|
|
30
|
+
"price": "$$",
|
|
31
|
+
"thumbnail": "https://persistent.oaistatic.com/pizzaz/pizzaz-3.png"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "neon-crust-works",
|
|
35
|
+
"name": "Neon Crust Works",
|
|
36
|
+
"coords": [-122.4388, 37.7775],
|
|
37
|
+
"description": "Deep‑dish and cornmeal crust favorites.",
|
|
38
|
+
"city": "Alamo Square",
|
|
39
|
+
"rating": 4.5,
|
|
40
|
+
"price": "$$",
|
|
41
|
+
"thumbnail": "https://persistent.oaistatic.com/pizzaz/pizzaz-6.png"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "luna-pie-collective",
|
|
45
|
+
"name": "Luna Pie Collective",
|
|
46
|
+
"coords": [-122.4077, 37.7990],
|
|
47
|
+
"description": "Wood‑fired pies and burrata in North Beach.",
|
|
48
|
+
"city": "North Beach",
|
|
49
|
+
"rating": 4.6,
|
|
50
|
+
"price": "$$",
|
|
51
|
+
"thumbnail": "https://persistent.oaistatic.com/pizzaz/pizzaz-4.png"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "bricklight-deep-dish",
|
|
55
|
+
"name": "Bricklight Deep Dish",
|
|
56
|
+
"coords": [-122.4097, 37.7992],
|
|
57
|
+
"description": "Chicago‑style pies from Tony Gemignani.",
|
|
58
|
+
"city": "North Beach",
|
|
59
|
+
"rating": 4.4,
|
|
60
|
+
"price": "$$$",
|
|
61
|
+
"thumbnail": "https://persistent.oaistatic.com/pizzaz/pizzaz-5.png"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "garden-ember-pies",
|
|
65
|
+
"name": "Garden Ember Pies",
|
|
66
|
+
"coords": [-122.4380, 37.7722],
|
|
67
|
+
"description": "Neighborhood spot with seasonal toppings.",
|
|
68
|
+
"city": "Lower Haight",
|
|
69
|
+
"rating": 4.4,
|
|
70
|
+
"price": "$$",
|
|
71
|
+
"thumbnail": "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "atlas-fire-pizzeria",
|
|
75
|
+
"name": "Atlas Fire Pizzeria",
|
|
76
|
+
"coords": [-122.4123, 37.7899],
|
|
77
|
+
"description": "Sourdough, wood‑fired pies near Nob Hill.",
|
|
78
|
+
"city": "Nob Hill",
|
|
79
|
+
"rating": 4.6,
|
|
80
|
+
"price": "$$$",
|
|
81
|
+
"thumbnail": "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "circuit-slice-garage",
|
|
85
|
+
"name": "Circuit Slice Garage",
|
|
86
|
+
"coords": [-122.4135, 37.7805],
|
|
87
|
+
"description": "Crispy‑edged Detroit‑style in SoMa.",
|
|
88
|
+
"city": "SoMa",
|
|
89
|
+
"rating": 4.5,
|
|
90
|
+
"price": "$$",
|
|
91
|
+
"thumbnail": "https://persistent.oaistatic.com/pizzaz/pizzaz-3.png"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "velvet-mozza-lounge",
|
|
95
|
+
"name": "Velvet Mozza Lounge",
|
|
96
|
+
"coords": [-122.4019, 37.7818],
|
|
97
|
+
"description": "Bianca pies and cocktails near Yerba Buena.",
|
|
98
|
+
"city": "Yerba Buena",
|
|
99
|
+
"rating": 4.3,
|
|
100
|
+
"price": "$$",
|
|
101
|
+
"thumbnail": "https://persistent.oaistatic.com/pizzaz/pizzaz-6.png"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button } from "@openai/apps-sdk-ui/components/Button";
|
|
3
|
+
import { Image } from "@openai/apps-sdk-ui/components/Image";
|
|
4
|
+
import { Badge } from "@openai/apps-sdk-ui/components/Badge";
|
|
5
|
+
|
|
6
|
+
function AlbumCard({ album, onSelect }) {
|
|
7
|
+
return (
|
|
8
|
+
<Button
|
|
9
|
+
type="button"
|
|
10
|
+
variant="ghost"
|
|
11
|
+
color="secondary"
|
|
12
|
+
pill={false}
|
|
13
|
+
className="group relative flex-shrink-0 w-[272px] bg-white text-left p-0 h-auto min-h-0 rounded-none shadow-none gap-0 before:hidden"
|
|
14
|
+
onClick={() => onSelect?.(album)}
|
|
15
|
+
>
|
|
16
|
+
<div className="flex w-full flex-col gap-2">
|
|
17
|
+
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-2xl shadow-lg">
|
|
18
|
+
<Image
|
|
19
|
+
src={album.cover}
|
|
20
|
+
alt={album.title}
|
|
21
|
+
className="h-full w-full object-cover"
|
|
22
|
+
loading="lazy"
|
|
23
|
+
/>
|
|
24
|
+
<Badge
|
|
25
|
+
variant="soft"
|
|
26
|
+
color="secondary"
|
|
27
|
+
size="sm"
|
|
28
|
+
pill
|
|
29
|
+
className="absolute left-3 top-3 bg-white/50 backdrop-blur-sm"
|
|
30
|
+
>
|
|
31
|
+
Featured
|
|
32
|
+
</Badge>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="px-1.5">
|
|
35
|
+
<div className="text-base font-medium truncate">{album.title}</div>
|
|
36
|
+
<div className="mt-0.5 text-sm font-normal text-black/60">
|
|
37
|
+
{album.photos.length} photos
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</Button>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default AlbumCard;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function FilmStrip({ album, selectedIndex, onSelect }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="h-full w-full overflow-auto flex flex-col items-center justify-center p-5 space-y-5">
|
|
6
|
+
{album.photos.map((photo, idx) => (
|
|
7
|
+
<button
|
|
8
|
+
key={photo.id}
|
|
9
|
+
type="button"
|
|
10
|
+
onClick={() => onSelect?.(idx)}
|
|
11
|
+
className={
|
|
12
|
+
"block w-full p-[1px] pointer-events-auto rounded-[10px] cursor-pointer border transition-[colors,opacity] " +
|
|
13
|
+
(idx === selectedIndex
|
|
14
|
+
? "border-black"
|
|
15
|
+
: "border-black/0 hover:border-black/30 opacity-60 hover:opacity-100")
|
|
16
|
+
}
|
|
17
|
+
>
|
|
18
|
+
<div className="aspect-[5/3] rounded-lg overflow-hidden w-full">
|
|
19
|
+
<img
|
|
20
|
+
src={photo.url}
|
|
21
|
+
alt={photo.title || `Photo ${idx + 1}`}
|
|
22
|
+
className="h-full w-full object-cover"
|
|
23
|
+
loading="lazy"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
</button>
|
|
27
|
+
))}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useMaxHeight } from "../use-max-height";
|
|
3
|
+
import FilmStrip from "./FilmStrip";
|
|
4
|
+
|
|
5
|
+
export default function FullscreenViewer({ album }) {
|
|
6
|
+
const maxHeight = useMaxHeight() ?? undefined;
|
|
7
|
+
const [index, setIndex] = React.useState(0);
|
|
8
|
+
|
|
9
|
+
React.useEffect(() => {
|
|
10
|
+
setIndex(0);
|
|
11
|
+
}, [album?.id]);
|
|
12
|
+
|
|
13
|
+
const photo = album?.photos?.[index];
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className="relative w-full h-full bg-white"
|
|
18
|
+
style={{
|
|
19
|
+
maxHeight,
|
|
20
|
+
height: maxHeight,
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
<div className="absolute inset-0 flex flex-row overflow-hidden">
|
|
24
|
+
{/* Film strip */}
|
|
25
|
+
<div className="hidden md:block absolute pointer-events-none z-10 left-0 top-0 bottom-0 w-40">
|
|
26
|
+
<FilmStrip album={album} selectedIndex={index} onSelect={setIndex} />
|
|
27
|
+
</div>
|
|
28
|
+
{/* Main photo */}
|
|
29
|
+
<div className="flex-1 min-w-0 px-40 py-10 relative flex items-center justify-center">
|
|
30
|
+
<div className="relative w-full h-full">
|
|
31
|
+
{photo ? (
|
|
32
|
+
<img
|
|
33
|
+
src={photo.url}
|
|
34
|
+
alt={photo.title || album.title}
|
|
35
|
+
className="absolute inset-0 m-auto rounded-3xl shadow-sm border border-black/10 max-w-full max-h-full object-contain"
|
|
36
|
+
/>
|
|
37
|
+
) : null}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"albums": [
|
|
3
|
+
{
|
|
4
|
+
"id": "summer-escape",
|
|
5
|
+
"title": "Summer Slice",
|
|
6
|
+
"cover": "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png",
|
|
7
|
+
"photos": [
|
|
8
|
+
{
|
|
9
|
+
"id": "s1",
|
|
10
|
+
"title": "Waves",
|
|
11
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "s2",
|
|
15
|
+
"title": "Palm trees",
|
|
16
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-3.png"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "s3",
|
|
20
|
+
"title": "Sunset",
|
|
21
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-6.png"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "city-lights",
|
|
27
|
+
"title": "Pepperoni Nights",
|
|
28
|
+
"cover": "https://persistent.oaistatic.com/pizzaz/pizzaz-4.png",
|
|
29
|
+
"photos": [
|
|
30
|
+
{
|
|
31
|
+
"id": "c1",
|
|
32
|
+
"title": "Downtown",
|
|
33
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-5.png"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "c2",
|
|
37
|
+
"title": "Neon",
|
|
38
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "c3",
|
|
42
|
+
"title": "Streets",
|
|
43
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "into-the-woods",
|
|
49
|
+
"title": "Truffle Forest",
|
|
50
|
+
"cover": "https://persistent.oaistatic.com/pizzaz/pizzaz-3.png",
|
|
51
|
+
"photos": [
|
|
52
|
+
{
|
|
53
|
+
"id": "n1",
|
|
54
|
+
"title": "Forest path",
|
|
55
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-6.png"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "n2",
|
|
59
|
+
"title": "Misty",
|
|
60
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-4.png"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "n3",
|
|
64
|
+
"title": "Waterfall",
|
|
65
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-5.png"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "pizza-tour",
|
|
71
|
+
"title": "Pizza tour",
|
|
72
|
+
"cover": "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png",
|
|
73
|
+
"photos": [
|
|
74
|
+
{
|
|
75
|
+
"id": "tonys-pizza-napoletana",
|
|
76
|
+
"title": "Tony's Pizza Napoletana",
|
|
77
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "golden-boy-pizza",
|
|
81
|
+
"title": "Golden Boy Pizza",
|
|
82
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-3.png"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "pizzeria-delfina-mission",
|
|
86
|
+
"title": "Pizzeria Delfina (Mission)",
|
|
87
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-6.png"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "ragazza",
|
|
91
|
+
"title": "Ragazza",
|
|
92
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-4.png"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "del-popolo",
|
|
96
|
+
"title": "Del Popolo",
|
|
97
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-5.png"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "square-pie-guys",
|
|
101
|
+
"title": "Square Pie Guys",
|
|
102
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "zero-zero",
|
|
106
|
+
"title": "Zero Zero",
|
|
107
|
+
"url": "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import useEmblaCarousel from "embla-carousel-react";
|
|
4
|
+
import { ArrowLeft, ArrowRight } from "lucide-react";
|
|
5
|
+
import albumsData from "./albums.json";
|
|
6
|
+
import { useMaxHeight } from "../use-max-height";
|
|
7
|
+
import { useOpenAiGlobal } from "../use-openai-global";
|
|
8
|
+
import FullscreenViewer from "./FullscreenViewer";
|
|
9
|
+
import AlbumCard from "./AlbumCard";
|
|
10
|
+
import { Button } from "@openai/apps-sdk-ui/components/Button";
|
|
11
|
+
|
|
12
|
+
function AlbumsCarousel({ onSelect }) {
|
|
13
|
+
const albums = albumsData?.albums || [];
|
|
14
|
+
const [emblaRef, emblaApi] = useEmblaCarousel({
|
|
15
|
+
align: "center",
|
|
16
|
+
loop: false,
|
|
17
|
+
containScroll: "trimSnaps",
|
|
18
|
+
slidesToScroll: "auto",
|
|
19
|
+
dragFree: false,
|
|
20
|
+
});
|
|
21
|
+
const [canPrev, setCanPrev] = React.useState(false);
|
|
22
|
+
const [canNext, setCanNext] = React.useState(false);
|
|
23
|
+
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
if (!emblaApi) return;
|
|
26
|
+
const updateButtons = () => {
|
|
27
|
+
setCanPrev(emblaApi.canScrollPrev());
|
|
28
|
+
setCanNext(emblaApi.canScrollNext());
|
|
29
|
+
};
|
|
30
|
+
updateButtons();
|
|
31
|
+
emblaApi.on("select", updateButtons);
|
|
32
|
+
emblaApi.on("reInit", updateButtons);
|
|
33
|
+
return () => {
|
|
34
|
+
emblaApi.off("select", updateButtons);
|
|
35
|
+
emblaApi.off("reInit", updateButtons);
|
|
36
|
+
};
|
|
37
|
+
}, [emblaApi]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="antialiased relative w-full text-black py-5 select-none">
|
|
41
|
+
<div className="overflow-hidden max-sm:mx-5" ref={emblaRef}>
|
|
42
|
+
<div className="flex gap-5 items-stretch">
|
|
43
|
+
{albums.map((album) => (
|
|
44
|
+
<AlbumCard key={album.id} album={album} onSelect={onSelect} />
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div
|
|
49
|
+
aria-hidden
|
|
50
|
+
className={
|
|
51
|
+
"pointer-events-none absolute inset-y-0 left-0 w-3 z-[5] transition-opacity duration-200 " +
|
|
52
|
+
(canPrev ? "opacity-100" : "opacity-0")
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
<div
|
|
56
|
+
className="h-full w-full border-l border-black/15 bg-gradient-to-r from-black/10 to-transparent"
|
|
57
|
+
style={{
|
|
58
|
+
WebkitMaskImage:
|
|
59
|
+
"linear-gradient(to bottom, transparent 0%, white 30%, white 70%, transparent 100%)",
|
|
60
|
+
maskImage:
|
|
61
|
+
"linear-gradient(to bottom, transparent 0%, white 30%, white 70%, transparent 100%)",
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
<div
|
|
66
|
+
aria-hidden
|
|
67
|
+
className={
|
|
68
|
+
"pointer-events-none absolute inset-y-0 right-0 w-3 z-[5] transition-opacity duration-200 " +
|
|
69
|
+
(canNext ? "opacity-100" : "opacity-0")
|
|
70
|
+
}
|
|
71
|
+
>
|
|
72
|
+
<div
|
|
73
|
+
className="h-full w-full border-r border-black/15 bg-gradient-to-l from-black/10 to-transparent"
|
|
74
|
+
style={{
|
|
75
|
+
WebkitMaskImage:
|
|
76
|
+
"linear-gradient(to bottom, transparent 0%, white 30%, white 70%, transparent 100%)",
|
|
77
|
+
maskImage:
|
|
78
|
+
"linear-gradient(to bottom, transparent 0%, white 30%, white 70%, transparent 100%)",
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
{canPrev && (
|
|
83
|
+
<Button
|
|
84
|
+
aria-label="Previous"
|
|
85
|
+
className="absolute left-2 top-1/2 -translate-y-1/2 z-10 shadow-lg"
|
|
86
|
+
color="secondary"
|
|
87
|
+
size="sm"
|
|
88
|
+
variant="soft"
|
|
89
|
+
uniform
|
|
90
|
+
onClick={() => emblaApi && emblaApi.scrollPrev()}
|
|
91
|
+
type="button"
|
|
92
|
+
>
|
|
93
|
+
<ArrowLeft
|
|
94
|
+
strokeWidth={1.5}
|
|
95
|
+
className="h-4.5 w-4.5"
|
|
96
|
+
aria-hidden="true"
|
|
97
|
+
/>
|
|
98
|
+
</Button>
|
|
99
|
+
)}
|
|
100
|
+
{canNext && (
|
|
101
|
+
<Button
|
|
102
|
+
aria-label="Next"
|
|
103
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 z-10 shadow-lg"
|
|
104
|
+
color="secondary"
|
|
105
|
+
size="sm"
|
|
106
|
+
variant="soft"
|
|
107
|
+
uniform
|
|
108
|
+
onClick={() => emblaApi && emblaApi.scrollNext()}
|
|
109
|
+
type="button"
|
|
110
|
+
>
|
|
111
|
+
<ArrowRight
|
|
112
|
+
strokeWidth={1.5}
|
|
113
|
+
className="h-4.5 w-4.5"
|
|
114
|
+
aria-hidden="true"
|
|
115
|
+
/>
|
|
116
|
+
</Button>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function App() {
|
|
123
|
+
const displayMode = useOpenAiGlobal("displayMode");
|
|
124
|
+
const [selectedAlbum, setSelectedAlbum] = React.useState(null);
|
|
125
|
+
const maxHeight = useMaxHeight() ?? undefined;
|
|
126
|
+
|
|
127
|
+
const handleSelectAlbum = (album) => {
|
|
128
|
+
setSelectedAlbum(album);
|
|
129
|
+
if (window?.webplus?.requestDisplayMode) {
|
|
130
|
+
window.webplus.requestDisplayMode({ mode: "fullscreen" });
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
className="relative antialiased w-full"
|
|
137
|
+
style={{
|
|
138
|
+
maxHeight,
|
|
139
|
+
height: displayMode === "fullscreen" ? maxHeight : undefined,
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{displayMode !== "fullscreen" && (
|
|
143
|
+
<AlbumsCarousel onSelect={handleSelectAlbum} />
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{displayMode === "fullscreen" && selectedAlbum && (
|
|
147
|
+
<FullscreenViewer album={selectedAlbum} />
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
createRoot(document.getElementById("pizzaz-albums-root")).render(<App />);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Star } from "lucide-react";
|
|
3
|
+
import { Button } from "@openai/apps-sdk-ui/components/Button";
|
|
4
|
+
import { Image } from "@openai/apps-sdk-ui/components/Image";
|
|
5
|
+
|
|
6
|
+
export default function PlaceCard({ place }) {
|
|
7
|
+
if (!place) return null;
|
|
8
|
+
return (
|
|
9
|
+
<div className="min-w-[220px] select-none max-w-[220px] w-[65vw] sm:w-[220px] self-stretch flex flex-col">
|
|
10
|
+
<div className="w-full">
|
|
11
|
+
<Image
|
|
12
|
+
src={place.thumbnail}
|
|
13
|
+
alt={place.name}
|
|
14
|
+
className="w-full aspect-square rounded-2xl object-cover ring ring-black/5 shadow-[0px_2px_6px_rgba(0,0,0,0.06)]"
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
<div className="mt-3 flex flex-col flex-1">
|
|
18
|
+
<div className="text-base font-medium truncate line-clamp-1">
|
|
19
|
+
{place.name}
|
|
20
|
+
</div>
|
|
21
|
+
<div className="text-xs mt-1 text-black/60 flex items-center gap-1">
|
|
22
|
+
<Star className="h-3 w-3" aria-hidden="true" />
|
|
23
|
+
{place.rating?.toFixed ? place.rating.toFixed(1) : place.rating}
|
|
24
|
+
{place.price ? <span>· {place.price}</span> : null}
|
|
25
|
+
<span>· San Francisco</span>
|
|
26
|
+
</div>
|
|
27
|
+
{place.description ? (
|
|
28
|
+
<div className="text-sm mt-2 text-black/80 flex-auto">
|
|
29
|
+
{place.description}
|
|
30
|
+
</div>
|
|
31
|
+
) : null}
|
|
32
|
+
<div className="mt-5">
|
|
33
|
+
<Button color="primary" size="sm" variant="solid">
|
|
34
|
+
Learn more
|
|
35
|
+
</Button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../index.css";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import useEmblaCarousel from "embla-carousel-react";
|
|
5
|
+
import { ArrowLeft, ArrowRight } from "lucide-react";
|
|
6
|
+
import markers from "../pizzaz/markers.json";
|
|
7
|
+
import PlaceCard from "./PlaceCard";
|
|
8
|
+
import { Button } from "@openai/apps-sdk-ui/components/Button";
|
|
9
|
+
|
|
10
|
+
function App() {
|
|
11
|
+
const places = markers?.places || [];
|
|
12
|
+
const [emblaRef, emblaApi] = useEmblaCarousel({
|
|
13
|
+
align: "center",
|
|
14
|
+
loop: false,
|
|
15
|
+
containScroll: "trimSnaps",
|
|
16
|
+
slidesToScroll: "auto",
|
|
17
|
+
dragFree: false,
|
|
18
|
+
});
|
|
19
|
+
const [canPrev, setCanPrev] = React.useState(false);
|
|
20
|
+
const [canNext, setCanNext] = React.useState(false);
|
|
21
|
+
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
if (!emblaApi) return;
|
|
24
|
+
const updateButtons = () => {
|
|
25
|
+
setCanPrev(emblaApi.canScrollPrev());
|
|
26
|
+
setCanNext(emblaApi.canScrollNext());
|
|
27
|
+
};
|
|
28
|
+
updateButtons();
|
|
29
|
+
emblaApi.on("select", updateButtons);
|
|
30
|
+
emblaApi.on("reInit", updateButtons);
|
|
31
|
+
return () => {
|
|
32
|
+
emblaApi.off("select", updateButtons);
|
|
33
|
+
emblaApi.off("reInit", updateButtons);
|
|
34
|
+
};
|
|
35
|
+
}, [emblaApi]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="antialiased relative w-full text-black py-5 bg-white">
|
|
39
|
+
<div className="overflow-hidden" ref={emblaRef}>
|
|
40
|
+
<div className="flex gap-4 max-sm:mx-5 items-stretch">
|
|
41
|
+
{places.map((place) => (
|
|
42
|
+
<PlaceCard key={place.id} place={place} />
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
{/* Edge gradients */}
|
|
47
|
+
<div
|
|
48
|
+
aria-hidden
|
|
49
|
+
className={
|
|
50
|
+
"pointer-events-none absolute inset-y-0 left-0 w-3 z-[5] transition-opacity duration-200 " +
|
|
51
|
+
(canPrev ? "opacity-100" : "opacity-0")
|
|
52
|
+
}
|
|
53
|
+
>
|
|
54
|
+
<div
|
|
55
|
+
className="h-full w-full border-l border-black/15 bg-gradient-to-r from-black/10 to-transparent"
|
|
56
|
+
style={{
|
|
57
|
+
WebkitMaskImage:
|
|
58
|
+
"linear-gradient(to bottom, transparent 0%, white 30%, white 70%, transparent 100%)",
|
|
59
|
+
maskImage:
|
|
60
|
+
"linear-gradient(to bottom, transparent 0%, white 30%, white 70%, transparent 100%)",
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
<div
|
|
65
|
+
aria-hidden
|
|
66
|
+
className={
|
|
67
|
+
"pointer-events-none absolute inset-y-0 right-0 w-3 z-[5] transition-opacity duration-200 " +
|
|
68
|
+
(canNext ? "opacity-100" : "opacity-0")
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
<div
|
|
72
|
+
className="h-full w-full border-r border-black/15 bg-gradient-to-l from-black/10 to-transparent"
|
|
73
|
+
style={{
|
|
74
|
+
WebkitMaskImage:
|
|
75
|
+
"linear-gradient(to bottom, transparent 0%, white 30%, white 70%, transparent 100%)",
|
|
76
|
+
maskImage:
|
|
77
|
+
"linear-gradient(to bottom, transparent 0%, white 30%, white 70%, transparent 100%)",
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
{canPrev && (
|
|
82
|
+
<Button
|
|
83
|
+
aria-label="Previous"
|
|
84
|
+
className="absolute left-2 top-1/2 -translate-y-1/2 z-10 shadow-lg"
|
|
85
|
+
color="secondary"
|
|
86
|
+
size="sm"
|
|
87
|
+
variant="soft"
|
|
88
|
+
uniform
|
|
89
|
+
onClick={() => emblaApi && emblaApi.scrollPrev()}
|
|
90
|
+
type="button"
|
|
91
|
+
>
|
|
92
|
+
<ArrowLeft
|
|
93
|
+
strokeWidth={1.5}
|
|
94
|
+
className="h-4.5 w-4.5"
|
|
95
|
+
aria-hidden="true"
|
|
96
|
+
/>
|
|
97
|
+
</Button>
|
|
98
|
+
)}
|
|
99
|
+
{canNext && (
|
|
100
|
+
<Button
|
|
101
|
+
aria-label="Next"
|
|
102
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 z-10 shadow-lg"
|
|
103
|
+
color="secondary"
|
|
104
|
+
size="sm"
|
|
105
|
+
variant="soft"
|
|
106
|
+
uniform
|
|
107
|
+
onClick={() => emblaApi && emblaApi.scrollNext()}
|
|
108
|
+
type="button"
|
|
109
|
+
>
|
|
110
|
+
<ArrowRight
|
|
111
|
+
strokeWidth={1.5}
|
|
112
|
+
className="h-4.5 w-4.5"
|
|
113
|
+
aria-hidden="true"
|
|
114
|
+
/>
|
|
115
|
+
</Button>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
createRoot(document.getElementById("pizzaz-carousel-root")).render(<App />);
|