@nitrostack/cli 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/README.md +131 -0
- package/dist/commands/build.d.ts +6 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +185 -0
- package/dist/commands/dev.d.ts +7 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +365 -0
- package/dist/commands/generate-types.d.ts +8 -0
- package/dist/commands/generate-types.d.ts.map +1 -0
- package/dist/commands/generate-types.js +219 -0
- package/dist/commands/generate.d.ts +12 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +375 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +324 -0
- package/dist/commands/install.d.ts +10 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +80 -0
- package/dist/commands/start.d.ts +6 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +70 -0
- package/dist/commands/upgrade.d.ts +10 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +214 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/mcp-dev-wrapper.d.ts +15 -0
- package/dist/mcp-dev-wrapper.d.ts.map +1 -0
- package/dist/mcp-dev-wrapper.js +187 -0
- package/dist/ui/branding.d.ts +31 -0
- package/dist/ui/branding.d.ts.map +1 -0
- package/dist/ui/branding.js +136 -0
- package/package.json +69 -0
- package/templates/typescript-oauth/.env.example +27 -0
- package/templates/typescript-oauth/OAUTH_SETUP.md +592 -0
- package/templates/typescript-oauth/README.md +263 -0
- package/templates/typescript-oauth/package.json +29 -0
- package/templates/typescript-oauth/src/app.module.ts +92 -0
- package/templates/typescript-oauth/src/guards/oauth.guard.ts +126 -0
- package/templates/typescript-oauth/src/health/system.health.ts +55 -0
- package/templates/typescript-oauth/src/index.ts +63 -0
- package/templates/typescript-oauth/src/modules/flights/booking.tools.ts +323 -0
- package/templates/typescript-oauth/src/modules/flights/flights.module.ts +14 -0
- package/templates/typescript-oauth/src/modules/flights/flights.prompts.ts +228 -0
- package/templates/typescript-oauth/src/modules/flights/flights.resources.ts +215 -0
- package/templates/typescript-oauth/src/modules/flights/flights.tools.ts +457 -0
- package/templates/typescript-oauth/src/services/duffel.service.ts +285 -0
- package/templates/typescript-oauth/src/widgets/app/airport-search/page.tsx +270 -0
- package/templates/typescript-oauth/src/widgets/app/flight-details/page.tsx +261 -0
- package/templates/typescript-oauth/src/widgets/app/flight-search-results/page.tsx +378 -0
- package/templates/typescript-oauth/src/widgets/app/globals.css +167 -0
- package/templates/typescript-oauth/src/widgets/app/layout.tsx +18 -0
- package/templates/typescript-oauth/src/widgets/app/order-cancellation/page.tsx +207 -0
- package/templates/typescript-oauth/src/widgets/app/order-summary/page.tsx +245 -0
- package/templates/typescript-oauth/src/widgets/app/payment-confirmation/page.tsx +152 -0
- package/templates/typescript-oauth/src/widgets/app/seat-selection/page.tsx +486 -0
- package/templates/typescript-oauth/src/widgets/next-env.d.ts +5 -0
- package/templates/typescript-oauth/src/widgets/next.config.js +45 -0
- package/templates/typescript-oauth/src/widgets/package-lock.json +4493 -0
- package/templates/typescript-oauth/src/widgets/package.json +24 -0
- package/templates/typescript-oauth/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-oauth/src/widgets/widget-manifest.json +395 -0
- package/templates/typescript-oauth/tsconfig.json +23 -0
- package/templates/typescript-pizzaz/README.md +252 -0
- package/templates/typescript-pizzaz/package.json +34 -0
- package/templates/typescript-pizzaz/src/app.module.ts +28 -0
- package/templates/typescript-pizzaz/src/index.ts +30 -0
- package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.data.ts +106 -0
- package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.module.ts +11 -0
- package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.service.ts +60 -0
- package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.tools.ts +197 -0
- package/templates/typescript-pizzaz/src/widgets/app/layout.tsx +18 -0
- package/templates/typescript-pizzaz/src/widgets/app/pizza-list/page.tsx +272 -0
- package/templates/typescript-pizzaz/src/widgets/app/pizza-map/page.tsx +216 -0
- package/templates/typescript-pizzaz/src/widgets/app/pizza-shop/page.tsx +374 -0
- package/templates/typescript-pizzaz/src/widgets/components/CompactShopCard.tsx +144 -0
- package/templates/typescript-pizzaz/src/widgets/components/PizzaCard.tsx +191 -0
- package/templates/typescript-pizzaz/src/widgets/next.config.js +45 -0
- package/templates/typescript-pizzaz/src/widgets/package.json +30 -0
- package/templates/typescript-pizzaz/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-pizzaz/src/widgets/widget-manifest.json +253 -0
- package/templates/typescript-pizzaz/tsconfig.json +30 -0
- package/templates/typescript-starter/README.md +320 -0
- package/templates/typescript-starter/package.json +25 -0
- package/templates/typescript-starter/src/app.module.ts +34 -0
- package/templates/typescript-starter/src/health/system.health.ts +55 -0
- package/templates/typescript-starter/src/index.ts +29 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.module.ts +12 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.prompts.ts +73 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.resources.ts +59 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +166 -0
- package/templates/typescript-starter/src/widgets/app/calculator-result/page.tsx +180 -0
- package/templates/typescript-starter/src/widgets/app/layout.tsx +18 -0
- package/templates/typescript-starter/src/widgets/next.config.js +45 -0
- package/templates/typescript-starter/src/widgets/package.json +24 -0
- package/templates/typescript-starter/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-starter/src/widgets/widget-manifest.json +48 -0
- package/templates/typescript-starter/tsconfig.json +23 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { ToolDecorator as Tool, Widget, ExecutionContext, Injectable, z } from 'nitrostack';
|
|
2
|
+
import { PizzazService } from './pizzaz.service.js';
|
|
3
|
+
|
|
4
|
+
const ShowMapSchema = z.object({
|
|
5
|
+
filter: z.enum(['open_now', 'top_rated', 'all']).optional().describe('Filter to apply'),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const ShowListSchema = z.object({
|
|
9
|
+
openNow: z.boolean().optional().describe('Show only shops that are currently open'),
|
|
10
|
+
minRating: z.number().min(1).max(5).optional().describe('Minimum rating (1-5)'),
|
|
11
|
+
maxPrice: z.number().min(1).max(3).optional().describe('Maximum price level (1-3)'),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const ShowShopSchema = z.object({
|
|
15
|
+
shopId: z.string().describe('ID of the pizza shop to display'),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
@Injectable()
|
|
19
|
+
export class PizzazTools {
|
|
20
|
+
constructor(private readonly pizzazService: PizzazService) { }
|
|
21
|
+
|
|
22
|
+
@Tool({
|
|
23
|
+
name: 'show_pizza_map',
|
|
24
|
+
description: 'Display an interactive map of pizza shops in San Francisco',
|
|
25
|
+
inputSchema: ShowMapSchema,
|
|
26
|
+
examples: {
|
|
27
|
+
request: { filter: 'all' },
|
|
28
|
+
response: {
|
|
29
|
+
shops: [
|
|
30
|
+
{
|
|
31
|
+
id: 'tonys-pizza',
|
|
32
|
+
name: "Tony's New York Pizza",
|
|
33
|
+
description: "Authentic New York-style pizza with a crispy thin crust",
|
|
34
|
+
address: "123 Main St, San Francisco, CA 94102",
|
|
35
|
+
coords: [-122.4194, 37.7749],
|
|
36
|
+
rating: 4.5,
|
|
37
|
+
reviews: 342,
|
|
38
|
+
priceLevel: 2,
|
|
39
|
+
cuisine: ['Italian', 'Pizza'],
|
|
40
|
+
hours: { open: '11:00 AM', close: '10:00 PM' },
|
|
41
|
+
phone: '(415) 555-0123',
|
|
42
|
+
website: 'https://tonyspizza.example.com',
|
|
43
|
+
image: 'https://images.unsplash.com/photo-1513104890138-7c749659a591',
|
|
44
|
+
specialties: ['Margherita', 'Pepperoni'],
|
|
45
|
+
openNow: true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'bella-napoli',
|
|
49
|
+
name: 'Bella Napoli',
|
|
50
|
+
description: "Traditional Neapolitan pizza baked in a wood-fired oven",
|
|
51
|
+
address: "456 Market St, San Francisco, CA 94103",
|
|
52
|
+
coords: [-122.4089, 37.7858],
|
|
53
|
+
rating: 4.8,
|
|
54
|
+
reviews: 521,
|
|
55
|
+
priceLevel: 3,
|
|
56
|
+
cuisine: ['Italian', 'Pizza'],
|
|
57
|
+
hours: { open: '12:00 PM', close: '11:00 PM' },
|
|
58
|
+
phone: '(415) 555-0456',
|
|
59
|
+
image: 'https://images.unsplash.com/photo-1574071318508-1cdbab80d002',
|
|
60
|
+
specialties: ['Marinara', 'Quattro Formaggi'],
|
|
61
|
+
openNow: true
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
filter: 'all',
|
|
65
|
+
totalShops: 2
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
@Widget('pizza-map')
|
|
70
|
+
async showPizzaMap(args: z.infer<typeof ShowMapSchema>, ctx: ExecutionContext) {
|
|
71
|
+
let shops;
|
|
72
|
+
|
|
73
|
+
switch (args.filter) {
|
|
74
|
+
case 'open_now':
|
|
75
|
+
shops = this.pizzazService.getShopsFiltered({ openNow: true });
|
|
76
|
+
break;
|
|
77
|
+
case 'top_rated':
|
|
78
|
+
shops = this.pizzazService.getTopRatedShops();
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
shops = this.pizzazService.getAllShops();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ctx.logger.info('Showing pizza map', { filter: args.filter, totalShops: shops.length });
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
shops,
|
|
88
|
+
filter: args.filter || 'all',
|
|
89
|
+
totalShops: shops.length,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Tool({
|
|
94
|
+
name: 'show_pizza_list',
|
|
95
|
+
description: 'Display a list of pizza shops with filtering and sorting options',
|
|
96
|
+
inputSchema: ShowListSchema,
|
|
97
|
+
examples: {
|
|
98
|
+
request: { openNow: true },
|
|
99
|
+
response: {
|
|
100
|
+
shops: [
|
|
101
|
+
{
|
|
102
|
+
id: 'tonys-pizza',
|
|
103
|
+
name: "Tony's New York Pizza",
|
|
104
|
+
description: "Authentic New York-style pizza with a crispy thin crust",
|
|
105
|
+
address: "123 Main St, San Francisco, CA 94102",
|
|
106
|
+
coords: [-122.4194, 37.7749],
|
|
107
|
+
rating: 4.5,
|
|
108
|
+
reviews: 342,
|
|
109
|
+
priceLevel: 2,
|
|
110
|
+
cuisine: ['Italian', 'Pizza'],
|
|
111
|
+
hours: { open: '11:00 AM', close: '10:00 PM' },
|
|
112
|
+
phone: '(415) 555-0123',
|
|
113
|
+
website: 'https://tonyspizza.example.com',
|
|
114
|
+
image: 'https://images.unsplash.com/photo-1513104890138-7c749659a591',
|
|
115
|
+
specialties: ['Margherita', 'Pepperoni'],
|
|
116
|
+
openNow: true
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
filters: { openNow: true },
|
|
120
|
+
totalShops: 1
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
@Widget('pizza-list')
|
|
125
|
+
async showPizzaList(args: z.infer<typeof ShowListSchema>, ctx: ExecutionContext) {
|
|
126
|
+
const shops = this.pizzazService.getShopsFiltered(args);
|
|
127
|
+
|
|
128
|
+
ctx.logger.info('Showing pizza list', { filters: args, totalShops: shops.length });
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
shops,
|
|
132
|
+
filters: args,
|
|
133
|
+
totalShops: shops.length,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Tool({
|
|
138
|
+
name: 'show_pizza_shop',
|
|
139
|
+
description: 'Display detailed information about a specific pizza shop',
|
|
140
|
+
inputSchema: ShowShopSchema,
|
|
141
|
+
examples: {
|
|
142
|
+
request: { shopId: 'tonys-pizza' },
|
|
143
|
+
response: {
|
|
144
|
+
shop: {
|
|
145
|
+
id: 'tonys-pizza',
|
|
146
|
+
name: "Tony's New York Pizza",
|
|
147
|
+
description: "Authentic New York-style pizza with a crispy thin crust and fresh toppings",
|
|
148
|
+
address: "123 Main St, San Francisco, CA 94102",
|
|
149
|
+
coords: [-122.4194, 37.7749],
|
|
150
|
+
rating: 4.5,
|
|
151
|
+
reviews: 342,
|
|
152
|
+
priceLevel: 2,
|
|
153
|
+
cuisine: ['Italian', 'Pizza', 'New York Style'],
|
|
154
|
+
hours: { open: '11:00 AM', close: '10:00 PM' },
|
|
155
|
+
phone: '(415) 555-0123',
|
|
156
|
+
website: 'https://tonyspizza.example.com',
|
|
157
|
+
image: 'https://images.unsplash.com/photo-1513104890138-7c749659a591',
|
|
158
|
+
specialties: ['Margherita', 'Pepperoni', 'White Pizza'],
|
|
159
|
+
openNow: true
|
|
160
|
+
},
|
|
161
|
+
relatedShops: [
|
|
162
|
+
{
|
|
163
|
+
id: 'bella-napoli',
|
|
164
|
+
name: 'Bella Napoli',
|
|
165
|
+
description: "Traditional Neapolitan pizza baked in a wood-fired oven",
|
|
166
|
+
address: "456 Market St, San Francisco, CA 94103",
|
|
167
|
+
coords: [-122.4089, 37.7858],
|
|
168
|
+
rating: 4.8,
|
|
169
|
+
reviews: 521,
|
|
170
|
+
priceLevel: 3,
|
|
171
|
+
cuisine: ['Italian', 'Pizza'],
|
|
172
|
+
hours: { open: '12:00 PM', close: '11:00 PM' },
|
|
173
|
+
phone: '(415) 555-0456',
|
|
174
|
+
image: 'https://images.unsplash.com/photo-1574071318508-1cdbab80d002',
|
|
175
|
+
specialties: ['Marinara', 'Quattro Formaggi'],
|
|
176
|
+
openNow: true
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
@Widget('pizza-shop')
|
|
183
|
+
async showPizzaShop(args: z.infer<typeof ShowShopSchema>, ctx: ExecutionContext) {
|
|
184
|
+
const shop = this.pizzazService.getShopById(args.shopId);
|
|
185
|
+
|
|
186
|
+
if (!shop) {
|
|
187
|
+
throw new Error(`Pizza shop not found: ${args.shopId}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
ctx.logger.info('Showing pizza shop', { shopId: args.shopId, shopName: shop.name });
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
shop,
|
|
194
|
+
relatedShops: this.pizzazService.getTopRatedShops(3).filter(s => s.id !== shop.id),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { WidgetLayout } from '@nitrostack/widgets';
|
|
4
|
+
import 'mapbox-gl/dist/mapbox-gl.css';
|
|
5
|
+
|
|
6
|
+
export default function RootLayout({
|
|
7
|
+
children,
|
|
8
|
+
}: {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}) {
|
|
11
|
+
return (
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<body style={{ margin: 0, padding: 0, fontFamily: 'system-ui, sans-serif' }}>
|
|
14
|
+
<WidgetLayout>{children}</WidgetLayout>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTheme, useWidgetState, useMaxHeight, useWidgetSDK } from '@nitrostack/widgets';
|
|
4
|
+
|
|
5
|
+
// Disable static generation - this is a dynamic widget
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
import { PizzaCard } from '../../components/PizzaCard';
|
|
8
|
+
import { SlidersHorizontal } from 'lucide-react';
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
|
|
11
|
+
interface PizzaShop {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
address: string;
|
|
16
|
+
coords: [number, number];
|
|
17
|
+
rating: number;
|
|
18
|
+
reviews: number;
|
|
19
|
+
priceLevel: 1 | 2 | 3;
|
|
20
|
+
cuisine: string[];
|
|
21
|
+
hours: { open: string; close: string };
|
|
22
|
+
phone: string;
|
|
23
|
+
website?: string;
|
|
24
|
+
image: string;
|
|
25
|
+
specialties: string[];
|
|
26
|
+
openNow: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface WidgetData {
|
|
30
|
+
shops: PizzaShop[];
|
|
31
|
+
filters: any;
|
|
32
|
+
totalShops: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function PizzaListWidget() {
|
|
36
|
+
const theme = useTheme();
|
|
37
|
+
const maxHeight = useMaxHeight();
|
|
38
|
+
const isDark = theme === 'dark';
|
|
39
|
+
|
|
40
|
+
const { isReady, getToolOutput, callTool } = useWidgetSDK();
|
|
41
|
+
|
|
42
|
+
// Access tool output
|
|
43
|
+
const data = getToolOutput<WidgetData>();
|
|
44
|
+
|
|
45
|
+
console.log('🍕 PizzaListWidget render:', { isReady, hasData: !!data, data });
|
|
46
|
+
|
|
47
|
+
// Persistent state for view mode and favorites
|
|
48
|
+
const [state, setState] = useWidgetState<{
|
|
49
|
+
viewMode: 'grid' | 'list';
|
|
50
|
+
favorites: string[];
|
|
51
|
+
sortBy: 'rating' | 'name' | 'price';
|
|
52
|
+
}>(() => ({
|
|
53
|
+
viewMode: 'grid',
|
|
54
|
+
favorites: [],
|
|
55
|
+
sortBy: 'rating',
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
const [showFilters, setShowFilters] = useState(false);
|
|
59
|
+
|
|
60
|
+
if (!data) {
|
|
61
|
+
return (
|
|
62
|
+
<div style={{
|
|
63
|
+
padding: '40px',
|
|
64
|
+
textAlign: 'center',
|
|
65
|
+
color: isDark ? '#fff' : '#000',
|
|
66
|
+
}}>
|
|
67
|
+
Loading pizza shops... {isReady ? '(SDK ready but no data)' : '(waiting for SDK)'}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check if shops array exists
|
|
73
|
+
if (!data.shops || !Array.isArray(data.shops)) {
|
|
74
|
+
console.error('❌ Invalid data structure:', data);
|
|
75
|
+
return (
|
|
76
|
+
<div style={{
|
|
77
|
+
padding: '40px',
|
|
78
|
+
textAlign: 'center',
|
|
79
|
+
color: isDark ? '#fff' : '#000',
|
|
80
|
+
}}>
|
|
81
|
+
Error: Invalid data structure. Expected shops array.
|
|
82
|
+
<pre style={{ marginTop: '16px', fontSize: '12px', textAlign: 'left' }}>
|
|
83
|
+
{JSON.stringify(data, null, 2)}
|
|
84
|
+
</pre>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const toggleFavorite = (shopId: string) => {
|
|
90
|
+
const favorites = state?.favorites || [];
|
|
91
|
+
const newFavorites = favorites.includes(shopId)
|
|
92
|
+
? favorites.filter(id => id !== shopId)
|
|
93
|
+
: [...favorites, shopId];
|
|
94
|
+
|
|
95
|
+
setState({ ...state, favorites: newFavorites });
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleShopClick = async (shopId: string) => {
|
|
99
|
+
// Call the show_pizza_shop tool to show shop details
|
|
100
|
+
await callTool('show_pizza_shop', { shopId });
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Sort shops
|
|
104
|
+
let sortedShops = [...data.shops];
|
|
105
|
+
switch (state?.sortBy) {
|
|
106
|
+
case 'rating':
|
|
107
|
+
sortedShops.sort((a, b) => b.rating - a.rating);
|
|
108
|
+
break;
|
|
109
|
+
case 'name':
|
|
110
|
+
sortedShops.sort((a, b) => a.name.localeCompare(b.name));
|
|
111
|
+
break;
|
|
112
|
+
case 'price':
|
|
113
|
+
sortedShops.sort((a, b) => a.priceLevel - b.priceLevel);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div style={{
|
|
119
|
+
background: isDark ? '#0a0a0a' : '#f9fafb',
|
|
120
|
+
minHeight: '400px',
|
|
121
|
+
maxHeight: maxHeight || '600px',
|
|
122
|
+
overflow: 'auto',
|
|
123
|
+
}}>
|
|
124
|
+
{/* Header */}
|
|
125
|
+
<div style={{
|
|
126
|
+
background: isDark ? '#1a1a1a' : '#ffffff',
|
|
127
|
+
borderBottom: `1px solid ${isDark ? '#333' : '#e5e7eb'}`,
|
|
128
|
+
padding: '12px 16px',
|
|
129
|
+
position: 'sticky',
|
|
130
|
+
top: 0,
|
|
131
|
+
zIndex: 10,
|
|
132
|
+
}}>
|
|
133
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
|
134
|
+
<div>
|
|
135
|
+
<h1 style={{
|
|
136
|
+
margin: '0 0 2px 0',
|
|
137
|
+
fontSize: '18px',
|
|
138
|
+
fontWeight: '700',
|
|
139
|
+
color: isDark ? '#fff' : '#111',
|
|
140
|
+
}}>
|
|
141
|
+
🍕 Pizza Shops
|
|
142
|
+
</h1>
|
|
143
|
+
<p style={{
|
|
144
|
+
margin: 0,
|
|
145
|
+
fontSize: '12px',
|
|
146
|
+
color: isDark ? '#999' : '#666',
|
|
147
|
+
}}>
|
|
148
|
+
{data.totalShops} shops found
|
|
149
|
+
</p>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Sort and Filter Controls */}
|
|
153
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
154
|
+
<select
|
|
155
|
+
value={state?.sortBy || 'rating'}
|
|
156
|
+
onChange={(e) => setState({ ...state, sortBy: e.target.value as any })}
|
|
157
|
+
style={{
|
|
158
|
+
padding: '6px 10px',
|
|
159
|
+
background: isDark ? '#1a1a1a' : '#fff',
|
|
160
|
+
border: `1px solid ${isDark ? '#444' : '#d1d5db'}`,
|
|
161
|
+
borderRadius: '6px',
|
|
162
|
+
color: isDark ? '#fff' : '#111',
|
|
163
|
+
fontSize: '12px',
|
|
164
|
+
cursor: 'pointer',
|
|
165
|
+
outline: 'none',
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
<option value="rating">⭐ Rating</option>
|
|
169
|
+
<option value="name">🔤 Name</option>
|
|
170
|
+
<option value="price">💰 Price</option>
|
|
171
|
+
</select>
|
|
172
|
+
|
|
173
|
+
<button
|
|
174
|
+
onClick={() => setShowFilters(!showFilters)}
|
|
175
|
+
style={{
|
|
176
|
+
padding: '8px 12px',
|
|
177
|
+
background: showFilters
|
|
178
|
+
? (isDark ? '#333' : '#f3f4f6')
|
|
179
|
+
: 'transparent',
|
|
180
|
+
border: `1px solid ${isDark ? '#444' : '#d1d5db'}`,
|
|
181
|
+
borderRadius: '8px',
|
|
182
|
+
cursor: 'pointer',
|
|
183
|
+
color: isDark ? '#fff' : '#111',
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
<SlidersHorizontal size={18} />
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* Filters */}
|
|
192
|
+
{showFilters && (
|
|
193
|
+
<div style={{
|
|
194
|
+
padding: '16px',
|
|
195
|
+
background: isDark ? '#111' : '#f9fafb',
|
|
196
|
+
borderRadius: '8px',
|
|
197
|
+
marginTop: '12px',
|
|
198
|
+
}}>
|
|
199
|
+
<p style={{
|
|
200
|
+
margin: '0 0 8px 0',
|
|
201
|
+
fontSize: '12px',
|
|
202
|
+
color: isDark ? '#999' : '#666',
|
|
203
|
+
}}>
|
|
204
|
+
Filters coming soon...
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{/* Favorites Count */}
|
|
210
|
+
{state?.favorites && state.favorites.length > 0 && (
|
|
211
|
+
<div style={{
|
|
212
|
+
marginTop: '12px',
|
|
213
|
+
padding: '8px 12px',
|
|
214
|
+
background: isDark ? '#1a1a1a' : '#fef3c7',
|
|
215
|
+
border: `1px solid ${isDark ? '#333' : '#fbbf24'}`,
|
|
216
|
+
borderRadius: '8px',
|
|
217
|
+
fontSize: '14px',
|
|
218
|
+
color: isDark ? '#fbbf24' : '#92400e',
|
|
219
|
+
}}>
|
|
220
|
+
❤️ {state.favorites.length} favorite{state.favorites.length !== 1 ? 's' : ''}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* Horizontal Scrolling Shop Cards */}
|
|
226
|
+
<div style={{
|
|
227
|
+
padding: '12px 16px',
|
|
228
|
+
overflowX: 'auto',
|
|
229
|
+
overflowY: 'hidden',
|
|
230
|
+
scrollSnapType: 'x mandatory',
|
|
231
|
+
WebkitOverflowScrolling: 'touch',
|
|
232
|
+
scrollbarWidth: 'thin',
|
|
233
|
+
}}>
|
|
234
|
+
<div style={{
|
|
235
|
+
display: 'flex',
|
|
236
|
+
gap: '12px',
|
|
237
|
+
paddingBottom: '4px',
|
|
238
|
+
}}>
|
|
239
|
+
{sortedShops.map(shop => (
|
|
240
|
+
<div
|
|
241
|
+
key={shop.id}
|
|
242
|
+
style={{
|
|
243
|
+
minWidth: '280px',
|
|
244
|
+
maxWidth: '280px',
|
|
245
|
+
scrollSnapAlign: 'start',
|
|
246
|
+
flexShrink: 0,
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
<PizzaCard
|
|
250
|
+
shop={shop}
|
|
251
|
+
isFavorite={state?.favorites?.includes(shop.id)}
|
|
252
|
+
onToggleFavorite={toggleFavorite}
|
|
253
|
+
onSelect={() => handleShopClick(shop.id)}
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
))}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{/* Footer */}
|
|
261
|
+
<div style={{
|
|
262
|
+
padding: '20px',
|
|
263
|
+
textAlign: 'center',
|
|
264
|
+
fontSize: '12px',
|
|
265
|
+
color: isDark ? '#666' : '#999',
|
|
266
|
+
borderTop: `1px solid ${isDark ? '#333' : '#e5e7eb'}`,
|
|
267
|
+
}}>
|
|
268
|
+
Powered by NitroStack • Theme: {theme || 'light'} • Scroll horizontally →
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|