@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,261 @@
1
+ 'use client';
2
+
3
+ import { useWidgetSDK, useTheme } from '@nitrostack/widgets';
4
+
5
+ /**
6
+ * Flight Details Widget - Compact view with segments, baggage, and fare conditions
7
+ */
8
+
9
+ interface Segment {
10
+ id: string;
11
+ origin: string;
12
+ destination: string;
13
+ departingAt: string;
14
+ arrivingAt: string;
15
+ duration: string;
16
+ airline: { name: string; code: string; flightNumber: string };
17
+ aircraft?: string;
18
+ }
19
+
20
+ interface Slice {
21
+ origin: { code: string; name: string; city: string };
22
+ destination: { code: string; name: string; city: string };
23
+ duration: string;
24
+ segments: Segment[];
25
+ }
26
+
27
+ interface FlightDetailsData {
28
+ id: string;
29
+ totalAmount: string;
30
+ totalCurrency: string;
31
+ slices: Slice[];
32
+ passengers: Array<{ id: string; type: string; baggageAllowance?: Array<{ type: string; quantity: number }> }>;
33
+ conditions: {
34
+ refundBeforeDeparture: { allowed: boolean; penaltyAmount?: string; penaltyCurrency?: string };
35
+ changeBeforeDeparture: { allowed: boolean; penaltyAmount?: string; penaltyCurrency?: string };
36
+ };
37
+ }
38
+
39
+ export default function FlightDetails() {
40
+ const { getToolOutput } = useWidgetSDK();
41
+ const theme = useTheme();
42
+ const data = getToolOutput<FlightDetailsData>();
43
+
44
+ const isDark = theme === 'dark';
45
+
46
+ const formatDuration = (duration: string) => {
47
+ const match = duration.match(/PT(\d+H)?(\d+M)?/);
48
+ if (!match) return duration;
49
+ const hours = match[1] ? parseInt(match[1]) : 0;
50
+ const minutes = match[2] ? parseInt(match[2]) : 0;
51
+ return `${hours}h ${minutes}m`;
52
+ };
53
+
54
+ const formatTime = (isoString: string) => {
55
+ const date = new Date(isoString);
56
+ return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
57
+ };
58
+
59
+ const formatDate = (isoString: string) => {
60
+ const date = new Date(isoString);
61
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
62
+ };
63
+
64
+ if (!data) {
65
+ return <div style={{ padding: '24px', textAlign: 'center' }}>Loading...</div>;
66
+ }
67
+
68
+ return (
69
+ <div className={isDark ? 'dark' : ''} style={{
70
+ padding: '16px',
71
+ background: isDark ? '#020617' : '#FFFFFF',
72
+ color: isDark ? '#F8FAFC' : '#020617'
73
+ }}>
74
+ {/* Header */}
75
+ <div className="card" style={{ marginBottom: '16px' }}>
76
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '12px' }}>
77
+ <div>
78
+ <h2 style={{ margin: 0, fontSize: '18px', fontWeight: 700 }}>Flight Details</h2>
79
+ <p style={{ margin: '4px 0 0 0', fontSize: '11px', color: isDark ? '#94A3B8' : '#64748B' }}>
80
+ Offer ID: {data.id}
81
+ </p>
82
+ </div>
83
+ <div style={{ textAlign: 'right' }}>
84
+ <div style={{ color: 'var(--primary)', fontSize: '24px', fontWeight: 700 }}>
85
+ {data.totalCurrency} {parseFloat(data.totalAmount).toFixed(2)}
86
+ </div>
87
+ <div style={{ fontSize: '10px', color: isDark ? '#94A3B8' : '#64748B' }}>Total Price</div>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ {/* Flight Itinerary */}
93
+ {data.slices.map((slice, sliceIndex) => (
94
+ <div key={sliceIndex} className="card" style={{ marginBottom: '12px' }}>
95
+ <h3 style={{ margin: '0 0 12px 0', fontSize: '14px', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '6px' }}>
96
+ <span>✈️</span>
97
+ <span>{slice.origin.city} → {slice.destination.city}</span>
98
+ </h3>
99
+
100
+ <div style={{
101
+ background: isDark ? '#0F172A' : '#F8FAFC',
102
+ borderRadius: '8px',
103
+ padding: '12px',
104
+ marginBottom: '12px',
105
+ fontSize: '12px',
106
+ textAlign: 'center',
107
+ color: '#3B9FFF',
108
+ fontWeight: 600
109
+ }}>
110
+ Duration: {formatDuration(slice.duration)}
111
+ </div>
112
+
113
+ {/* Segments */}
114
+ {slice.segments.map((segment) => (
115
+ <div key={segment.id} style={{
116
+ background: isDark ? '#1F2937' : '#F9FAFB',
117
+ borderRadius: '8px',
118
+ padding: '12px',
119
+ marginBottom: '8px',
120
+ border: `1px solid ${isDark ? '#334155' : '#E2E8F0'}`
121
+ }}>
122
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '10px' }}>
123
+ <div style={{ fontSize: '14px', fontWeight: 600 }}>
124
+ {segment.airline.name} {segment.airline.flightNumber}
125
+ </div>
126
+ <div style={{ fontSize: '11px', color: isDark ? '#94A3B8' : '#64748B' }}>
127
+ {formatDuration(segment.duration)}
128
+ </div>
129
+ </div>
130
+
131
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr auto 1fr', gap: '12px', alignItems: 'center' }}>
132
+ <div>
133
+ <div style={{ fontSize: '18px', fontWeight: 700 }}>{formatTime(segment.departingAt)}</div>
134
+ <div style={{ fontSize: '12px', color: isDark ? '#94A3B8' : '#64748B', marginTop: '2px' }}>
135
+ {segment.origin}
136
+ </div>
137
+ <div style={{ fontSize: '10px', color: isDark ? '#64748B' : '#94A3B8', marginTop: '2px' }}>
138
+ {formatDate(segment.departingAt)}
139
+ </div>
140
+ </div>
141
+
142
+ <div style={{
143
+ width: '40px',
144
+ height: '2px',
145
+ background: 'linear-gradient(90deg, #3B9FFF 0%, #2563EB 100%)',
146
+ position: 'relative'
147
+ }}>
148
+ <div style={{
149
+ position: 'absolute',
150
+ right: '-4px',
151
+ top: '50%',
152
+ transform: 'translateY(-50%)',
153
+ width: 0,
154
+ height: 0,
155
+ borderLeft: '6px solid #2563EB',
156
+ borderTop: '3px solid transparent',
157
+ borderBottom: '3px solid transparent'
158
+ }} />
159
+ </div>
160
+
161
+ <div style={{ textAlign: 'right' }}>
162
+ <div style={{ fontSize: '18px', fontWeight: 700 }}>{formatTime(segment.arrivingAt)}</div>
163
+ <div style={{ fontSize: '12px', color: isDark ? '#94A3B8' : '#64748B', marginTop: '2px' }}>
164
+ {segment.destination}
165
+ </div>
166
+ <div style={{ fontSize: '10px', color: isDark ? '#64748B' : '#94A3B8', marginTop: '2px' }}>
167
+ {formatDate(segment.arrivingAt)}
168
+ </div>
169
+ </div>
170
+ </div>
171
+
172
+ {segment.aircraft && (
173
+ <div style={{
174
+ marginTop: '10px',
175
+ paddingTop: '10px',
176
+ borderTop: `1px solid ${isDark ? '#334155' : '#E2E8F0'}`,
177
+ fontSize: '11px',
178
+ color: isDark ? '#94A3B8' : '#64748B'
179
+ }}>
180
+ ✈️ {segment.aircraft}
181
+ </div>
182
+ )}
183
+ </div>
184
+ ))}
185
+ </div>
186
+ ))}
187
+
188
+ {/* Baggage */}
189
+ <div className="card" style={{ marginBottom: '12px' }}>
190
+ <h3 style={{ margin: '0 0 12px 0', fontSize: '14px', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '6px' }}>
191
+ <span>🧳</span>
192
+ <span>Baggage Allowance</span>
193
+ </h3>
194
+ {data.passengers.map((passenger, index) => (
195
+ <div key={passenger.id} style={{
196
+ background: isDark ? '#0F172A' : '#F8FAFC',
197
+ borderRadius: '8px',
198
+ padding: '12px',
199
+ marginBottom: index < data.passengers.length - 1 ? '8px' : '0'
200
+ }}>
201
+ <div style={{ fontSize: '12px', fontWeight: 600, marginBottom: '6px' }}>
202
+ Passenger {index + 1} ({passenger.type})
203
+ </div>
204
+ {passenger.baggageAllowance && passenger.baggageAllowance.length > 0 ? (
205
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
206
+ {passenger.baggageAllowance.map((bag, bagIndex) => (
207
+ <span key={bagIndex} className="badge badge-info" style={{ fontSize: '11px' }}>
208
+ {bag.quantity}x {bag.type.replace('_', ' ')}
209
+ </span>
210
+ ))}
211
+ </div>
212
+ ) : (
213
+ <div style={{ fontSize: '11px', color: isDark ? '#94A3B8' : '#64748B' }}>
214
+ No baggage info
215
+ </div>
216
+ )}
217
+ </div>
218
+ ))}
219
+ </div>
220
+
221
+ {/* Fare Conditions */}
222
+ {data.conditions && (
223
+ <div className="card">
224
+ <h3 style={{ margin: '0 0 12px 0', fontSize: '14px', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '6px' }}>
225
+ <span>📋</span>
226
+ <span>Fare Conditions</span>
227
+ </h3>
228
+ <div style={{ display: 'grid', gap: '8px' }}>
229
+ <div className={data.conditions.refundBeforeDeparture?.allowed ? 'badge-success' : 'badge-warning'} style={{
230
+ padding: '12px',
231
+ borderRadius: '8px'
232
+ }}>
233
+ <div style={{ fontSize: '12px', fontWeight: 600, marginBottom: '4px' }}>
234
+ {data.conditions.refundBeforeDeparture?.allowed ? '✓' : '✗'} Refund Before Departure
235
+ </div>
236
+ {data.conditions.refundBeforeDeparture?.penaltyAmount && (
237
+ <div style={{ fontSize: '10px', opacity: 0.8 }}>
238
+ Penalty: {data.conditions.refundBeforeDeparture.penaltyCurrency} {data.conditions.refundBeforeDeparture.penaltyAmount}
239
+ </div>
240
+ )}
241
+ </div>
242
+
243
+ <div className={data.conditions.changeBeforeDeparture?.allowed ? 'badge-success' : 'badge-warning'} style={{
244
+ padding: '12px',
245
+ borderRadius: '8px'
246
+ }}>
247
+ <div style={{ fontSize: '12px', fontWeight: 600, marginBottom: '4px' }}>
248
+ {data.conditions.changeBeforeDeparture?.allowed ? '✓' : '✗'} Changes Before Departure
249
+ </div>
250
+ {data.conditions.changeBeforeDeparture?.penaltyAmount && (
251
+ <div style={{ fontSize: '10px', opacity: 0.8 }}>
252
+ Penalty: {data.conditions.changeBeforeDeparture.penaltyCurrency} {data.conditions.changeBeforeDeparture.penaltyAmount}
253
+ </div>
254
+ )}
255
+ </div>
256
+ </div>
257
+ </div>
258
+ )}
259
+ </div>
260
+ );
261
+ }
@@ -0,0 +1,378 @@
1
+ 'use client';
2
+
3
+ import { useWidgetSDK, useTheme } from '@nitrostack/widgets';
4
+
5
+ /**
6
+ * Flight Search Results Widget
7
+ *
8
+ * Modern, compact display of flight search results with Nitrocloud branding.
9
+ */
10
+
11
+ interface FlightSegment {
12
+ origin: string;
13
+ destination: string;
14
+ departureTime: string;
15
+ arrivalTime: string;
16
+ duration: string;
17
+ stops: number;
18
+ airline: string;
19
+ flightNumber: string;
20
+ }
21
+
22
+ interface FlightOffer {
23
+ id: string;
24
+ totalAmount: string;
25
+ totalCurrency: string;
26
+ outbound: FlightSegment;
27
+ return?: FlightSegment;
28
+ fareType: string;
29
+ refundable: boolean;
30
+ changeable: boolean;
31
+ }
32
+
33
+ interface FlightSearchData {
34
+ searchParams: {
35
+ origin: string;
36
+ destination: string;
37
+ departureDate: string;
38
+ returnDate?: string;
39
+ passengers: {
40
+ adults: number;
41
+ children: number;
42
+ infants: number;
43
+ };
44
+ cabinClass: string;
45
+ };
46
+ totalOffers: number;
47
+ offers: FlightOffer[];
48
+ }
49
+
50
+ export default function FlightSearchResults() {
51
+ const { getToolOutput, callTool } = useWidgetSDK();
52
+ const theme = useTheme();
53
+ const data = getToolOutput<FlightSearchData>();
54
+ const isDark = theme === 'dark';
55
+
56
+ const handleFlightClick = async (offerId: string) => {
57
+ try {
58
+ await callTool('get_flight_details', { offerId });
59
+ } catch (error) {
60
+ console.error('Failed to get flight details:', error);
61
+ }
62
+ };
63
+
64
+ const formatDuration = (duration: string) => {
65
+ const match = duration.match(/PT(\d+H)?(\d+M)?/);
66
+ if (!match) return duration;
67
+ const hours = match[1] ? parseInt(match[1]) : 0;
68
+ const minutes = match[2] ? parseInt(match[2]) : 0;
69
+ return `${hours}h ${minutes}m`;
70
+ };
71
+
72
+ const formatTime = (isoString: string) => {
73
+ return new Date(isoString).toLocaleTimeString('en-US', {
74
+ hour: '2-digit',
75
+ minute: '2-digit',
76
+ hour12: false
77
+ });
78
+ };
79
+
80
+ const formatDate = (dateString: string) => {
81
+ return new Date(dateString).toLocaleDateString('en-US', {
82
+ month: 'short',
83
+ day: 'numeric'
84
+ });
85
+ };
86
+
87
+ const getAirlineInitials = (name: string) => {
88
+ return name.split(' ').map(w => w[0]).join('').substring(0, 2).toUpperCase();
89
+ };
90
+
91
+ if (!data?.searchParams) {
92
+ return (
93
+ <div style={{
94
+ padding: '24px',
95
+ textAlign: 'center',
96
+ color: isDark ? '#F8FAFC' : '#020617'
97
+ }}>
98
+ <div style={{ fontSize: '48px', marginBottom: '12px' }}>⚠️</div>
99
+ <div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '8px' }}>
100
+ Invalid Data
101
+ </div>
102
+ <div style={{ fontSize: '14px', color: isDark ? '#94A3B8' : '#64748B' }}>
103
+ Flight search data is missing
104
+ </div>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ const totalPassengers = (data.searchParams.passengers?.adults || 0) +
110
+ (data.searchParams.passengers?.children || 0) +
111
+ (data.searchParams.passengers?.infants || 0);
112
+
113
+ const FlightSegment = ({ segment, label }: { segment: FlightSegment; label: string }) => (
114
+ <div style={{
115
+ background: isDark ? '#0F172A' : '#F8FAFC',
116
+ borderRadius: '8px',
117
+ padding: '12px',
118
+ marginBottom: '8px'
119
+ }}>
120
+ <div style={{
121
+ fontSize: '12px',
122
+ fontWeight: 600,
123
+ color: '#3B9FFF',
124
+ marginBottom: '8px',
125
+ display: 'flex',
126
+ alignItems: 'center',
127
+ gap: '6px'
128
+ }}>
129
+ <span>{label === 'Outbound' ? '✈️' : '🔄'}</span>
130
+ <span>{label}</span>
131
+ </div>
132
+
133
+ <div style={{
134
+ display: 'grid',
135
+ gridTemplateColumns: '1fr auto 1fr',
136
+ gap: '12px',
137
+ alignItems: 'center'
138
+ }}>
139
+ <div style={{ textAlign: 'left' }}>
140
+ <div style={{ fontSize: '18px', fontWeight: 700, color: isDark ? '#F8FAFC' : '#020617' }}>
141
+ {formatTime(segment.departureTime)}
142
+ </div>
143
+ <div style={{ fontSize: '12px', color: isDark ? '#94A3B8' : '#64748B', marginTop: '2px' }}>
144
+ {segment.origin}
145
+ </div>
146
+ </div>
147
+
148
+ <div style={{
149
+ textAlign: 'center',
150
+ padding: '8px',
151
+ background: isDark ? '#1E293B' : 'white',
152
+ borderRadius: '6px',
153
+ minWidth: '100px'
154
+ }}>
155
+ <div style={{ fontSize: '12px', fontWeight: 600, color: isDark ? '#F8FAFC' : '#020617' }}>
156
+ {formatDuration(segment.duration)}
157
+ </div>
158
+ <div style={{
159
+ height: '2px',
160
+ background: 'linear-gradient(90deg, #3B9FFF 0%, #2563EB 100%)',
161
+ margin: '6px 0',
162
+ position: 'relative'
163
+ }}>
164
+ {segment.stops > 0 && (
165
+ <div style={{
166
+ width: '6px',
167
+ height: '6px',
168
+ background: '#3B9FFF',
169
+ borderRadius: '50%',
170
+ position: 'absolute',
171
+ top: '50%',
172
+ left: '50%',
173
+ transform: 'translate(-50%, -50%)'
174
+ }} />
175
+ )}
176
+ </div>
177
+ <div style={{ fontSize: '10px', color: isDark ? '#94A3B8' : '#64748B' }}>
178
+ {segment.stops === 0 ? 'Direct' : `${segment.stops} stop${segment.stops > 1 ? 's' : ''}`}
179
+ </div>
180
+ </div>
181
+
182
+ <div style={{ textAlign: 'right' }}>
183
+ <div style={{ fontSize: '18px', fontWeight: 700, color: isDark ? '#F8FAFC' : '#020617' }}>
184
+ {formatTime(segment.arrivalTime)}
185
+ </div>
186
+ <div style={{ fontSize: '12px', color: isDark ? '#94A3B8' : '#64748B', marginTop: '2px' }}>
187
+ {segment.destination}
188
+ </div>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ );
193
+
194
+ return (
195
+ <div className={isDark ? 'dark' : ''} style={{
196
+ padding: '16px',
197
+ background: isDark ? '#020617' : '#FFFFFF',
198
+ color: isDark ? '#F8FAFC' : '#020617'
199
+ }}>
200
+ {/* Header */}
201
+ <div style={{
202
+ background: isDark ? '#0F172A' : '#F8FAFC',
203
+ borderRadius: '12px',
204
+ padding: '16px',
205
+ marginBottom: '16px',
206
+ border: `1px solid ${isDark ? '#334155' : '#E2E8F0'}`
207
+ }}>
208
+ <div style={{
209
+ display: 'flex',
210
+ alignItems: 'center',
211
+ justifyContent: 'space-between',
212
+ flexWrap: 'wrap',
213
+ gap: '12px',
214
+ marginBottom: '12px'
215
+ }}>
216
+ <div style={{
217
+ fontSize: '18px',
218
+ fontWeight: 700,
219
+ display: 'flex',
220
+ alignItems: 'center',
221
+ gap: '8px'
222
+ }}>
223
+ <span>{data.searchParams.origin}</span>
224
+ <span style={{ color: '#3B9FFF' }}>✈️</span>
225
+ <span>{data.searchParams.destination}</span>
226
+ </div>
227
+
228
+ <div className="badge badge-info">
229
+ {data.totalOffers} flights
230
+ </div>
231
+ </div>
232
+
233
+ <div style={{
234
+ display: 'flex',
235
+ gap: '16px',
236
+ flexWrap: 'wrap',
237
+ fontSize: '12px',
238
+ color: isDark ? '#94A3B8' : '#64748B'
239
+ }}>
240
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
241
+ <span>📅</span>
242
+ <span>
243
+ {formatDate(data.searchParams.departureDate)}
244
+ {data.searchParams.returnDate && ` - ${formatDate(data.searchParams.returnDate)}`}
245
+ </span>
246
+ </div>
247
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
248
+ <span>👥</span>
249
+ <span>{totalPassengers} pax</span>
250
+ </div>
251
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
252
+ <span>💺</span>
253
+ <span>{(data.searchParams.cabinClass || 'economy').replace('_', ' ')}</span>
254
+ </div>
255
+ </div>
256
+ </div>
257
+
258
+ {/* Flight Offers */}
259
+ {data.offers.length > 0 ? (
260
+ <div style={{
261
+ display: 'flex',
262
+ gap: '12px',
263
+ overflowX: 'auto',
264
+ paddingBottom: '12px',
265
+ scrollbarWidth: 'thin',
266
+ scrollbarColor: isDark ? '#334155 #0F172A' : '#CBD5E1 #F1F5F9'
267
+ }}>
268
+ {data.offers.map((offer) => (
269
+ <div key={offer.id} style={{
270
+ minWidth: '320px',
271
+ maxWidth: '320px',
272
+ background: isDark ? '#1a1a1a' : '#ffffff',
273
+ border: `1px solid ${isDark ? '#333' : '#e5e7eb'}`,
274
+ borderRadius: '12px',
275
+ padding: '16px',
276
+ boxShadow: isDark ? '0 2px 8px rgba(0,0,0,0.3)' : '0 2px 8px rgba(0,0,0,0.1)',
277
+ transition: 'all 0.2s ease',
278
+ cursor: 'pointer'
279
+ }}
280
+ onClick={() => handleFlightClick(offer.id)}
281
+ onMouseEnter={(e) => {
282
+ e.currentTarget.style.transform = 'translateY(-2px)';
283
+ e.currentTarget.style.boxShadow = isDark
284
+ ? '0 4px 12px rgba(59, 159, 255, 0.2)'
285
+ : '0 4px 12px rgba(59, 159, 255, 0.15)';
286
+ }}
287
+ onMouseLeave={(e) => {
288
+ e.currentTarget.style.transform = 'translateY(0)';
289
+ e.currentTarget.style.boxShadow = isDark
290
+ ? '0 2px 8px rgba(0,0,0,0.3)'
291
+ : '0 2px 8px rgba(0,0,0,0.1)';
292
+ }}>
293
+ {/* Offer Header */}
294
+ <div style={{
295
+ display: 'flex',
296
+ justifyContent: 'space-between',
297
+ alignItems: 'center',
298
+ marginBottom: '12px',
299
+ paddingBottom: '12px',
300
+ borderBottom: `1px solid ${isDark ? '#334155' : '#E2E8F0'}`
301
+ }}>
302
+ <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
303
+ <div style={{
304
+ width: '40px',
305
+ height: '40px',
306
+ background: 'linear-gradient(135deg, #3B9FFF 0%, #2563EB 100%)',
307
+ borderRadius: '8px',
308
+ display: 'flex',
309
+ alignItems: 'center',
310
+ justifyContent: 'center',
311
+ color: 'white',
312
+ fontWeight: 700,
313
+ fontSize: '14px'
314
+ }}>
315
+ {getAirlineInitials(offer.outbound.airline)}
316
+ </div>
317
+ <div>
318
+ <div style={{ fontSize: '14px', fontWeight: 600 }}>
319
+ {offer.outbound.airline}
320
+ </div>
321
+ <div style={{ fontSize: '11px', color: isDark ? '#94A3B8' : '#64748B' }}>
322
+ {offer.outbound.flightNumber}
323
+ </div>
324
+ </div>
325
+ </div>
326
+
327
+ <div style={{ textAlign: 'right' }}>
328
+ <div style={{
329
+ color: 'var(--primary)',
330
+ fontSize: '24px',
331
+ fontWeight: 700
332
+ }}>
333
+ {offer.totalCurrency} {parseFloat(offer.totalAmount).toFixed(0)}
334
+ </div>
335
+ <div style={{ fontSize: '10px', color: isDark ? '#94A3B8' : '#64748B' }}>
336
+ Total
337
+ </div>
338
+ </div>
339
+ </div>
340
+
341
+ {/* Flight Segments */}
342
+ <FlightSegment segment={offer.outbound} label="Outbound" />
343
+ {offer.return && <FlightSegment segment={offer.return} label="Return" />}
344
+
345
+ {/* Badges */}
346
+ <div style={{
347
+ display: 'flex',
348
+ gap: '6px',
349
+ flexWrap: 'wrap',
350
+ marginTop: '12px'
351
+ }}>
352
+ <span className={offer.refundable ? 'badge badge-success' : 'badge badge-warning'}>
353
+ {offer.refundable ? '✓ Refundable' : '✗ Non-refundable'}
354
+ </span>
355
+ {offer.changeable && (
356
+ <span className="badge badge-success">✓ Changeable</span>
357
+ )}
358
+ <span className="badge badge-info">{offer.fareType}</span>
359
+ </div>
360
+ </div>
361
+ ))}
362
+ </div>
363
+ ) : (
364
+ <div style={{
365
+ background: isDark ? '#0F172A' : '#F8FAFC',
366
+ borderRadius: '12px',
367
+ padding: '32px',
368
+ textAlign: 'center'
369
+ }}>
370
+ <div style={{ fontSize: '48px', marginBottom: '12px' }}>✈️</div>
371
+ <div style={{ fontSize: '14px', color: isDark ? '#94A3B8' : '#64748B' }}>
372
+ No flights found. Try adjusting your search.
373
+ </div>
374
+ </div>
375
+ )}
376
+ </div>
377
+ );
378
+ }