@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.
Files changed (100) hide show
  1. package/README.md +131 -0
  2. package/dist/commands/build.d.ts +6 -0
  3. package/dist/commands/build.d.ts.map +1 -0
  4. package/dist/commands/build.js +185 -0
  5. package/dist/commands/dev.d.ts +7 -0
  6. package/dist/commands/dev.d.ts.map +1 -0
  7. package/dist/commands/dev.js +365 -0
  8. package/dist/commands/generate-types.d.ts +8 -0
  9. package/dist/commands/generate-types.d.ts.map +1 -0
  10. package/dist/commands/generate-types.js +219 -0
  11. package/dist/commands/generate.d.ts +12 -0
  12. package/dist/commands/generate.d.ts.map +1 -0
  13. package/dist/commands/generate.js +375 -0
  14. package/dist/commands/init.d.ts +7 -0
  15. package/dist/commands/init.d.ts.map +1 -0
  16. package/dist/commands/init.js +324 -0
  17. package/dist/commands/install.d.ts +10 -0
  18. package/dist/commands/install.d.ts.map +1 -0
  19. package/dist/commands/install.js +80 -0
  20. package/dist/commands/start.d.ts +6 -0
  21. package/dist/commands/start.d.ts.map +1 -0
  22. package/dist/commands/start.js +70 -0
  23. package/dist/commands/upgrade.d.ts +10 -0
  24. package/dist/commands/upgrade.d.ts.map +1 -0
  25. package/dist/commands/upgrade.js +214 -0
  26. package/dist/index.d.ts +11 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +94 -0
  29. package/dist/mcp-dev-wrapper.d.ts +15 -0
  30. package/dist/mcp-dev-wrapper.d.ts.map +1 -0
  31. package/dist/mcp-dev-wrapper.js +187 -0
  32. package/dist/ui/branding.d.ts +31 -0
  33. package/dist/ui/branding.d.ts.map +1 -0
  34. package/dist/ui/branding.js +136 -0
  35. package/package.json +69 -0
  36. package/templates/typescript-oauth/.env.example +27 -0
  37. package/templates/typescript-oauth/OAUTH_SETUP.md +592 -0
  38. package/templates/typescript-oauth/README.md +263 -0
  39. package/templates/typescript-oauth/package.json +29 -0
  40. package/templates/typescript-oauth/src/app.module.ts +92 -0
  41. package/templates/typescript-oauth/src/guards/oauth.guard.ts +126 -0
  42. package/templates/typescript-oauth/src/health/system.health.ts +55 -0
  43. package/templates/typescript-oauth/src/index.ts +63 -0
  44. package/templates/typescript-oauth/src/modules/flights/booking.tools.ts +323 -0
  45. package/templates/typescript-oauth/src/modules/flights/flights.module.ts +14 -0
  46. package/templates/typescript-oauth/src/modules/flights/flights.prompts.ts +228 -0
  47. package/templates/typescript-oauth/src/modules/flights/flights.resources.ts +215 -0
  48. package/templates/typescript-oauth/src/modules/flights/flights.tools.ts +457 -0
  49. package/templates/typescript-oauth/src/services/duffel.service.ts +285 -0
  50. package/templates/typescript-oauth/src/widgets/app/airport-search/page.tsx +270 -0
  51. package/templates/typescript-oauth/src/widgets/app/flight-details/page.tsx +261 -0
  52. package/templates/typescript-oauth/src/widgets/app/flight-search-results/page.tsx +378 -0
  53. package/templates/typescript-oauth/src/widgets/app/globals.css +167 -0
  54. package/templates/typescript-oauth/src/widgets/app/layout.tsx +18 -0
  55. package/templates/typescript-oauth/src/widgets/app/order-cancellation/page.tsx +207 -0
  56. package/templates/typescript-oauth/src/widgets/app/order-summary/page.tsx +245 -0
  57. package/templates/typescript-oauth/src/widgets/app/payment-confirmation/page.tsx +152 -0
  58. package/templates/typescript-oauth/src/widgets/app/seat-selection/page.tsx +486 -0
  59. package/templates/typescript-oauth/src/widgets/next-env.d.ts +5 -0
  60. package/templates/typescript-oauth/src/widgets/next.config.js +45 -0
  61. package/templates/typescript-oauth/src/widgets/package-lock.json +4493 -0
  62. package/templates/typescript-oauth/src/widgets/package.json +24 -0
  63. package/templates/typescript-oauth/src/widgets/tsconfig.json +28 -0
  64. package/templates/typescript-oauth/src/widgets/widget-manifest.json +395 -0
  65. package/templates/typescript-oauth/tsconfig.json +23 -0
  66. package/templates/typescript-pizzaz/README.md +252 -0
  67. package/templates/typescript-pizzaz/package.json +34 -0
  68. package/templates/typescript-pizzaz/src/app.module.ts +28 -0
  69. package/templates/typescript-pizzaz/src/index.ts +30 -0
  70. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.data.ts +106 -0
  71. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.module.ts +11 -0
  72. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.service.ts +60 -0
  73. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.tools.ts +197 -0
  74. package/templates/typescript-pizzaz/src/widgets/app/layout.tsx +18 -0
  75. package/templates/typescript-pizzaz/src/widgets/app/pizza-list/page.tsx +272 -0
  76. package/templates/typescript-pizzaz/src/widgets/app/pizza-map/page.tsx +216 -0
  77. package/templates/typescript-pizzaz/src/widgets/app/pizza-shop/page.tsx +374 -0
  78. package/templates/typescript-pizzaz/src/widgets/components/CompactShopCard.tsx +144 -0
  79. package/templates/typescript-pizzaz/src/widgets/components/PizzaCard.tsx +191 -0
  80. package/templates/typescript-pizzaz/src/widgets/next.config.js +45 -0
  81. package/templates/typescript-pizzaz/src/widgets/package.json +30 -0
  82. package/templates/typescript-pizzaz/src/widgets/tsconfig.json +28 -0
  83. package/templates/typescript-pizzaz/src/widgets/widget-manifest.json +253 -0
  84. package/templates/typescript-pizzaz/tsconfig.json +30 -0
  85. package/templates/typescript-starter/README.md +320 -0
  86. package/templates/typescript-starter/package.json +25 -0
  87. package/templates/typescript-starter/src/app.module.ts +34 -0
  88. package/templates/typescript-starter/src/health/system.health.ts +55 -0
  89. package/templates/typescript-starter/src/index.ts +29 -0
  90. package/templates/typescript-starter/src/modules/calculator/calculator.module.ts +12 -0
  91. package/templates/typescript-starter/src/modules/calculator/calculator.prompts.ts +73 -0
  92. package/templates/typescript-starter/src/modules/calculator/calculator.resources.ts +59 -0
  93. package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +166 -0
  94. package/templates/typescript-starter/src/widgets/app/calculator-result/page.tsx +180 -0
  95. package/templates/typescript-starter/src/widgets/app/layout.tsx +18 -0
  96. package/templates/typescript-starter/src/widgets/next.config.js +45 -0
  97. package/templates/typescript-starter/src/widgets/package.json +24 -0
  98. package/templates/typescript-starter/src/widgets/tsconfig.json +28 -0
  99. package/templates/typescript-starter/src/widgets/widget-manifest.json +48 -0
  100. package/templates/typescript-starter/tsconfig.json +23 -0
