sunpeak 0.4.2 → 0.4.4

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.
Files changed (31) hide show
  1. package/README.md +2 -2
  2. package/bin/sunpeak.js +1 -1
  3. package/dist/index.cjs +4 -4
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +4 -4
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/template/README.md +1 -1
  9. package/template/dev/main.tsx +1 -1
  10. package/template/mcp/server.ts +5 -9
  11. package/template/src/components/album/albums.tsx +7 -7
  12. package/template/src/components/apps/AlbumsApp.tsx +13 -0
  13. package/template/src/{App.tsx → components/apps/App.tsx} +2 -2
  14. package/template/src/components/{simulations/carousel-simulation.tsx → apps/PlacesApp.tsx} +11 -17
  15. package/template/src/components/apps/active-app.ts +11 -0
  16. package/template/src/components/apps/index.ts +3 -0
  17. package/template/src/components/card/card.tsx +4 -0
  18. package/template/src/components/index.ts +1 -1
  19. package/template/src/index.chatgpt.tsx +4 -3
  20. package/template/src/index.ts +0 -1
  21. package/template/src/simulations/albums-simulation.ts +129 -0
  22. package/template/src/simulations/app-configs.ts +30 -0
  23. package/template/src/simulations/app-simulation.ts +15 -0
  24. package/template/src/simulations/carousel-simulation.ts +66 -0
  25. package/template/src/simulations/index.ts +16 -0
  26. package/template/src/simulations/simulations.ts +74 -0
  27. package/template/data/albums.json +0 -112
  28. package/template/data/places.json +0 -49
  29. package/template/src/components/simulations/albums-simulation.tsx +0 -20
  30. package/template/src/components/simulations/app-simulation.tsx +0 -13
  31. package/template/src/components/simulations/index.tsx +0 -14
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sunpeak",
3
- "version": "0.4.2",
4
- "description": "The ChatGPT Apps UI SDK. Build and test your ChatGPT App UI locally with OpenAI apps-sdk-ui components.",
3
+ "version": "0.4.4",
4
+ "description": "The MCP App SDK. Quickstart, build, & test your ChatGPT App locally!",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -10,7 +10,7 @@ For an initial overview of your new app and the sunpeak API, refer to the [docum
10
10
  pnpm dev
11
11
  ```
12
12
 
13
- Edit [src/App.tsx](./src/App.tsx) to build your app UI.
13
+ Edit [./src/components/apps/App.tsx](./src/components/apps/App.tsx) to build your app UI.
14
14
 
15
15
  ## Development
16
16
 
@@ -3,7 +3,7 @@ import '../src/styles/globals.css';
3
3
  import { StrictMode } from 'react';
4
4
  import { createRoot } from 'react-dom/client';
5
5
  import { ChatGPTSimulator } from 'sunpeak';
6
- import { simulations } from '../src/components';
6
+ import { simulations } from '../src/simulations';
7
7
 
8
8
  createRoot(document.getElementById('root')!).render(
9
9
  <StrictMode>
@@ -1,20 +1,16 @@
1
1
  import { runMCPServer } from 'sunpeak/mcp';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import { readFileSync } from 'fs';
4
+ import { activeConfig } from '../src/simulations/app-configs.js';
5
5
 
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
 
8
- // Load places data from JSON file
9
- const placesDataPath = path.resolve(__dirname, '../data/places.json');
10
- const toolOutput = JSON.parse(readFileSync(placesDataPath, 'utf-8'));
11
-
12
8
  runMCPServer({
13
- name: 'my-sunpeak-app',
9
+ name: activeConfig.appName,
14
10
  version: '0.1.0',
15
11
  distPath: path.resolve(__dirname, '../dist/chatgpt/index.js'),
16
- toolName: 'show-places',
17
- toolDescription: 'Show popular places in Austin',
18
- dummyData: toolOutput,
12
+ toolName: activeConfig.toolName,
13
+ toolDescription: activeConfig.toolDescription,
14
+ dummyData: activeConfig.toolOutput ?? {},
19
15
  port: 6766,
20
16
  });
@@ -15,22 +15,22 @@ export interface Album {
15
15
  }>
16
16
  }
17
17
 
18
- export interface OpenAIAlbumsData extends Record<string, unknown> {
18
+ export interface AlbumsData extends Record<string, unknown> {
19
19
  albums: Album[]
20
20
  }
21
21
 
22
- export interface OpenAIAlbumsState extends Record<string, unknown> {
22
+ export interface AlbumsState extends Record<string, unknown> {
23
23
  selectedAlbumId?: string | null
24
24
  }
25
25
 
26
- export type OpenAIAlbumsProps = {
26
+ export type AlbumsProps = {
27
27
  className?: string
28
28
  }
29
29
 
30
- export const OpenAIAlbums = React.forwardRef<HTMLDivElement, OpenAIAlbumsProps>(
30
+ export const Albums = React.forwardRef<HTMLDivElement, AlbumsProps>(
31
31
  ({ className }, ref) => {
32
- const data = useWidgetProps<OpenAIAlbumsData>(() => ({ albums: [] }))
33
- const [widgetState, setWidgetState] = useWidgetState<OpenAIAlbumsState>(() => ({
32
+ const data = useWidgetProps<AlbumsData>(() => ({ albums: [] }))
33
+ const [widgetState, setWidgetState] = useWidgetState<AlbumsState>(() => ({
34
34
  selectedAlbumId: null,
35
35
  }))
36
36
  const displayMode = useDisplayMode()
@@ -74,4 +74,4 @@ export const OpenAIAlbums = React.forwardRef<HTMLDivElement, OpenAIAlbumsProps>(
74
74
  )
75
75
  }
76
76
  )
77
- OpenAIAlbums.displayName = "OpenAIAlbums"
77
+ Albums.displayName = "Albums"
@@ -0,0 +1,13 @@
1
+ import * as React from "react"
2
+ import { Albums } from "../album"
3
+
4
+ /**
5
+ * Production-ready Albums App
6
+ *
7
+ * This app displays photo albums in a carousel layout with fullscreen viewing capability.
8
+ * Can be dropped into any production environment without changes.
9
+ */
10
+ export const AlbumsApp = React.forwardRef<HTMLDivElement>((_props, ref) => {
11
+ return <Albums ref={ref} />
12
+ })
13
+ AlbumsApp.displayName = "AlbumsApp"
@@ -1,4 +1,4 @@
1
- import './styles/globals.css';
1
+ import '../../styles/globals.css';
2
2
 
3
3
  import { useWidgetState } from 'sunpeak';
4
4
  import { Button } from '@openai/apps-sdk-ui/components/Button';
@@ -18,7 +18,7 @@ interface CounterState extends Record<string, unknown> {
18
18
  * - Check out the components folder for reusable components
19
19
  * - Use sunpeak hooks for state management and display modes
20
20
  * - Edit this file and see your changes live
21
- * - Edit ./components/simulations/app-simulation.tsx to customize your simulation
21
+ * - Edit ../../simulations/app-simulation.tsx to customize your simulation
22
22
  */
23
23
  export function App() {
24
24
  const [widgetState, setWidgetState] = useWidgetState<CounterState>(() => ({
@@ -1,8 +1,13 @@
1
1
  import * as React from "react"
2
- import type { Simulation } from "sunpeak"
3
2
  import { useWidgetProps } from "sunpeak"
4
3
  import { Carousel, Card } from ".."
5
- import placesData from "../../../data/places.json"
4
+
5
+ /**
6
+ * Production-ready Places App
7
+ *
8
+ * This app displays places in a carousel layout with cards.
9
+ * Can be dropped into any production environment without changes.
10
+ */
6
11
 
7
12
  export interface Place {
8
13
  id: string
@@ -14,12 +19,12 @@ export interface Place {
14
19
  description: string
15
20
  }
16
21
 
17
- export interface CarouselSimulationData extends Record<string, unknown> {
22
+ export interface PlacesData extends Record<string, unknown> {
18
23
  places: Place[]
19
24
  }
20
25
 
21
- const CarouselComponent = React.forwardRef<HTMLDivElement>((_props, ref) => {
22
- const data = useWidgetProps<CarouselSimulationData>(() => ({ places: [] }))
26
+ export const PlacesApp = React.forwardRef<HTMLDivElement>((_props, ref) => {
27
+ const data = useWidgetProps<PlacesData>(() => ({ places: [] }))
23
28
 
24
29
  return (
25
30
  <div ref={ref}>
@@ -49,15 +54,4 @@ const CarouselComponent = React.forwardRef<HTMLDivElement>((_props, ref) => {
49
54
  </div>
50
55
  )
51
56
  })
52
- CarouselComponent.displayName = "CarouselComponent"
53
-
54
- export const carouselSimulation: Simulation = {
55
- value: 'carousel',
56
- label: 'Carousel',
57
- component: CarouselComponent,
58
- appName: 'Splorin',
59
- appIcon: '✈️',
60
- userMessage: 'Show me popular places to visit in Austin Texas',
61
- toolOutput: placesData,
62
- widgetState: null,
63
- }
57
+ PlacesApp.displayName = "PlacesApp"
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Active app selection - SINGLE SOURCE OF TRUTH
3
+ *
4
+ * 👇 CHANGE THIS LINE to switch which app is active for the:
5
+ * - ChatGPT build (index.chatgpt.tsx)
6
+ * - MCP server (server.ts)
7
+ */
8
+
9
+ export type AppName = 'app' | 'albums' | 'carousel';
10
+
11
+ export const ACTIVE_APP: AppName = 'app';
@@ -0,0 +1,3 @@
1
+ export { App } from './App';
2
+ export { AlbumsApp } from './AlbumsApp';
3
+ export { PlacesApp, type Place, type PlacesData } from './PlacesApp';
@@ -79,6 +79,10 @@ export const Card = React.forwardRef<HTMLDivElement, CardProps>(
79
79
  variantClasses[variant],
80
80
  className
81
81
  )}
82
+ style={{
83
+ maxWidth: image ? `${imageMaxWidth}px` : undefined,
84
+ ...props.style,
85
+ }}
82
86
  onClick={handleCardClick}
83
87
  {...props}
84
88
  >
@@ -1,4 +1,4 @@
1
1
  export * from './card';
2
2
  export * from './carousel';
3
3
  export * from './album';
4
- export * from './simulations';
4
+ export * from './apps';
@@ -1,8 +1,9 @@
1
1
  import { createRoot } from 'react-dom/client';
2
- import { App } from './App';
2
+ import { ActiveComponent as ActiveApp } from './simulations/simulations';
3
3
 
4
- // Mount the App to the root element
4
+ // Mount the active app
5
+ // To switch apps, edit ACTIVE_APP in components/apps/active-app.ts
5
6
  const root = document.getElementById('root');
6
7
  if (root) {
7
- createRoot(root).render(<App />);
8
+ createRoot(root).render(<ActiveApp />);
8
9
  }
@@ -1,3 +1,2 @@
1
1
  export * from './components';
2
2
  export * from './lib';
3
- export * from './App'
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Server-safe configuration for the albums simulation.
3
+ * This file contains only metadata and doesn't import React components or CSS.
4
+ */
5
+
6
+ const albumsData = {
7
+ albums: [
8
+ {
9
+ id: "summer-escape",
10
+ title: "Summer Slice",
11
+ cover: "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png",
12
+ photos: [
13
+ {
14
+ id: "s1",
15
+ title: "Waves",
16
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
17
+ },
18
+ {
19
+ id: "s2",
20
+ title: "Palm trees",
21
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-3.png"
22
+ },
23
+ {
24
+ id: "s3",
25
+ title: "Sunset",
26
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-6.png"
27
+ }
28
+ ]
29
+ },
30
+ {
31
+ id: "city-lights",
32
+ title: "Pepperoni Nights",
33
+ cover: "https://persistent.oaistatic.com/pizzaz/pizzaz-4.png",
34
+ photos: [
35
+ {
36
+ id: "c1",
37
+ title: "Downtown",
38
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-5.png"
39
+ },
40
+ {
41
+ id: "c2",
42
+ title: "Neon",
43
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png"
44
+ },
45
+ {
46
+ id: "c3",
47
+ title: "Streets",
48
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
49
+ }
50
+ ]
51
+ },
52
+ {
53
+ id: "into-the-woods",
54
+ title: "Truffle Forest",
55
+ cover: "https://persistent.oaistatic.com/pizzaz/pizzaz-3.png",
56
+ photos: [
57
+ {
58
+ id: "n1",
59
+ title: "Forest path",
60
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-6.png"
61
+ },
62
+ {
63
+ id: "n2",
64
+ title: "Misty",
65
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-4.png"
66
+ },
67
+ {
68
+ id: "n3",
69
+ title: "Waterfall",
70
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-5.png"
71
+ }
72
+ ]
73
+ },
74
+ {
75
+ id: "pizza-tour",
76
+ title: "Pizza tour",
77
+ cover: "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png",
78
+ photos: [
79
+ {
80
+ id: "tonys-pizza-napoletana",
81
+ title: "Tony's Pizza Napoletana",
82
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
83
+ },
84
+ {
85
+ id: "golden-boy-pizza",
86
+ title: "Golden Boy Pizza",
87
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-3.png"
88
+ },
89
+ {
90
+ id: "pizzeria-delfina-mission",
91
+ title: "Pizzeria Delfina (Mission)",
92
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-6.png"
93
+ },
94
+ {
95
+ id: "ragazza",
96
+ title: "Ragazza",
97
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-4.png"
98
+ },
99
+ {
100
+ id: "del-popolo",
101
+ title: "Del Popolo",
102
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-5.png"
103
+ },
104
+ {
105
+ id: "square-pie-guys",
106
+ title: "Square Pie Guys",
107
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-1.png"
108
+ },
109
+ {
110
+ id: "zero-zero",
111
+ title: "Zero Zero",
112
+ url: "https://persistent.oaistatic.com/pizzaz/pizzaz-2.png"
113
+ }
114
+ ]
115
+ }
116
+ ]
117
+ };
118
+
119
+ export const albumsSimulationConfig = {
120
+ appName: 'Pizzaz',
121
+ value: 'albums',
122
+ label: 'Albums',
123
+ appIcon: '🍕',
124
+ userMessage: 'Pizza time',
125
+ toolOutput: albumsData,
126
+ widgetState: null,
127
+ toolName: 'show-app',
128
+ toolDescription: 'Show photo albums',
129
+ } as const;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Server-safe app configurations
3
+ *
4
+ * This file contains only metadata and can be safely imported in Node.js contexts
5
+ * (like MCP servers) without causing issues with CSS imports or React components.
6
+ *
7
+ * To switch apps, change ACTIVE_APP in components/apps/active-app.ts
8
+ */
9
+
10
+ import { ACTIVE_APP } from '../components/apps/active-app';
11
+ import { appSimulationConfig } from './app-simulation';
12
+ import { albumsSimulationConfig } from './albums-simulation';
13
+ import { carouselSimulationConfig } from './carousel-simulation';
14
+
15
+ /**
16
+ * Server-safe config map - contains only metadata, no React components
17
+ */
18
+ export const CONFIG_MAP = {
19
+ app: appSimulationConfig,
20
+ albums: albumsSimulationConfig,
21
+ carousel: carouselSimulationConfig,
22
+ } as const;
23
+
24
+ export type AppName = keyof typeof CONFIG_MAP;
25
+
26
+ // Re-export for convenience
27
+ export { ACTIVE_APP };
28
+
29
+ // Active config for server use
30
+ export const activeConfig = CONFIG_MAP[ACTIVE_APP];
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Server-safe configuration for the app simulation.
3
+ * This file contains only metadata and doesn't import React components or CSS.
4
+ */
5
+ export const appSimulationConfig = {
6
+ appName: 'sunpeak',
7
+ value: 'app',
8
+ label: 'My App',
9
+ appIcon: '🏗️',
10
+ userMessage: 'What do you have for me today?',
11
+ toolOutput: null,
12
+ widgetState: null,
13
+ toolName: 'show-app',
14
+ toolDescription: 'Show the Sunpeak app',
15
+ } as const;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Server-safe configuration for the carousel simulation.
3
+ * This file contains only metadata and doesn't import React components or CSS.
4
+ */
5
+
6
+ const placesData = {
7
+ places: [
8
+ {
9
+ id: "1",
10
+ name: "Lady Bird Lake",
11
+ rating: 4.5,
12
+ category: "Waterfront",
13
+ location: "Austin",
14
+ image: "https://images.unsplash.com/photo-1520950237264-dfe336995c34?w=400&h=400&fit=crop",
15
+ description: "Scenic lake perfect for kayaking, paddleboarding, and trails."
16
+ },
17
+ {
18
+ id: "2",
19
+ name: "Texas State Capitol",
20
+ rating: 4.8,
21
+ category: "Historic Site",
22
+ location: "Austin",
23
+ image: "https://images.unsplash.com/photo-1664231978322-4d0b45c7027b?w=400&h=400&fit=crop",
24
+ description: "Stunning capitol building with free tours and beautiful grounds."
25
+ },
26
+ {
27
+ id: "3",
28
+ name: "The Paramount Theatre",
29
+ rating: 4.7,
30
+ category: "Architecture",
31
+ location: "Austin",
32
+ image: "https://images.unsplash.com/photo-1583097090970-4d3b940ea1a0?w=400&h=400&fit=crop",
33
+ description: "Century-old performance and movie theatre in the heart of downtown Austin."
34
+ },
35
+ {
36
+ id: "4",
37
+ name: "Zilker Park",
38
+ rating: 4.7,
39
+ category: "Park",
40
+ location: "Austin",
41
+ image: "https://images.unsplash.com/photo-1563828568124-f800803ba13c?w=400&h=400&fit=crop",
42
+ description: "Popular park with trails, sports fields, and Barton Springs Pool."
43
+ },
44
+ {
45
+ id: "5",
46
+ name: "South Congress Avenue",
47
+ rating: 4.6,
48
+ category: "Landmark",
49
+ location: "Austin",
50
+ image: "https://images.unsplash.com/photo-1588993608283-7f0eda4438be?w=400&h=400&fit=crop",
51
+ description: "Vibrant street with unique shops, restaurants, and live music."
52
+ }
53
+ ]
54
+ };
55
+
56
+ export const carouselSimulationConfig = {
57
+ appName: 'Splorin',
58
+ value: 'carousel',
59
+ label: 'Carousel',
60
+ appIcon: '✈️',
61
+ userMessage: 'Show me popular places to visit in Austin Texas',
62
+ toolOutput: placesData,
63
+ widgetState: null,
64
+ toolName: 'show-app',
65
+ toolDescription: 'Show popular places to visit in Austin, Texas',
66
+ } as const;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Simulation configurations
3
+ *
4
+ * These configs contain only metadata and can be safely imported in Node.js contexts
5
+ * (like MCP servers) without causing issues with CSS imports or React components.
6
+ */
7
+
8
+ export { appSimulationConfig } from './app-simulation';
9
+ export { carouselSimulationConfig } from './carousel-simulation';
10
+ export { albumsSimulationConfig } from './albums-simulation';
11
+
12
+ /**
13
+ * Simulations - DO NOT import in Node.js/MCP server contexts!
14
+ * These include React components and CSS imports.
15
+ */
16
+ export { appSimulation, albumsSimulation, carouselSimulation, simulations } from './simulations';
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Simulations - combines apps with their simulation configs
3
+ *
4
+ * This file creates simulations by pairing production-ready apps with their
5
+ * server-safe configs. Each simulation is used for development and testing.
6
+ *
7
+ * IMPORTANT: This file imports React components and CSS. Do not import this
8
+ * in Node.js/MCP server contexts. Use app-configs.ts instead.
9
+ *
10
+ * To switch apps, change ACTIVE_APP in components/apps/active-app.ts
11
+ */
12
+
13
+ import type { Simulation } from "sunpeak"
14
+ import type { ComponentType } from "react"
15
+ import { App } from "../components/apps/App"
16
+ import { AlbumsApp } from "../components/apps/AlbumsApp"
17
+ import { PlacesApp } from "../components/apps/PlacesApp"
18
+ import { CONFIG_MAP, ACTIVE_APP } from "./app-configs"
19
+
20
+ export const appSimulation: Simulation = {
21
+ ...CONFIG_MAP.app,
22
+ component: App,
23
+ }
24
+
25
+ export const albumsSimulation: Simulation = {
26
+ ...CONFIG_MAP.albums,
27
+ component: AlbumsApp,
28
+ }
29
+
30
+ export const carouselSimulation: Simulation = {
31
+ ...CONFIG_MAP.carousel,
32
+ component: PlacesApp,
33
+ }
34
+
35
+ export const simulations = [
36
+ appSimulation,
37
+ carouselSimulation,
38
+ albumsSimulation,
39
+ ]
40
+
41
+ /**
42
+ * Static map from simulation name to simulation config and component
43
+ */
44
+ export const APP_MAP = {
45
+ app: {
46
+ config: CONFIG_MAP.app,
47
+ component: App,
48
+ simulation: appSimulation,
49
+ },
50
+ albums: {
51
+ config: CONFIG_MAP.albums,
52
+ component: AlbumsApp,
53
+ simulation: albumsSimulation,
54
+ },
55
+ carousel: {
56
+ config: CONFIG_MAP.carousel,
57
+ component: PlacesApp,
58
+ simulation: carouselSimulation,
59
+ },
60
+ } as const satisfies Record<string, {
61
+ config: typeof CONFIG_MAP[keyof typeof CONFIG_MAP];
62
+ component: ComponentType;
63
+ simulation: Simulation;
64
+ }>;
65
+
66
+ export type AppName = keyof typeof APP_MAP;
67
+
68
+ // Re-export for convenience
69
+ export { ACTIVE_APP, CONFIG_MAP };
70
+
71
+ // Derived exports for convenience
72
+ export const activeSimulation = APP_MAP[ACTIVE_APP].simulation;
73
+ export const activeConfig = APP_MAP[ACTIVE_APP].config;
74
+ export const ActiveComponent = APP_MAP[ACTIVE_APP].component;
@@ -1,112 +0,0 @@
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
- }