@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,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
|
+
}
|