@@ -0,0 +1,216 @@
1
+ 'use client';
2
+
3
+ import { useTheme, useWidgetState, useMaxHeight, useDisplayMode, useWidgetSDK } from '@nitrostack/widgets';
4
+
5
+ // Disable static generation - this is a dynamic widget
6
+ export const dynamic = 'force-dynamic';
7
+ import { useEffect, useRef, useState } from 'react';
8
+ import mapboxgl from 'mapbox-gl';
9
+ import 'mapbox-gl/dist/mapbox-gl.css';
10
+ import { CompactShopCard } from '../../components/CompactShopCard';
11
+ import { Maximize2 } from 'lucide-react';
12
+
13
+ interface PizzaShop {
14
+ id: string;
15
+ name: string;
16
+ description: string;
17
+ address: string;
18
+ coords: [number, number];
19
+ rating: number;
20
+ reviews: number;
21
+ priceLevel: 1 | 2 | 3;
22
+ cuisine: string[];
23
+ hours: { open: string; close: string };
24
+ phone: string;
25
+ website?: string;
26
+ image: string;
27
+ specialties: string[];
28
+ openNow: boolean;
29
+ }
30
+
31
+ interface WidgetData {
32
+ shops: PizzaShop[];
33
+ filter: string;
34
+ totalShops: number;
35
+ }
36
+
37
+ export default function PizzaMapWidget() {
38
+ const theme = useTheme();
39
+ const maxHeight = useMaxHeight();
40
+ const displayMode = useDisplayMode();
41
+ const isDark = theme === 'dark';
42
+ const mapContainer = useRef<HTMLDivElement>(null);
43
+ const map = useRef<mapboxgl.Map | null>(null);
44
+ const [selectedShop, setSelectedShop] = useState<PizzaShop | null>(null);
45
+
46
+ const { isReady, getToolOutput, callTool, requestFullscreen } = useWidgetSDK();
47
+
48
+ // Access tool output
49
+ const data = getToolOutput<WidgetData>();
50
+
51
+ // Persistent state
52
+ const [state, setState] = useWidgetState<{
53
+ favorites: string[];
54
+ }>(() => ({
55
+ favorites: [],
56
+ }));
57
+
58
+ useEffect(() => {
59
+ if (!mapContainer.current || !data || map.current) return;
60
+
61
+ // Initialize Mapbox
62
+ const initMap = async () => {
63
+ try {
64
+ // Set your Mapbox token here
65
+ mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || 'pk.eyJ1IjoiZXJpY25pbmciLCJhIjoiY21icXlubWM1MDRiczJvb2xwM2p0amNyayJ9.n-3O6JI5nOp_Lw96ZO5vJQ';
66
+
67
+ const mapInstance = new mapboxgl.Map({
68
+ container: mapContainer.current!,
69
+ style: isDark ? 'mapbox://styles/mapbox/dark-v11' : 'mapbox://styles/mapbox/streets-v12',
70
+ center: data.shops[0]?.coords || [-122.4194, 37.7749],
71
+ zoom: 12,
72
+ });
73
+
74
+ // Add markers
75
+ data.shops.forEach(shop => {
76
+ const el = document.createElement('div');
77
+ el.className = 'marker';
78
+ el.style.backgroundImage = 'url(https://docs.mapbox.com/mapbox-gl-js/assets/custom_marker.png)';
79
+ el.style.width = '30px';
80
+ el.style.height = '40px';
81
+ el.style.backgroundSize = '100%';
82
+ el.style.cursor = 'pointer';
83
+
84
+ el.addEventListener('click', () => {
85
+ setSelectedShop(shop);
86
+ });
87
+
88
+ new mapboxgl.Marker(el)
89
+ .setLngLat(shop.coords)
90
+ .addTo(mapInstance);
91
+ });
92
+
93
+ // Fit bounds to show all markers
94
+ if (data.shops.length > 1) {
95
+ const bounds = new mapboxgl.LngLatBounds();
96
+ data.shops.forEach(shop => bounds.extend(shop.coords));
97
+ mapInstance.fitBounds(bounds, { padding: 50 });
98
+ }
99
+
100
+ map.current = mapInstance;
101
+ } catch (error) {
102
+ console.error('Failed to load Mapbox:', error);
103
+ }
104
+ };
105
+
106
+ initMap();
107
+
108
+ return () => {
109
+ if (map.current) {
110
+ map.current.remove();
111
+ }
112
+ };
113
+ }, [data, isDark]);
114
+
115
+ if (!data) {
116
+ return (
117
+ <div style={{
118
+ padding: '40px',
119
+ textAlign: 'center',
120
+ color: isDark ? '#fff' : '#000',
121
+ }}>
122
+ Loading map... {isReady ? '(SDK ready but no data)' : '(waiting for SDK)'}
123
+ </div>
124
+ );
125
+ }
126
+
127
+ const handleShopClick = async (shopId: string) => {
128
+ // Call the show_pizza_shop tool to show shop details
129
+ await callTool('show_pizza_shop', { shopId });
130
+ };
131
+
132
+ const requestFullscreenMode = async () => {
133
+ await requestFullscreen();
134
+ };
135
+
136
+ return (
137
+ <div style={{
138
+ position: 'relative',
139
+ height: maxHeight || '650px',
140
+ background: isDark ? '#0a0a0a' : '#f9fafb',
141
+ overflow: 'hidden',
142
+ }}>
143
+ {/* Map Container - Full Screen */}
144
+ <div
145
+ ref={mapContainer}
146
+ style={{
147
+ position: 'absolute',
148
+ inset: 0,
149
+ }}
150
+ />
151
+
152
+ {/* Enlarge Button */}
153
+ <button
154
+ onClick={requestFullscreen}
155
+ style={{
156
+ position: 'absolute',
157
+ top: '16px',
158
+ right: '16px',
159
+ padding: '10px',
160
+ background: 'rgba(255, 255, 255, 0.9)',
161
+ border: 'none',
162
+ borderRadius: '8px',
163
+ cursor: 'pointer',
164
+ boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
165
+ zIndex: 10,
166
+ display: 'flex',
167
+ alignItems: 'center',
168
+ justifyContent: 'center',
169
+ }}
170
+ >
171
+ <Maximize2 size={18} style={{ color: '#111' }} />
172
+ </button>
173
+
174
+ {/* Overlay Shop Cards - Bottom */}
175
+ <div style={{
176
+ position: 'absolute',
177
+ bottom: '8px',
178
+ left: '16px',
179
+ right: '16px',
180
+ zIndex: 5,
181
+ overflowX: 'auto',
182
+ overflowY: 'hidden',
183
+ scrollSnapType: 'x mandatory',
184
+ WebkitOverflowScrolling: 'touch',
185
+ scrollbarWidth: 'none',
186
+ msOverflowStyle: 'none',
187
+ }}>
188
+ <div style={{
189
+ display: 'flex',
190
+ gap: '12px',
191
+ paddingBottom: '8px',
192
+ }}>
193
+ {data.shops.map(shop => (
194
+ <div
195
+ key={shop.id}
196
+ style={{
197
+ scrollSnapAlign: 'start',
198
+ flexShrink: 0,
199
+ }}
200
+ >
201
+ <CompactShopCard
202
+ shop={shop}
203
+ isSelected={selectedShop?.id === shop.id}
204
+ onClick={() => {
205
+ setSelectedShop(shop);
206
+ handleShopClick(shop.id);
207
+ }}
208
+ isDark={isDark}
209
+ />
210
+ </div>
211
+ ))}
212
+ </div>
213
+ </div>
214
+ </div>
215
+ );
216
+ }
@@ -0,0 +1,374 @@
1
+ 'use client';
2
+
3
+ import { useTheme, useMaxHeight, useWidgetSDK } from '@nitrostack/widgets';
4
+
5
+ // Disable static generation - this is a dynamic widget
6
+ export const dynamic = 'force-dynamic';
7
+ import { Star, MapPin, Phone, Globe, Clock, Heart, Share2, Navigation } from 'lucide-react';
8
+
9
+ interface PizzaShop {
10
+ id: string;
11
+ name: string;
12
+ description: string;
13
+ address: string;
14
+ coords: [number, number];
15
+ rating: number;
16
+ reviews: number;
17
+ priceLevel: 1 | 2 | 3;
18
+ cuisine: string[];
19
+ hours: { open: string; close: string };
20
+ phone: string;
21
+ website?: string;
22
+ image: string;
23
+ specialties: string[];
24
+ openNow: boolean;
25
+ }
26
+
27
+ interface WidgetData {
28
+ shop: PizzaShop;
29
+ relatedShops: PizzaShop[];
30
+ }
31
+
32
+ export default function PizzaShopWidget() {
33
+ const theme = useTheme();
34
+ const maxHeight = useMaxHeight();
35
+ const isDark = theme === 'dark';
36
+ const { isReady, getToolOutput, openExternal } = useWidgetSDK();
37
+
38
+ // Access tool output
39
+ const data = getToolOutput<WidgetData>();
40
+
41
+ if (!data) {
42
+ return (
43
+ <div style={{
44
+ padding: '40px',
45
+ textAlign: 'center',
46
+ color: isDark ? '#fff' : '#000',
47
+ }}>
48
+ Loading shop details... {isReady ? '(SDK ready but no data)' : '(waiting for SDK)'}
49
+ </div>
50
+ );
51
+ }
52
+
53
+ const { shop, relatedShops } = data;
54
+ const priceSymbol = '$'.repeat(shop.priceLevel);
55
+
56
+ const openMaps = () => {
57
+ const url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(shop.address)}`;
58
+ openExternal(url);
59
+ };
60
+
61
+ const callPhone = () => {
62
+ openExternal(`tel:${shop.phone}`);
63
+ };
64
+
65
+ const visitWebsite = () => {
66
+ if (shop.website) {
67
+ openExternal(shop.website);
68
+ }
69
+ };
70
+
71
+ return (
72
+ <div style={{
73
+ background: isDark ? '#0a0a0a' : '#f9fafb',
74
+ minHeight: '500px',
75
+ maxHeight: maxHeight || '800px',
76
+ overflow: 'auto',
77
+ }}>
78
+ {/* Hero Image */}
79
+ <div style={{ position: 'relative', height: '300px', overflow: 'hidden' }}>
80
+ <img
81
+ src={shop.image}
82
+ alt={shop.name}
83
+ style={{
84
+ width: '100%',
85
+ height: '100%',
86
+ objectFit: 'cover',
87
+ }}
88
+ />
89
+ <div style={{
90
+ position: 'absolute',
91
+ inset: 0,
92
+ background: 'linear-gradient(to top, rgba(0,0,0,0.7), transparent)',
93
+ }} />
94
+
95
+ {/* Shop Name Overlay */}
96
+ <div style={{
97
+ position: 'absolute',
98
+ bottom: '24px',
99
+ left: '24px',
100
+ right: '24px',
101
+ }}>
102
+ <h1 style={{
103
+ margin: '0 0 8px 0',
104
+ fontSize: '32px',
105
+ fontWeight: '700',
106
+ color: '#fff',
107
+ textShadow: '0 2px 4px rgba(0,0,0,0.5)',
108
+ }}>
109
+ {shop.name}
110
+ </h1>
111
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
112
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
113
+ <Star size={20} fill="#fbbf24" stroke="#fbbf24" />
114
+ <span style={{ fontSize: '18px', fontWeight: '600', color: '#fff' }}>
115
+ {shop.rating}
116
+ </span>
117
+ <span style={{ fontSize: '14px', color: '#ccc' }}>
118
+ ({shop.reviews} reviews)
119
+ </span>
120
+ </div>
121
+ <span style={{ fontSize: '16px', color: '#fff' }}>{priceSymbol}</span>
122
+ {shop.openNow && (
123
+ <span style={{
124
+ background: '#10b981',
125
+ color: 'white',
126
+ padding: '4px 12px',
127
+ borderRadius: '12px',
128
+ fontSize: '14px',
129
+ fontWeight: '600',
130
+ }}>
131
+ Open Now
132
+ </span>
133
+ )}
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ {/* Content */}
139
+ <div style={{ padding: '24px' }}>
140
+ {/* Description */}
141
+ <p style={{
142
+ margin: '0 0 24px 0',
143
+ fontSize: '16px',
144
+ lineHeight: '1.6',
145
+ color: isDark ? '#ccc' : '#666',
146
+ }}>
147
+ {shop.description}
148
+ </p>
149
+
150
+ {/* Cuisine Tags */}
151
+ <div style={{ marginBottom: '24px' }}>
152
+ <h3 style={{
153
+ margin: '0 0 12px 0',
154
+ fontSize: '14px',
155
+ fontWeight: '600',
156
+ color: isDark ? '#999' : '#666',
157
+ textTransform: 'uppercase',
158
+ letterSpacing: '0.5px',
159
+ }}>
160
+ Cuisine
161
+ </h3>
162
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
163
+ {shop.cuisine.map(c => (
164
+ <span
165
+ key={c}
166
+ style={{
167
+ background: isDark ? '#1a1a1a' : '#f3f4f6',
168
+ color: isDark ? '#fff' : '#111',
169
+ padding: '8px 16px',
170
+ borderRadius: '20px',
171
+ fontSize: '14px',
172
+ fontWeight: '500',
173
+ border: `1px solid ${isDark ? '#333' : '#e5e7eb'}`,
174
+ }}
175
+ >
176
+ {c}
177
+ </span>
178
+ ))}
179
+ </div>
180
+ </div>
181
+
182
+ {/* Specialties */}
183
+ <div style={{ marginBottom: '24px' }}>
184
+ <h3 style={{
185
+ margin: '0 0 12px 0',
186
+ fontSize: '14px',
187
+ fontWeight: '600',
188
+ color: isDark ? '#999' : '#666',
189
+ textTransform: 'uppercase',
190
+ letterSpacing: '0.5px',
191
+ }}>
192
+ Specialties
193
+ </h3>
194
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
195
+ {shop.specialties.map(s => (
196
+ <span
197
+ key={s}
198
+ style={{
199
+ background: isDark ? '#1a1a1a' : '#fef3c7',
200
+ color: isDark ? '#fbbf24' : '#92400e',
201
+ padding: '8px 16px',
202
+ borderRadius: '20px',
203
+ fontSize: '14px',
204
+ fontWeight: '500',
205
+ border: `1px solid ${isDark ? '#333' : '#fbbf24'}`,
206
+ }}
207
+ >
208
+ 🍕 {s}
209
+ </span>
210
+ ))}
211
+ </div>
212
+ </div>
213
+
214
+ {/* Contact Info */}
215
+ <div style={{
216
+ background: isDark ? '#1a1a1a' : '#fff',
217
+ border: `1px solid ${isDark ? '#333' : '#e5e7eb'}`,
218
+ borderRadius: '12px',
219
+ padding: '20px',
220
+ marginBottom: '24px',
221
+ }}>
222
+ <h3 style={{
223
+ margin: '0 0 16px 0',
224
+ fontSize: '18px',
225
+ fontWeight: '600',
226
+ color: isDark ? '#fff' : '#111',
227
+ }}>
228
+ Contact & Hours
229
+ </h3>
230
+
231
+ {/* Address */}
232
+ <div style={{ display: 'flex', alignItems: 'start', gap: '12px', marginBottom: '12px' }}>
233
+ <MapPin size={20} style={{ color: isDark ? '#999' : '#666', marginTop: '2px', flexShrink: 0 }} />
234
+ <div style={{ flex: 1 }}>
235
+ <p style={{ margin: '0 0 8px 0', fontSize: '14px', color: isDark ? '#ccc' : '#666' }}>
236
+ {shop.address}
237
+ </p>
238
+ <button
239
+ onClick={openMaps}
240
+ style={{
241
+ padding: '6px 12px',
242
+ background: isDark ? '#333' : '#f3f4f6',
243
+ border: 'none',
244
+ borderRadius: '6px',
245
+ fontSize: '13px',
246
+ color: isDark ? '#fff' : '#111',
247
+ cursor: 'pointer',
248
+ display: 'flex',
249
+ alignItems: 'center',
250
+ gap: '6px',
251
+ }}
252
+ >
253
+ <Navigation size={14} />
254
+ Get Directions
255
+ </button>
256
+ </div>
257
+ </div>
258
+
259
+ {/* Phone */}
260
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
261
+ <Phone size={20} style={{ color: isDark ? '#999' : '#666' }} />
262
+ <button
263
+ onClick={callPhone}
264
+ style={{
265
+ background: 'none',
266
+ border: 'none',
267
+ fontSize: '14px',
268
+ color: isDark ? '#60a5fa' : '#2563eb',
269
+ cursor: 'pointer',
270
+ textDecoration: 'underline',
271
+ }}
272
+ >
273
+ {shop.phone}
274
+ </button>
275
+ </div>
276
+
277
+ {/* Website */}
278
+ {shop.website && (
279
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
280
+ <Globe size={20} style={{ color: isDark ? '#999' : '#666' }} />
281
+ <button
282
+ onClick={visitWebsite}
283
+ style={{
284
+ background: 'none',
285
+ border: 'none',
286
+ fontSize: '14px',
287
+ color: isDark ? '#60a5fa' : '#2563eb',
288
+ cursor: 'pointer',
289
+ textDecoration: 'underline',
290
+ }}
291
+ >
292
+ Visit Website
293
+ </button>
294
+ </div>
295
+ )}
296
+
297
+ {/* Hours */}
298
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
299
+ <Clock size={20} style={{ color: isDark ? '#999' : '#666' }} />
300
+ <span style={{ fontSize: '14px', color: isDark ? '#ccc' : '#666' }}>
301
+ {shop.hours.open} - {shop.hours.close}
302
+ </span>
303
+ </div>
304
+ </div>
305
+
306
+ {/* Related Shops */}
307
+ {relatedShops.length > 0 && (
308
+ <div>
309
+ <h3 style={{
310
+ margin: '0 0 16px 0',
311
+ fontSize: '18px',
312
+ fontWeight: '600',
313
+ color: isDark ? '#fff' : '#111',
314
+ }}>
315
+ You Might Also Like
316
+ </h3>
317
+ <div style={{ display: 'grid', gap: '12px' }}>
318
+ {relatedShops.map(related => (
319
+ <div
320
+ key={related.id}
321
+ style={{
322
+ background: isDark ? '#1a1a1a' : '#fff',
323
+ border: `1px solid ${isDark ? '#333' : '#e5e7eb'}`,
324
+ borderRadius: '12px',
325
+ padding: '16px',
326
+ display: 'flex',
327
+ gap: '16px',
328
+ }}
329
+ >
330
+ <img
331
+ src={related.image}
332
+ alt={related.name}
333
+ style={{
334
+ width: '80px',
335
+ height: '80px',
336
+ borderRadius: '8px',
337
+ objectFit: 'cover',
338
+ }}
339
+ />
340
+ <div style={{ flex: 1 }}>
341
+ <h4 style={{
342
+ margin: '0 0 4px 0',
343
+ fontSize: '16px',
344
+ fontWeight: '600',
345
+ color: isDark ? '#fff' : '#111',
346
+ }}>
347
+ {related.name}
348
+ </h4>
349
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
350
+ <Star size={14} fill="#fbbf24" stroke="#fbbf24" />
351
+ <span style={{ fontSize: '14px', color: isDark ? '#ccc' : '#666' }}>
352
+ {related.rating}
353
+ </span>
354
+ </div>
355
+ <p style={{
356
+ margin: 0,
357
+ fontSize: '13px',
358
+ color: isDark ? '#999' : '#666',
359
+ overflow: 'hidden',
360
+ textOverflow: 'ellipsis',
361
+ whiteSpace: 'nowrap',
362
+ }}>
363
+ {related.description}
364
+ </p>
365
+ </div>
366
+ </div>
367
+ ))}
368
+ </div>
369
+ </div>
370
+ )}
371
+ </div>
372
+ </div>
373
+ );
374
+ }