@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,285 @@
1
+ import { Duffel } from '@duffel/api';
2
+ import { Injectable } from 'nitrostack';
3
+
4
+ /**
5
+ * Duffel API Service
6
+ *
7
+ * Handles all interactions with the Duffel API for flight search and booking.
8
+ * Implements best practices from Duffel documentation.
9
+ */
10
+ @Injectable()
11
+ export class DuffelService {
12
+ private duffel: Duffel;
13
+
14
+ constructor() {
15
+ const apiKey = process.env.DUFFEL_API_KEY;
16
+ if (!apiKey) {
17
+ throw new Error('DUFFEL_API_KEY environment variable is required');
18
+ }
19
+
20
+ this.duffel = new Duffel({
21
+ token: apiKey
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Search for flights using offer requests
27
+ *
28
+ * @param params Search parameters
29
+ * @returns Flight offers
30
+ */
31
+ async searchFlights(params: {
32
+ origin: string;
33
+ destination: string;
34
+ departureDate: string;
35
+ returnDate?: string;
36
+ passengers: Array<{ type: 'adult' } | { type: 'child'; age: number } | { type: 'infant_without_seat' }>;
37
+ cabinClass?: 'economy' | 'premium_economy' | 'business' | 'first';
38
+ maxConnections?: number;
39
+ departureTime?: { from: string; to: string };
40
+ arrivalTime?: { from: string; to: string };
41
+ }) {
42
+ try {
43
+ const slices: any[] = [
44
+ {
45
+ origin: params.origin,
46
+ destination: params.destination,
47
+ departure_date: params.departureDate,
48
+ ...(params.departureTime && { departure_time: params.departureTime }),
49
+ ...(params.arrivalTime && { arrival_time: params.arrivalTime })
50
+ }
51
+ ];
52
+
53
+ // Add return slice if round trip
54
+ if (params.returnDate) {
55
+ slices.push({
56
+ origin: params.destination,
57
+ destination: params.origin,
58
+ departure_date: params.returnDate
59
+ });
60
+ }
61
+
62
+ const offerRequest = await this.duffel.offerRequests.create({
63
+ slices,
64
+ passengers: params.passengers as any,
65
+ cabin_class: params.cabinClass,
66
+ max_connections: params.maxConnections as 0 | 1 | 2 | undefined,
67
+ return_offers: true
68
+ });
69
+
70
+ return {
71
+ id: offerRequest.data.id,
72
+ offers: offerRequest.data.offers || [],
73
+ passengers: offerRequest.data.passengers,
74
+ slices: offerRequest.data.slices
75
+ };
76
+ } catch (error: any) {
77
+ throw new Error(`Flight search failed: ${error.message || JSON.stringify(error.errors || error)}`);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get a specific offer by ID
83
+ */
84
+ async getOffer(offerId: string) {
85
+ try {
86
+ const offer = await this.duffel.offers.get(offerId);
87
+ return offer.data;
88
+ } catch (error: any) {
89
+ throw new Error(`Failed to get offer: ${error.message}`);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Get available seats for an offer
95
+ */
96
+ async getAvailableSeats(offerId: string) {
97
+ try {
98
+ const seatMaps = await this.duffel.seatMaps.get({ offer_id: offerId });
99
+ return seatMaps.data;
100
+ } catch (error: any) {
101
+ throw new Error(`Failed to get seat maps: ${error.message}`);
102
+ }
103
+ }
104
+
105
+
106
+ /**
107
+ * Generate a simple UUID-like identifier
108
+ */
109
+ private generateId(): string {
110
+ return 'pas_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
111
+ }
112
+
113
+ /**
114
+ * Create an order (book a flight) with optional hold
115
+ * This creates a new offer request with passenger details to get passenger IDs,
116
+ * then uses those IDs to create the order
117
+ */
118
+ async createOrder(params: {
119
+ selectedOffers: string[];
120
+ passengers: Array<{
121
+ title: 'mr' | 'ms' | 'mrs' | 'miss' | 'dr';
122
+ given_name: string;
123
+ family_name: string;
124
+ gender: 'M' | 'F';
125
+ born_on: string;
126
+ email: string;
127
+ phone_number: string;
128
+ }>;
129
+ }) {
130
+ try {
131
+ // Step 1: Get the offer to extract flight details
132
+ const offer = await this.duffel.offers.get(params.selectedOffers[0]);
133
+ const offerData = offer.data;
134
+
135
+ // Step 2: Create a new offer request with passenger details to get passenger IDs
136
+ const offerRequest = await this.duffel.offerRequests.create({
137
+ slices: offerData.slices.map((slice: any) => ({
138
+ origin: slice.origin.iata_code,
139
+ destination: slice.destination.iata_code,
140
+ departure_date: slice.segments[0].departing_at.split('T')[0]
141
+ })),
142
+ passengers: params.passengers.map(p => ({
143
+ type: 'adult', // Simplified - you can enhance this based on age
144
+ given_name: p.given_name,
145
+ family_name: p.family_name,
146
+ title: p.title,
147
+ gender: p.gender.toLowerCase() as 'm' | 'f',
148
+ born_on: p.born_on,
149
+ email: p.email,
150
+ phone_number: p.phone_number
151
+ })),
152
+ cabin_class: (offerData as any).cabin_class || 'economy',
153
+ return_offers: true // We need offers to be returned!
154
+ } as any);
155
+
156
+ const passengerIds = offerRequest.data.passengers.map((p: any) => p.id);
157
+
158
+ // Step 3: Create order using passenger IDs from the NEW offer request
159
+ // We need to use an offer from THIS offer request, not the original one
160
+ const newOfferId = (offerRequest.data as any).offers?.[0]?.id;
161
+
162
+ if (!newOfferId) {
163
+ throw new Error('No offers returned from offer request with passenger details');
164
+ }
165
+
166
+ const orderData: any = {
167
+ selected_offers: [newOfferId], // Use offer from the NEW request
168
+ passengers: passengerIds.map((id: string, index: number) => ({
169
+ id: id,
170
+ ...params.passengers[index],
171
+ gender: params.passengers[index].gender.toLowerCase()
172
+ })),
173
+ type: 'hold' // Always create hold orders
174
+ };
175
+
176
+ const order = await this.duffel.orders.create(orderData);
177
+ return order.data;
178
+ } catch (error: any) {
179
+ // Log detailed error information
180
+ const errorDetails = {
181
+ message: error.message,
182
+ errors: error.errors,
183
+ response: error.response?.data,
184
+ status: error.response?.status
185
+ };
186
+ console.error('Duffel order creation failed:', JSON.stringify(errorDetails, null, 2));
187
+
188
+ throw new Error(`Failed to create order: ${error.message || JSON.stringify(error.errors || error.response?.data || error)}`);
189
+ }
190
+ }
191
+
192
+
193
+
194
+ /**
195
+ * Get available seats for an offer
196
+ */
197
+ async getSeatsForOffer(offerId: string) {
198
+ try {
199
+ const seatMaps = await this.duffel.seatMaps.get({ offer_id: offerId });
200
+ return seatMaps.data;
201
+ } catch (error: any) {
202
+ throw new Error(`Failed to get seats: ${error.message || JSON.stringify(error.errors || error)}`);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Create order change request for seats
208
+ */
209
+ async createOrderChangeForSeats(orderId: string) {
210
+ try {
211
+ const changeRequest = await this.duffel.orderChangeRequests.create({
212
+ order_id: orderId
213
+ } as any);
214
+ return changeRequest.data;
215
+ } catch (error: any) {
216
+ throw new Error(`Failed to create order change: ${error.message || JSON.stringify(error.errors || error)}`);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Get available services (baggage, seats) for an order
222
+ */
223
+ async getAvailableServices(orderId: string) {
224
+ try {
225
+ const services = await this.duffel.orderChangeRequests.create({
226
+ order_id: orderId
227
+ } as any);
228
+ return services.data;
229
+ } catch (error: any) {
230
+ throw new Error(`Failed to get available services: ${error.message || JSON.stringify(error.errors || error)}`);
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Get order details
236
+ */
237
+ async getOrder(orderId: string) {
238
+ try {
239
+ const order = await this.duffel.orders.get(orderId);
240
+ return order.data;
241
+ } catch (error: any) {
242
+ throw new Error(`Failed to get order: ${error.message}`);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Cancel an order
248
+ */
249
+ async cancelOrder(orderId: string) {
250
+ try {
251
+ const cancellation = await this.duffel.orderCancellations.create({
252
+ order_id: orderId
253
+ });
254
+ return cancellation.data;
255
+ } catch (error: any) {
256
+ throw new Error(`Failed to cancel order: ${error.message}`);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Search for airports by query
262
+ */
263
+ async searchAirports(query: string) {
264
+ try {
265
+ const suggestions = await this.duffel.suggestions.list({
266
+ query: query
267
+ });
268
+ return suggestions.data;
269
+ } catch (error: any) {
270
+ throw new Error(`Failed to search airports: ${error.message}`);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Get airlines list
276
+ */
277
+ async getAirlines() {
278
+ try {
279
+ const airlines = await this.duffel.airlines.list();
280
+ return airlines.data;
281
+ } catch (error: any) {
282
+ throw new Error(`Failed to get airlines: ${error.message}`);
283
+ }
284
+ }
285
+ }
@@ -0,0 +1,270 @@
1
+ 'use client';
2
+
3
+ import { useWidgetSDK, useTheme } from '@nitrostack/widgets';
4
+
5
+ /**
6
+ * Airport Search Widget
7
+ *
8
+ * Compact display of airport search results with IATA codes and locations.
9
+ */
10
+
11
+ interface AirportResult {
12
+ id: string;
13
+ name: string;
14
+ iataCode: string;
15
+ icaoCode?: string;
16
+ cityName?: string;
17
+ type: string;
18
+ latitude?: number;
19
+ longitude?: number;
20
+ timeZone?: string;
21
+ }
22
+
23
+ interface AirportSearchData {
24
+ query: string;
25
+ results: AirportResult[];
26
+ }
27
+
28
+ export default function AirportSearch() {
29
+ const { getToolOutput } = useWidgetSDK();
30
+ const theme = useTheme();
31
+ const data = getToolOutput<AirportSearchData>();
32
+
33
+ const isDark = theme === 'dark';
34
+
35
+ const getTypeIcon = (type: string) => {
36
+ const icons: Record<string, string> = {
37
+ 'airport': '✈️',
38
+ 'city': '🏙️',
39
+ 'station': '🚉',
40
+ 'bus_station': '🚌',
41
+ 'heliport': '🚁'
42
+ };
43
+ return icons[type] || '📍';
44
+ };
45
+
46
+ if (!data) {
47
+ return (
48
+ <div style={{ padding: '24px', textAlign: 'center', color: isDark ? '#F8FAFC' : '#020617' }}>
49
+ Loading...
50
+ </div>
51
+ );
52
+ }
53
+
54
+ return (
55
+ <div className={isDark ? 'dark' : ''} style={{
56
+ padding: '16px',
57
+ background: isDark ? '#020617' : '#FFFFFF',
58
+ color: isDark ? '#F8FAFC' : '#020617'
59
+ }}>
60
+ {/* Header */}
61
+ <div style={{
62
+ background: isDark ? '#0F172A' : '#F8FAFC',
63
+ borderRadius: '12px',
64
+ padding: '16px',
65
+ marginBottom: '16px',
66
+ border: `1px solid ${isDark ? '#334155' : '#E2E8F0'}`
67
+ }}>
68
+ <div style={{
69
+ display: 'flex',
70
+ alignItems: 'center',
71
+ gap: '10px',
72
+ marginBottom: '8px'
73
+ }}>
74
+ <span style={{ fontSize: '24px' }}>🔍</span>
75
+ <h2 style={{
76
+ margin: 0,
77
+ fontSize: '18px',
78
+ fontWeight: 700
79
+ }}>
80
+ Airport Search
81
+ </h2>
82
+ </div>
83
+ <p style={{
84
+ margin: '8px 0 0 34px',
85
+ fontSize: '14px',
86
+ color: isDark ? '#94A3B8' : '#64748B'
87
+ }}>
88
+ Searching: <strong>"{data.query}"</strong>
89
+ </p>
90
+ <div style={{
91
+ marginTop: '8px',
92
+ marginLeft: '34px',
93
+ fontSize: '12px',
94
+ color: isDark ? '#94A3B8' : '#64748B'
95
+ }}>
96
+ {data.results.length} result{data.results.length !== 1 ? 's' : ''}
97
+ </div>
98
+ </div>
99
+
100
+ {/* Results */}
101
+ {data.results.length > 0 ? (
102
+ <div style={{
103
+ display: 'flex',
104
+ gap: '12px',
105
+ overflowX: 'auto',
106
+ paddingBottom: '12px',
107
+ scrollbarWidth: 'thin',
108
+ scrollbarColor: isDark ? '#334155 #0F172A' : '#CBD5E1 #F1F5F9'
109
+ }}>
110
+ {data.results.map((airport) => (
111
+ <div key={airport.id} style={{
112
+ minWidth: '300px',
113
+ maxWidth: '300px',
114
+ background: isDark ? '#1a1a1a' : '#ffffff',
115
+ border: `1px solid ${isDark ? '#333' : '#e5e7eb'}`,
116
+ borderRadius: '12px',
117
+ padding: '16px',
118
+ boxShadow: isDark ? '0 2px 8px rgba(0,0,0,0.3)' : '0 2px 8px rgba(0,0,0,0.1)',
119
+ transition: 'all 0.2s ease',
120
+ cursor: 'pointer'
121
+ }}
122
+ onMouseEnter={(e) => {
123
+ e.currentTarget.style.transform = 'translateY(-2px)';
124
+ e.currentTarget.style.boxShadow = isDark
125
+ ? '0 4px 12px rgba(59, 159, 255, 0.2)'
126
+ : '0 4px 12px rgba(59, 159, 255, 0.15)';
127
+ }}
128
+ onMouseLeave={(e) => {
129
+ e.currentTarget.style.transform = 'translateY(0)';
130
+ e.currentTarget.style.boxShadow = isDark
131
+ ? '0 2px 8px rgba(0,0,0,0.3)'
132
+ : '0 4px 12px rgba(0,0,0,0.1)';
133
+ }}>
134
+ <div style={{
135
+ display: 'flex',
136
+ justifyContent: 'space-between',
137
+ alignItems: 'flex-start',
138
+ gap: '12px'
139
+ }}>
140
+ {/* Left side */}
141
+ <div style={{ flex: 1 }}>
142
+ <div style={{
143
+ display: 'flex',
144
+ alignItems: 'center',
145
+ gap: '10px',
146
+ marginBottom: '6px'
147
+ }}>
148
+ <span style={{ fontSize: '20px' }}>
149
+ {getTypeIcon(airport.type)}
150
+ </span>
151
+ <div>
152
+ <h3 style={{
153
+ margin: 0,
154
+ fontSize: '16px',
155
+ fontWeight: 600
156
+ }}>
157
+ {airport.name}
158
+ </h3>
159
+ {airport.cityName && (
160
+ <p style={{
161
+ margin: '2px 0 0 0',
162
+ fontSize: '12px',
163
+ color: isDark ? '#94A3B8' : '#64748B'
164
+ }}>
165
+ 📍 {airport.cityName}
166
+ </p>
167
+ )}
168
+ </div>
169
+ </div>
170
+
171
+ {/* Additional details */}
172
+ <div style={{
173
+ display: 'flex',
174
+ gap: '12px',
175
+ marginTop: '8px',
176
+ flexWrap: 'wrap',
177
+ fontSize: '11px',
178
+ color: isDark ? '#94A3B8' : '#64748B'
179
+ }}>
180
+ {airport.timeZone && (
181
+ <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
182
+ <span>🕐</span>
183
+ <span>{airport.timeZone}</span>
184
+ </div>
185
+ )}
186
+ {airport.latitude && airport.longitude && (
187
+ <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
188
+ <span>🌍</span>
189
+ <span>{airport.latitude.toFixed(2)}, {airport.longitude.toFixed(2)}</span>
190
+ </div>
191
+ )}
192
+ </div>
193
+ </div>
194
+
195
+ {/* Right side - Codes */}
196
+ <div style={{
197
+ display: 'flex',
198
+ flexDirection: 'column',
199
+ gap: '6px',
200
+ alignItems: 'flex-end'
201
+ }}>
202
+ {/* IATA Code */}
203
+ <div style={{
204
+ background: 'var(--primary)',
205
+ color: 'white',
206
+ padding: '6px 12px',
207
+ borderRadius: '8px',
208
+ fontWeight: 700,
209
+ fontSize: '16px',
210
+ letterSpacing: '0.5px'
211
+ }}>
212
+ {airport.iataCode || 'N/A'}
213
+ </div>
214
+
215
+ {/* ICAO Code */}
216
+ {airport.icaoCode && (
217
+ <div style={{
218
+ background: isDark ? '#1E293B' : '#F1F5F9',
219
+ color: isDark ? '#CBD5E1' : '#475569',
220
+ padding: '3px 10px',
221
+ borderRadius: '6px',
222
+ fontSize: '10px',
223
+ fontWeight: 600
224
+ }}>
225
+ ICAO: {airport.icaoCode}
226
+ </div>
227
+ )}
228
+
229
+ {/* Type badge */}
230
+ <div className="badge badge-info" style={{ fontSize: '10px' }}>
231
+ {airport.type.replace('_', ' ')}
232
+ </div>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ ))}
237
+ </div>
238
+ ) : (
239
+ <div style={{
240
+ background: isDark ? '#0F172A' : '#F8FAFC',
241
+ borderRadius: '12px',
242
+ padding: '32px',
243
+ textAlign: 'center'
244
+ }}>
245
+ <div style={{ fontSize: '48px', marginBottom: '12px' }}>🔍</div>
246
+ <div style={{ fontSize: '16px', marginBottom: '6px' }}>
247
+ No airports found
248
+ </div>
249
+ <div style={{ fontSize: '12px', color: isDark ? '#94A3B8' : '#64748B' }}>
250
+ Try a different city or airport code
251
+ </div>
252
+ </div>
253
+ )}
254
+
255
+ {/* Help text */}
256
+ <div style={{
257
+ background: isDark ? '#0F172A' : '#F8FAFC',
258
+ borderRadius: '8px',
259
+ padding: '12px',
260
+ marginTop: '12px',
261
+ fontSize: '11px',
262
+ color: isDark ? '#94A3B8' : '#64748B',
263
+ textAlign: 'center',
264
+ border: `1px solid ${isDark ? '#334155' : '#E2E8F0'}`
265
+ }}>
266
+ 💡 <strong>Tip:</strong> Use the IATA code (3-letter) for flight searches
267
+ </div>
268
+ </div>
269
+ );
270
+ }