@voyant-travel/flights-react 0.119.2

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 (187) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +74 -0
  3. package/dist/admin/index.d.ts +134 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +122 -0
  6. package/dist/admin/pages/flight-book-page.d.ts +12 -0
  7. package/dist/admin/pages/flight-book-page.d.ts.map +1 -0
  8. package/dist/admin/pages/flight-book-page.js +40 -0
  9. package/dist/admin/pages/flights-index-page.d.ts +14 -0
  10. package/dist/admin/pages/flights-index-page.d.ts.map +1 -0
  11. package/dist/admin/pages/flights-index-page.js +28 -0
  12. package/dist/client.d.ts +16 -0
  13. package/dist/client.d.ts.map +1 -0
  14. package/dist/client.js +75 -0
  15. package/dist/components/airline-logo.d.ts +19 -0
  16. package/dist/components/airline-logo.d.ts.map +1 -0
  17. package/dist/components/airline-logo.js +18 -0
  18. package/dist/components/airport-combobox.d.ts +20 -0
  19. package/dist/components/airport-combobox.d.ts.map +1 -0
  20. package/dist/components/airport-combobox.js +31 -0
  21. package/dist/components/billing-pickers.d.ts +19 -0
  22. package/dist/components/billing-pickers.d.ts.map +1 -0
  23. package/dist/components/billing-pickers.js +148 -0
  24. package/dist/components/flight-baggage-step.d.ts +32 -0
  25. package/dist/components/flight-baggage-step.d.ts.map +1 -0
  26. package/dist/components/flight-baggage-step.js +119 -0
  27. package/dist/components/flight-billing-step.d.ts +69 -0
  28. package/dist/components/flight-billing-step.d.ts.map +1 -0
  29. package/dist/components/flight-billing-step.js +117 -0
  30. package/dist/components/flight-booking-journey.d.ts +31 -0
  31. package/dist/components/flight-booking-journey.d.ts.map +1 -0
  32. package/dist/components/flight-booking-journey.js +103 -0
  33. package/dist/components/flight-booking-ledger.d.ts +53 -0
  34. package/dist/components/flight-booking-ledger.d.ts.map +1 -0
  35. package/dist/components/flight-booking-ledger.js +104 -0
  36. package/dist/components/flight-booking-page.d.ts +25 -0
  37. package/dist/components/flight-booking-page.d.ts.map +1 -0
  38. package/dist/components/flight-booking-page.js +175 -0
  39. package/dist/components/flight-booking-shell-helpers.d.ts +29 -0
  40. package/dist/components/flight-booking-shell-helpers.d.ts.map +1 -0
  41. package/dist/components/flight-booking-shell-helpers.js +204 -0
  42. package/dist/components/flight-booking-shell-panels.d.ts +24 -0
  43. package/dist/components/flight-booking-shell-panels.d.ts.map +1 -0
  44. package/dist/components/flight-booking-shell-panels.js +39 -0
  45. package/dist/components/flight-booking-shell-types.d.ts +49 -0
  46. package/dist/components/flight-booking-shell-types.d.ts.map +1 -0
  47. package/dist/components/flight-booking-shell-types.js +18 -0
  48. package/dist/components/flight-booking-shell.d.ts +12 -0
  49. package/dist/components/flight-booking-shell.d.ts.map +1 -0
  50. package/dist/components/flight-booking-shell.js +210 -0
  51. package/dist/components/flight-contact-form.d.ts +16 -0
  52. package/dist/components/flight-contact-form.d.ts.map +1 -0
  53. package/dist/components/flight-contact-form.js +25 -0
  54. package/dist/components/flight-fare-upsell-step.d.ts +26 -0
  55. package/dist/components/flight-fare-upsell-step.d.ts.map +1 -0
  56. package/dist/components/flight-fare-upsell-step.js +169 -0
  57. package/dist/components/flight-filters-bar.d.ts +19 -0
  58. package/dist/components/flight-filters-bar.d.ts.map +1 -0
  59. package/dist/components/flight-filters-bar.js +98 -0
  60. package/dist/components/flight-itinerary.d.ts +28 -0
  61. package/dist/components/flight-itinerary.d.ts.map +1 -0
  62. package/dist/components/flight-itinerary.js +110 -0
  63. package/dist/components/flight-offer-detail.d.ts +21 -0
  64. package/dist/components/flight-offer-detail.d.ts.map +1 -0
  65. package/dist/components/flight-offer-detail.js +49 -0
  66. package/dist/components/flight-offer-row.d.ts +25 -0
  67. package/dist/components/flight-offer-row.d.ts.map +1 -0
  68. package/dist/components/flight-offer-row.js +78 -0
  69. package/dist/components/flight-order-confirmation.d.ts +13 -0
  70. package/dist/components/flight-order-confirmation.d.ts.map +1 -0
  71. package/dist/components/flight-order-confirmation.js +46 -0
  72. package/dist/components/flight-passenger-form.d.ts +49 -0
  73. package/dist/components/flight-passenger-form.d.ts.map +1 -0
  74. package/dist/components/flight-passenger-form.js +159 -0
  75. package/dist/components/flight-payment-selector.d.ts +13 -0
  76. package/dist/components/flight-payment-selector.d.ts.map +1 -0
  77. package/dist/components/flight-payment-selector.js +32 -0
  78. package/dist/components/flight-payment-step.d.ts +32 -0
  79. package/dist/components/flight-payment-step.d.ts.map +1 -0
  80. package/dist/components/flight-payment-step.js +81 -0
  81. package/dist/components/flight-search-form.d.ts +14 -0
  82. package/dist/components/flight-search-form.d.ts.map +1 -0
  83. package/dist/components/flight-search-form.js +58 -0
  84. package/dist/components/flight-seat-map.d.ts +32 -0
  85. package/dist/components/flight-seat-map.d.ts.map +1 -0
  86. package/dist/components/flight-seat-map.js +101 -0
  87. package/dist/components/flight-seats-step.d.ts +40 -0
  88. package/dist/components/flight-seats-step.d.ts.map +1 -0
  89. package/dist/components/flight-seats-step.js +214 -0
  90. package/dist/components/flight-services-step.d.ts +27 -0
  91. package/dist/components/flight-services-step.d.ts.map +1 -0
  92. package/dist/components/flight-services-step.js +123 -0
  93. package/dist/components/flights-page-panels.d.ts +27 -0
  94. package/dist/components/flights-page-panels.d.ts.map +1 -0
  95. package/dist/components/flights-page-panels.js +40 -0
  96. package/dist/components/flights-page-types.d.ts +39 -0
  97. package/dist/components/flights-page-types.d.ts.map +1 -0
  98. package/dist/components/flights-page-types.js +1 -0
  99. package/dist/components/flights-page-utils.d.ts +14 -0
  100. package/dist/components/flights-page-utils.d.ts.map +1 -0
  101. package/dist/components/flights-page-utils.js +79 -0
  102. package/dist/components/flights-page.d.ts +4 -0
  103. package/dist/components/flights-page.d.ts.map +1 -0
  104. package/dist/components/flights-page.js +209 -0
  105. package/dist/components/passenger-contact-picker.d.ts +16 -0
  106. package/dist/components/passenger-contact-picker.d.ts.map +1 -0
  107. package/dist/components/passenger-contact-picker.js +45 -0
  108. package/dist/components/pax-cabin-popover.d.ts +18 -0
  109. package/dist/components/pax-cabin-popover.d.ts.map +1 -0
  110. package/dist/components/pax-cabin-popover.js +35 -0
  111. package/dist/components/popular-routes.d.ts +42 -0
  112. package/dist/components/popular-routes.d.ts.map +1 -0
  113. package/dist/components/popular-routes.js +108 -0
  114. package/dist/hooks/index.d.ts +13 -0
  115. package/dist/hooks/index.d.ts.map +1 -0
  116. package/dist/hooks/index.js +12 -0
  117. package/dist/hooks/use-aircraft.d.ts +17 -0
  118. package/dist/hooks/use-aircraft.d.ts.map +1 -0
  119. package/dist/hooks/use-aircraft.js +18 -0
  120. package/dist/hooks/use-airlines.d.ts +18 -0
  121. package/dist/hooks/use-airlines.d.ts.map +1 -0
  122. package/dist/hooks/use-airlines.js +18 -0
  123. package/dist/hooks/use-airport-search.d.ts +28 -0
  124. package/dist/hooks/use-airport-search.d.ts.map +1 -0
  125. package/dist/hooks/use-airport-search.js +23 -0
  126. package/dist/hooks/use-airports.d.ts +21 -0
  127. package/dist/hooks/use-airports.d.ts.map +1 -0
  128. package/dist/hooks/use-airports.js +17 -0
  129. package/dist/hooks/use-flight-ancillaries.d.ts +63 -0
  130. package/dist/hooks/use-flight-ancillaries.d.ts.map +1 -0
  131. package/dist/hooks/use-flight-ancillaries.js +24 -0
  132. package/dist/hooks/use-flight-book.d.ts +139 -0
  133. package/dist/hooks/use-flight-book.d.ts.map +1 -0
  134. package/dist/hooks/use-flight-book.js +24 -0
  135. package/dist/hooks/use-flight-offer.d.ts +106 -0
  136. package/dist/hooks/use-flight-offer.d.ts.map +1 -0
  137. package/dist/hooks/use-flight-offer.js +20 -0
  138. package/dist/hooks/use-flight-order.d.ts +286 -0
  139. package/dist/hooks/use-flight-order.d.ts.map +1 -0
  140. package/dist/hooks/use-flight-order.js +38 -0
  141. package/dist/hooks/use-flight-orders.d.ts +147 -0
  142. package/dist/hooks/use-flight-orders.d.ts.map +1 -0
  143. package/dist/hooks/use-flight-orders.js +31 -0
  144. package/dist/hooks/use-flight-search.d.ts +110 -0
  145. package/dist/hooks/use-flight-search.d.ts.map +1 -0
  146. package/dist/hooks/use-flight-search.js +18 -0
  147. package/dist/hooks/use-flight-seat-map.d.ts +49 -0
  148. package/dist/hooks/use-flight-seat-map.d.ts.map +1 -0
  149. package/dist/hooks/use-flight-seat-map.js +23 -0
  150. package/dist/hooks/use-saved-payment-methods.d.ts +23 -0
  151. package/dist/hooks/use-saved-payment-methods.d.ts.map +1 -0
  152. package/dist/hooks/use-saved-payment-methods.js +20 -0
  153. package/dist/i18n/en.d.ts +465 -0
  154. package/dist/i18n/en.d.ts.map +1 -0
  155. package/dist/i18n/en.js +520 -0
  156. package/dist/i18n/index.d.ts +5 -0
  157. package/dist/i18n/index.d.ts.map +1 -0
  158. package/dist/i18n/index.js +3 -0
  159. package/dist/i18n/messages.d.ts +392 -0
  160. package/dist/i18n/messages.d.ts.map +1 -0
  161. package/dist/i18n/messages.js +1 -0
  162. package/dist/i18n/provider.d.ts +952 -0
  163. package/dist/i18n/provider.d.ts.map +1 -0
  164. package/dist/i18n/provider.js +44 -0
  165. package/dist/i18n/ro.d.ts +465 -0
  166. package/dist/i18n/ro.d.ts.map +1 -0
  167. package/dist/i18n/ro.js +520 -0
  168. package/dist/index.d.ts +7 -0
  169. package/dist/index.d.ts.map +1 -0
  170. package/dist/index.js +6 -0
  171. package/dist/provider.d.ts +2 -0
  172. package/dist/provider.d.ts.map +1 -0
  173. package/dist/provider.js +1 -0
  174. package/dist/query-keys.d.ts +42 -0
  175. package/dist/query-keys.d.ts.map +1 -0
  176. package/dist/query-keys.js +22 -0
  177. package/dist/query-options.d.ts +827 -0
  178. package/dist/query-options.d.ts.map +1 -0
  179. package/dist/query-options.js +58 -0
  180. package/dist/schemas.d.ts +1658 -0
  181. package/dist/schemas.d.ts.map +1 -0
  182. package/dist/schemas.js +295 -0
  183. package/dist/ui.d.ts +31 -0
  184. package/dist/ui.d.ts.map +1 -0
  185. package/dist/ui.js +28 -0
  186. package/package.json +148 -0
  187. package/src/styles.css +11 -0
@@ -0,0 +1,210 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button } from "@voyant-travel/ui/components/button";
4
+ import { ChevronLeft, ChevronRight } from "lucide-react";
5
+ import { useMemo, useState } from "react";
6
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
7
+ import { FlightBaggageStep } from "./flight-baggage-step.js";
8
+ import { emptyBillingValue, FlightBillingStep, validateBilling, } from "./flight-billing-step.js";
9
+ import { FlightBookingLedger } from "./flight-booking-ledger.js";
10
+ import { buildLedgerExtras, getVisibleFlightBookingSteps, mergeOffers, } from "./flight-booking-shell-helpers.js";
11
+ import { ConfirmStep, ReviewStep, Stepper } from "./flight-booking-shell-panels.js";
12
+ import { FlightFareUpsellStep } from "./flight-fare-upsell-step.js";
13
+ import { FlightPassengerForm, validatePassengers } from "./flight-passenger-form.js";
14
+ import { FlightPaymentStep } from "./flight-payment-step.js";
15
+ import { FlightSeatsStep } from "./flight-seats-step.js";
16
+ import { FlightServicesStep } from "./flight-services-step.js";
17
+ /**
18
+ * Multi-step booking shell with a sticky right-rail price ledger and a top
19
+ * stepper. Steps: review → passengers → bags → services → contact+payment
20
+ * → confirm. The shell owns the per-leg `FlightItinerarySelection` plus
21
+ * ancillary picks (baggage / assistance / extras), and synthesizes a
22
+ * combined `FlightOffer` (with both itineraries merged) when `onBook` is
23
+ * called — so the booking adapter sees a single offer with all legs intact.
24
+ */
25
+ export function FlightBookingShell({ selection, passengers, onBook, onBooked, onCancel, onEditOutbound, onEditReturn, carrierName, airportName, ancillaries, seatMaps, savedPaymentMethods, paymentCapabilities, documentsRequired, renderPassengerPicker, renderBillingPersonPicker, renderBillingOrgPicker, onSaveBillingDefaults, }) {
26
+ const messages = useFlightsUiMessagesOrDefault();
27
+ const [stepId, setStepId] = useState("review");
28
+ const [paxList, setPaxList] = useState([]);
29
+ const [billing, setBilling] = useState(emptyBillingValue);
30
+ const [payment, setPayment] = useState({ type: "hold" });
31
+ const [paymentSavedId, setPaymentSavedId] = useState(null);
32
+ const [submitting, setSubmitting] = useState(false);
33
+ const [error, setError] = useState(null);
34
+ const [baggage, setBaggage] = useState([]);
35
+ const [assistance, setAssistance] = useState([]);
36
+ const [extras, setExtras] = useState([]);
37
+ const [seats, setSeats] = useState([]);
38
+ const [fareBundles, setFareBundles] = useState([]);
39
+ const [seatsMode, setSeatsMode] = useState("auto");
40
+ const [sameForBothDirections, setSameForBothDirections] = useState(true);
41
+ const [sameFareForAllPax, setSameFareForAllPax] = useState(true);
42
+ const steps = useMemo(() => getVisibleFlightBookingSteps({ selection, ancillaries, seatMaps }), [selection, ancillaries, seatMaps]);
43
+ // If the visible-step list shrinks while we're sitting on a now-hidden
44
+ // step (e.g., catalog loaded with no services), fall back to the prior
45
+ // visible step. Done in render to keep state derived + free of effects.
46
+ const stepIdx = (() => {
47
+ const idx = steps.findIndex((s) => s.id === stepId);
48
+ return idx >= 0 ? idx : 0;
49
+ })();
50
+ const step = steps[stepIdx];
51
+ const combinedOffer = useMemo(() => mergeOffers(selection), [selection]);
52
+ const ancillarySelection = useMemo(() => {
53
+ if (baggage.length === 0 &&
54
+ assistance.length === 0 &&
55
+ extras.length === 0 &&
56
+ seats.length === 0 &&
57
+ fareBundles.length === 0) {
58
+ return undefined;
59
+ }
60
+ return {
61
+ ...(baggage.length > 0 ? { baggage } : {}),
62
+ ...(assistance.length > 0 ? { assistance } : {}),
63
+ ...(extras.length > 0 ? { extras } : {}),
64
+ ...(seats.length > 0 ? { seats } : {}),
65
+ ...(fareBundles.length > 0 ? { fareBundle: fareBundles } : {}),
66
+ };
67
+ }, [baggage, assistance, extras, seats, fareBundles]);
68
+ const paxErrors = validatePassengers(paxList);
69
+ const billingError = validateBilling(billing);
70
+ // Per-leg ledger line items derived from picks + catalogs + seat picks.
71
+ const { outboundExtras, returnExtras } = useMemo(() => buildLedgerExtras({
72
+ baggage,
73
+ extras,
74
+ assistance,
75
+ seats,
76
+ fareBundles,
77
+ outboundOffer: selection.outbound,
78
+ returnOffer: selection.return,
79
+ outboundCatalog: ancillaries?.outboundCatalog ?? null,
80
+ returnCatalog: ancillaries?.returnCatalog ?? null,
81
+ seatMaps,
82
+ messages,
83
+ }), [
84
+ baggage,
85
+ extras,
86
+ assistance,
87
+ seats,
88
+ fareBundles,
89
+ selection.outbound,
90
+ selection.return,
91
+ ancillaries?.outboundCatalog,
92
+ ancillaries?.returnCatalog,
93
+ seatMaps,
94
+ messages,
95
+ ]);
96
+ const completedSections = useMemo(() => {
97
+ const set = new Set();
98
+ set.add("flights");
99
+ // A section is "complete" once the user has navigated past it. We only
100
+ // mark sections that correspond to currently-visible steps.
101
+ const passedIdx = stepIdx;
102
+ const visibleIds = new Set(steps.map((s) => s.id));
103
+ const passed = (id) => {
104
+ if (!visibleIds.has(id))
105
+ return false;
106
+ const i = steps.findIndex((s) => s.id === id);
107
+ return i >= 0 && passedIdx > i;
108
+ };
109
+ if (passed("passengers"))
110
+ set.add("passengers");
111
+ if (passed("bags"))
112
+ set.add("bags");
113
+ if (passed("seats"))
114
+ set.add("seats");
115
+ if (passed("services"))
116
+ set.add("services");
117
+ if (passed("billing"))
118
+ set.add("billing");
119
+ if (stepIdx >= steps.length - 1)
120
+ set.add("payment");
121
+ return set;
122
+ }, [stepIdx, steps]);
123
+ if (!step)
124
+ return null;
125
+ const canContinue = (() => {
126
+ switch (step.id) {
127
+ case "review":
128
+ return true;
129
+ case "fares":
130
+ // Fare bundles are optional — keeping the base "Basic" fare is fine.
131
+ return true;
132
+ case "passengers":
133
+ return Object.keys(paxErrors).length === 0 && paxList.length > 0;
134
+ case "bags":
135
+ // Bags are optional — user may skip every passenger.
136
+ return true;
137
+ case "seats":
138
+ // Seats are optional under "skip" / "auto"; under "now" any picks are fine.
139
+ return true;
140
+ case "services":
141
+ return true;
142
+ case "billing":
143
+ return billingError == null;
144
+ case "payment":
145
+ return true;
146
+ case "confirm":
147
+ return true;
148
+ }
149
+ })();
150
+ const goNext = () => {
151
+ if (step?.id === "billing" && billing.saveAsDefault) {
152
+ onSaveBillingDefaults?.(billing);
153
+ }
154
+ const nextStep = steps[Math.min(steps.length - 1, stepIdx + 1)];
155
+ if (nextStep)
156
+ setStepId(nextStep.id);
157
+ };
158
+ const goBack = () => {
159
+ const prevStep = steps[Math.max(0, stepIdx - 1)];
160
+ if (prevStep)
161
+ setStepId(prevStep.id);
162
+ };
163
+ const submit = async () => {
164
+ setError(null);
165
+ setSubmitting(true);
166
+ try {
167
+ // Attach the structured billing address to the payment intent (when card)
168
+ // and surface the contact (email/phone) at the request level. Travel
169
+ // documents already live on each FlightPassenger.documents from the
170
+ // merged passenger form — no extra plumbing needed here.
171
+ const billingAddress = {
172
+ line1: billing.line1,
173
+ ...(billing.line2 ? { line2: billing.line2 } : {}),
174
+ city: billing.city,
175
+ ...(billing.region ? { region: billing.region } : {}),
176
+ ...(billing.postalCode ? { postalCode: billing.postalCode } : {}),
177
+ countryCode: billing.countryCode,
178
+ };
179
+ const paymentWithAddress = payment.type === "card" ? { ...payment, billingAddress } : payment;
180
+ const order = await onBook({
181
+ offerId: combinedOffer.offerId,
182
+ offer: combinedOffer,
183
+ passengers: paxList,
184
+ contact: { email: billing.email, phone: billing.phone },
185
+ paymentIntent: paymentWithAddress,
186
+ ...(ancillarySelection ? { ancillaries: ancillarySelection } : {}),
187
+ });
188
+ onBooked?.(order);
189
+ }
190
+ catch (err) {
191
+ setError(err instanceof Error ? err.message : String(err));
192
+ }
193
+ finally {
194
+ setSubmitting(false);
195
+ }
196
+ };
197
+ return (_jsxs("div", { className: "flex flex-col gap-6 lg:grid lg:grid-cols-[1fr_360px] lg:items-start", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-5", children: [_jsx(Stepper, { steps: steps, currentIdx: stepIdx, messages: messages }), error && (_jsx("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 p-3 text-destructive text-sm", children: error })), _jsxs("div", { className: "flex flex-col gap-4", children: [step.id === "review" && (_jsx(ReviewStep, { selection: selection, carrierName: carrierName, airportName: airportName })), step.id === "fares" && (_jsx(FlightFareUpsellStep, { outboundOffer: selection.outbound, returnOffer: selection.return, passengers: paxList, passengerCounts: passengers, value: fareBundles, onChange: setFareBundles, sameForAllPassengers: sameFareForAllPax, onSameForAllPassengersChange: setSameFareForAllPax })), step.id === "passengers" && (_jsx(FlightPassengerForm, { counts: passengers, value: paxList, onChange: setPaxList, documentsRequired: documentsRequired, renderPicker: renderPassengerPicker })), step.id === "bags" && (_jsx(FlightBaggageStep, { outboundCatalog: ancillaries?.outboundCatalog ?? null, returnCatalog: ancillaries?.returnCatalog ?? null, outboundOffer: selection.outbound, returnOffer: selection.return, passengers: paxList, passengerCounts: passengers, value: baggage, onChange: setBaggage, sameForBothDirections: sameForBothDirections, onSameForBothDirectionsChange: setSameForBothDirections, loading: ancillaries?.loading })), step.id === "seats" && (_jsx(FlightSeatsStep, { outboundOffer: selection.outbound, returnOffer: selection.return, passengers: paxList, passengerCounts: passengers, value: seats, onChange: setSeats, mode: seatsMode, onModeChange: setSeatsMode, getSeatMap: seatMaps?.getSeatMap ??
198
+ (() => ({ seatMap: null, error: messages.flightBookingShell.seatMapsUnavailable })) })), step.id === "services" && (_jsx(FlightServicesStep, { outboundCatalog: ancillaries?.outboundCatalog ?? null, returnCatalog: ancillaries?.returnCatalog ?? null, outboundOffer: selection.outbound, returnOffer: selection.return, passengers: paxList, passengerCounts: passengers, assistance: assistance, extras: extras, onAssistanceChange: setAssistance, onExtrasChange: setExtras, loading: ancillaries?.loading })), step.id === "billing" && (_jsx(FlightBillingStep, { value: billing, onChange: setBilling, eligiblePassengers: paxList
199
+ .filter((p) => p.type === "adult" && p.firstName.trim() !== "" && p.lastName.trim() !== "")
200
+ .map((p) => ({
201
+ id: p.passengerId,
202
+ firstName: p.firstName,
203
+ ...(p.middleName ? { middleName: p.middleName } : {}),
204
+ lastName: p.lastName,
205
+ })), renderPersonPicker: renderBillingPersonPicker, renderOrgPicker: renderBillingOrgPicker })), step.id === "payment" && (_jsx(FlightPaymentStep, { value: payment, onChange: setPayment, savedMethods: savedPaymentMethods?.methods ?? [], loadingSavedMethods: savedPaymentMethods?.loading, selectedSavedId: paymentSavedId, onSelectSaved: setPaymentSavedId, capabilities: paymentCapabilities })), step.id === "confirm" && (_jsx(ConfirmStep, { selection: selection, passengers: paxList, billing: billing, payment: payment, carrierName: carrierName, airportName: airportName }))] }), _jsxs("div", { className: "flex items-center justify-between border-t pt-4", children: [_jsxs(Button, { type: "button", variant: "ghost", onClick: () => (stepIdx === 0 ? onCancel?.() : goBack()), disabled: submitting, children: [_jsx(ChevronLeft, { className: "mr-1 h-4 w-4" }), stepIdx === 0
206
+ ? messages.flightBookingShell.backToResults
207
+ : messages.flightBookingShell.back] }), step.id === "confirm" ? (_jsx(Button, { onClick: submit, disabled: submitting, children: submitting
208
+ ? messages.flightBookingShell.booking
209
+ : messages.flightBookingShell.confirmBooking })) : (_jsxs(Button, { onClick: goNext, disabled: !canContinue, children: [messages.flightBookingShell.continue, _jsx(ChevronRight, { className: "ml-1 h-4 w-4" })] }))] })] }), _jsx("div", { className: "lg:sticky lg:top-6", children: _jsx(FlightBookingLedger, { selection: selection, passengers: passengers, carrierName: carrierName, airportName: airportName, outboundExtras: outboundExtras, returnExtras: returnExtras, onEditOutbound: onEditOutbound, onEditReturn: onEditReturn, completedSections: completedSections }) })] }));
210
+ }
@@ -0,0 +1,16 @@
1
+ export interface FlightContactValue {
2
+ email?: string;
3
+ phone?: string;
4
+ }
5
+ export interface FlightContactFormProps {
6
+ value: FlightContactValue;
7
+ onChange: (next: FlightContactValue) => void;
8
+ }
9
+ /**
10
+ * Booking contact details — used by the connector to send confirmation +
11
+ * any operational disruption notices. Email is required by most providers;
12
+ * phone is recommended but optional.
13
+ */
14
+ export declare function FlightContactForm({ value, onChange }: FlightContactFormProps): import("react/jsx-runtime").JSX.Element;
15
+ export declare function validateContact(value: FlightContactValue): string | null;
16
+ //# sourceMappingURL=flight-contact-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-contact-form.d.ts","sourceRoot":"","sources":["../../src/components/flight-contact-form.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,kBAAkB,CAAA;IACzB,QAAQ,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAA;CAC7C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,sBAAsB,2CA0C5E;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,GAAG,IAAI,CAMxE"}
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Input } from "@voyant-travel/ui/components/input";
4
+ import { Label } from "@voyant-travel/ui/components/label";
5
+ import { Mail, Phone } from "lucide-react";
6
+ import { flightsUiEn } from "../i18n/en.js";
7
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
8
+ /**
9
+ * Booking contact details — used by the connector to send confirmation +
10
+ * any operational disruption notices. Email is required by most providers;
11
+ * phone is recommended but optional.
12
+ */
13
+ export function FlightContactForm({ value, onChange }) {
14
+ const messages = useFlightsUiMessagesOrDefault().flightContactForm;
15
+ return (_jsxs("div", { className: "rounded-lg border bg-card p-4 shadow-sm", children: [_jsx("h3", { className: "mb-3 font-medium text-sm", children: messages.title }), _jsx("p", { className: "mb-4 text-xs text-muted-foreground", children: messages.description }), _jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: "text-[11px] uppercase tracking-wider text-muted-foreground", children: [messages.email, " ", _jsx("span", { className: "ml-0.5 text-destructive", children: "*" })] }), _jsxs("div", { className: "relative", children: [_jsx(Mail, { className: "-translate-y-1/2 absolute top-1/2 left-3 h-4 w-4 text-muted-foreground" }), _jsx(Input, { type: "email", autoComplete: "email", value: value.email ?? "", onChange: (e) => onChange({ ...value, email: e.target.value }), placeholder: messages.emailPlaceholder, className: "pl-9" })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { className: "text-[11px] uppercase tracking-wider text-muted-foreground", children: messages.phone }), _jsxs("div", { className: "relative", children: [_jsx(Phone, { className: "-translate-y-1/2 absolute top-1/2 left-3 h-4 w-4 text-muted-foreground" }), _jsx(Input, { type: "tel", autoComplete: "tel", value: value.phone ?? "", onChange: (e) => onChange({ ...value, phone: e.target.value }), placeholder: messages.phonePlaceholder, className: "pl-9" })] })] })] })] }));
16
+ }
17
+ export function validateContact(value) {
18
+ const messages = flightsUiEn.flightContactForm.validation;
19
+ if (!value.email?.trim())
20
+ return messages.emailRequired;
21
+ // Loose email check — adapters do their own validation
22
+ if (!/.+@.+\..+/.test(value.email))
23
+ return messages.emailInvalid;
24
+ return null;
25
+ }
@@ -0,0 +1,26 @@
1
+ import type { AncillarySelection, FlightOffer, FlightPassenger, PassengerCounts } from "@voyant-travel/flights/contract/types";
2
+ type FareBundlePicks = NonNullable<AncillarySelection["fareBundle"]>;
3
+ export interface FlightFareUpsellStepProps {
4
+ outboundOffer: FlightOffer;
5
+ returnOffer?: FlightOffer;
6
+ /** Filled passenger entries — used for per-pax labels. */
7
+ passengers: FlightPassenger[];
8
+ /** Pax counts when the form hasn't been filled yet (fallback labels). */
9
+ passengerCounts: PassengerCounts;
10
+ value: FareBundlePicks;
11
+ onChange: (next: FareBundlePicks) => void;
12
+ /** Default-on toggle: one pick applies to every passenger on the leg. */
13
+ sameForAllPassengers: boolean;
14
+ onSameForAllPassengersChange: (next: boolean) => void;
15
+ }
16
+ /**
17
+ * Per-pax per-leg branded-fare upsell step. For multi-pax bookings the
18
+ * "Same fare for all passengers" toggle (default ON) collapses the picker
19
+ * back to one card grid per leg, keeping the common case ("everyone on
20
+ * Standard") to one click. Toggling off splits each leg into per-pax card
21
+ * grids — useful for the "Adult 1 Plus, Adult 2 Basic" case full-service
22
+ * carriers + B2B agency bookings actually exercise.
23
+ */
24
+ export declare function FlightFareUpsellStep({ outboundOffer, returnOffer, passengers, passengerCounts, value, onChange, sameForAllPassengers, onSameForAllPassengersChange, }: FlightFareUpsellStepProps): import("react/jsx-runtime").JSX.Element;
25
+ export {};
26
+ //# sourceMappingURL=flight-fare-upsell-step.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-fare-upsell-step.d.ts","sourceRoot":"","sources":["../../src/components/flight-fare-upsell-step.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,kBAAkB,EAElB,WAAW,EACX,eAAe,EACf,eAAe,EAChB,MAAM,uCAAuC,CAAA;AAQ9C,KAAK,eAAe,GAAG,WAAW,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC,CAAA;AAGpE,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,WAAW,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,0DAA0D;IAC1D,UAAU,EAAE,eAAe,EAAE,CAAA;IAC7B,yEAAyE;IACzE,eAAe,EAAE,eAAe,CAAA;IAChC,KAAK,EAAE,eAAe,CAAA;IACtB,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACzC,yEAAyE;IACzE,oBAAoB,EAAE,OAAO,CAAA;IAC7B,4BAA4B,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;CACtD;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,aAAa,EACb,WAAW,EACX,UAAU,EACV,eAAe,EACf,KAAK,EACL,QAAQ,EACR,oBAAoB,EACpB,4BAA4B,GAC7B,EAAE,yBAAyB,2CAiG3B"}
@@ -0,0 +1,169 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { formatMessage } from "@voyant-travel/i18n";
4
+ import { Checkbox } from "@voyant-travel/ui/components/checkbox";
5
+ import { cn } from "@voyant-travel/ui/lib/utils";
6
+ import { Briefcase, Check, Crown, Luggage, Sparkles, X } from "lucide-react";
7
+ import { useMemo } from "react";
8
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
9
+ /**
10
+ * Per-pax per-leg branded-fare upsell step. For multi-pax bookings the
11
+ * "Same fare for all passengers" toggle (default ON) collapses the picker
12
+ * back to one card grid per leg, keeping the common case ("everyone on
13
+ * Standard") to one click. Toggling off splits each leg into per-pax card
14
+ * grids — useful for the "Adult 1 Plus, Adult 2 Basic" case full-service
15
+ * carriers + B2B agency bookings actually exercise.
16
+ */
17
+ export function FlightFareUpsellStep({ outboundOffer, returnOffer, passengers, passengerCounts, value, onChange, sameForAllPassengers, onSameForAllPassengersChange, }) {
18
+ const messages = useFlightsUiMessagesOrDefault();
19
+ const paxRows = useMemo(() => buildPassengerRows(passengers, passengerCounts, messages), [passengers, passengerCounts, messages]);
20
+ const isMultiPax = paxRows.length > 1;
21
+ const outboundBundles = outboundOffer.fareBundles ?? [];
22
+ const returnBundles = returnOffer?.fareBundles ?? [];
23
+ if (outboundBundles.length === 0 && returnBundles.length === 0) {
24
+ return (_jsx("div", { className: "rounded-xl border border-dashed p-6 text-center text-muted-foreground text-sm", children: messages.flightFareUpsellStep.unavailable }));
25
+ }
26
+ const setPick = (passengerId, sliceIndex, bundleId) => {
27
+ const filtered = value.filter((p) => !(p.passengerId === passengerId && p.sliceIndex === sliceIndex));
28
+ if (bundleId) {
29
+ onChange([...filtered, { passengerId, sliceIndex, bundleId }]);
30
+ }
31
+ else {
32
+ onChange(filtered);
33
+ }
34
+ };
35
+ /**
36
+ * "Same fare for all" handler — picks on behalf of every passenger on the
37
+ * given leg. A null bundleId clears all picks for that leg.
38
+ */
39
+ const setLegPick = (sliceIndex, bundleId) => {
40
+ const filtered = value.filter((p) => p.sliceIndex !== sliceIndex);
41
+ if (!bundleId) {
42
+ onChange(filtered);
43
+ return;
44
+ }
45
+ const additions = paxRows.map((p) => ({
46
+ passengerId: p.passengerId,
47
+ sliceIndex,
48
+ bundleId,
49
+ }));
50
+ onChange([...filtered, ...additions]);
51
+ };
52
+ return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h2", { className: "font-semibold text-base", children: messages.flightFareUpsellStep.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: messages.flightFareUpsellStep.description })] }), isMultiPax && (_jsxs("div", { className: "flex shrink-0 items-center gap-2 text-sm", children: [_jsx(Checkbox, { id: "fare-same-for-all", checked: sameForAllPassengers, onCheckedChange: (v) => onSameForAllPassengersChange(!!v) }), _jsx("label", { htmlFor: "fare-same-for-all", className: "cursor-pointer", children: messages.flightFareUpsellStep.sameForAllPassengers })] }))] }), _jsx(FareLegSection, { label: messages.common.legLabels.outbound, bundles: outboundBundles, sliceIndex: 0, paxRows: paxRows, value: value, sameForAll: sameForAllPassengers || !isMultiPax, onSetPick: setPick, onSetLegPick: setLegPick, messages: messages }), returnOffer && returnBundles.length > 0 && (_jsx(FareLegSection, { label: messages.common.legLabels.return, bundles: returnBundles, sliceIndex: 1, paxRows: paxRows, value: value, sameForAll: sameForAllPassengers || !isMultiPax, onSetPick: setPick, onSetLegPick: setLegPick, messages: messages }))] }));
53
+ }
54
+ function FareLegSection({ label, bundles, sliceIndex, paxRows, value, sameForAll, onSetPick, onSetLegPick, messages, }) {
55
+ // For "same for all" mode we treat the leg as having one selection — the
56
+ // bundleId common to every pax pick on this leg (null when split or none).
57
+ const legPick = (() => {
58
+ if (paxRows.length === 0)
59
+ return null;
60
+ const ids = paxRows.map((p) => value.find((v) => v.passengerId === p.passengerId && v.sliceIndex === sliceIndex)
61
+ ?.bundleId ?? null);
62
+ if (ids.every((id) => id === ids[0]))
63
+ return ids[0] ?? null;
64
+ return null;
65
+ })();
66
+ const someoneSelected = value.some((v) => v.sliceIndex === sliceIndex);
67
+ return (_jsxs("section", { className: "flex flex-col gap-3", children: [_jsxs("header", { className: "flex items-baseline justify-between gap-2", children: [_jsx("h3", { className: "font-medium text-[11px] uppercase tracking-wider text-muted-foreground", children: label }), someoneSelected && (_jsx("button", { type: "button", onClick: () => onSetLegPick(sliceIndex, null), className: "text-muted-foreground text-xs hover:text-foreground", children: messages.flightFareUpsellStep.resetToBasic }))] }), sameForAll ? (_jsx(BundleGrid, { bundles: bundles, selectedId: legPick, contextLabel: paxRows.length > 1
68
+ ? formatMessage(messages.flightFareUpsellStep.appliesToAllPassengers, {
69
+ count: paxRows.length,
70
+ })
71
+ : null, onPick: (id) => onSetLegPick(sliceIndex, id), messages: messages })) : (_jsx("div", { className: "flex flex-col gap-4", children: paxRows.map((pax) => {
72
+ const pick = value.find((v) => v.passengerId === pax.passengerId && v.sliceIndex === sliceIndex);
73
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "font-medium text-sm", children: pax.label }), _jsx(BundleGrid, { bundles: bundles, selectedId: pick?.bundleId ?? null, onPick: (id) => onSetPick(pax.passengerId, sliceIndex, id), messages: messages })] }, pax.passengerId));
74
+ }) }))] }));
75
+ }
76
+ function BundleGrid({ bundles, selectedId, contextLabel, onPick, messages, }) {
77
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("div", { className: "grid gap-3 md:grid-cols-3", children: bundles.map((b) => (_jsx(BundleCard, { bundle: b, selected: selectedId === b.id, isBasicByDefault: selectedId == null && b.tier === "basic", onPick: () => onPick(b.tier === "basic" ? null : b.id), messages: messages }, b.id))) }), contextLabel && _jsx("span", { className: "text-[11px] text-muted-foreground", children: contextLabel })] }));
78
+ }
79
+ function BundleCard({ bundle, selected, isBasicByDefault, onPick, messages, }) {
80
+ const delta = Number(bundle.priceDelta.amount);
81
+ const deltaLabel = delta > 0
82
+ ? `+${formatMoney(bundle.priceDelta.amount, bundle.priceDelta.currency)}`
83
+ : messages.common.included;
84
+ const showActiveRing = selected || isBasicByDefault;
85
+ return (_jsxs("button", { type: "button", onClick: onPick, className: cn("relative flex flex-col gap-3 rounded-lg border bg-card p-4 text-left transition-colors", showActiveRing
86
+ ? "border-primary ring-2 ring-primary/20"
87
+ : "hover:border-primary/40 hover:bg-accent/30", bundle.recommended && !showActiveRing && "border-primary/40"), children: [bundle.recommended && (_jsx("span", { className: "-translate-y-1/2 absolute top-0 left-4 rounded-full bg-primary px-2 py-0.5 font-medium text-[9px] text-primary-foreground uppercase tracking-wider", children: messages.common.recommended })), _jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [_jsxs("span", { className: "flex items-center gap-1.5 font-semibold text-sm", children: [_jsx(TierIcon, { tier: bundle.tier }), bundle.label] }), _jsx("span", { className: cn("font-medium text-xs", delta > 0 ? "text-foreground" : "text-emerald-600"), children: deltaLabel })] }), _jsxs("ul", { className: "flex flex-col gap-1.5", children: [_jsx(Inclusion, { ok: !!bundle.inclusions.cabinBag?.included, icon: _jsx(Briefcase, { className: "h-3.5 w-3.5" }), label: bundle.inclusions.cabinBag?.included
88
+ ? formatMessage(messages.flightFareUpsellStep.cabinBag, {
89
+ weight: bundle.inclusions.cabinBag.weightKg
90
+ ? `(${bundle.inclusions.cabinBag.weightKg} kg)`
91
+ : "",
92
+ }).trim()
93
+ : messages.flightFareUpsellStep.noCabinBag }), _jsx(Inclusion, { ok: !!bundle.inclusions.checkedBag?.included, icon: _jsx(Luggage, { className: "h-3.5 w-3.5" }), label: bundle.inclusions.checkedBag?.included
94
+ ? formatMessage(messages.flightFareUpsellStep.checkedBag, {
95
+ weight: bundle.inclusions.checkedBag.weightKg
96
+ ? `${bundle.inclusions.checkedBag.weightKg} kg`
97
+ : "",
98
+ pieces: bundle.inclusions.checkedBag.pieces && bundle.inclusions.checkedBag.pieces > 1
99
+ ? ` × ${bundle.inclusions.checkedBag.pieces}`
100
+ : "",
101
+ }).trim()
102
+ : messages.flightFareUpsellStep.noCheckedBag }), _jsx(Inclusion, { ok: bundle.inclusions.seatSelection !== "none" && bundle.inclusions.seatSelection != null, icon: _jsx(Sparkles, { className: "h-3.5 w-3.5" }), label: bundle.inclusions.seatSelection === "free"
103
+ ? messages.flightFareUpsellStep.freeSeatSelection
104
+ : bundle.inclusions.seatSelection === "standard"
105
+ ? messages.flightFareUpsellStep.standardSeatSelection
106
+ : messages.flightFareUpsellStep.noSeatSelection }), _jsx(Inclusion, { ok: !!bundle.inclusions.priorityBoarding, label: messages.flightFareUpsellStep.priorityBoarding }), _jsx(Inclusion, { ok: !!bundle.inclusions.loungeAccess, label: messages.flightFareUpsellStep.loungeAccess }), _jsx(Inclusion, { ok: !!bundle.inclusions.changeable, label: bundle.inclusions.changeable
107
+ ? messages.flightFareUpsellStep.freeChanges
108
+ : messages.flightFareUpsellStep.changesForFee }), _jsx(Inclusion, { ok: !!bundle.inclusions.refundable, label: bundle.inclusions.refundable
109
+ ? messages.flightFareUpsellStep.refundable
110
+ : messages.flightFareUpsellStep.nonRefundable }), bundle.inclusions.notes?.map((n) => (_jsx(Inclusion, { ok: true, label: n }, n)))] }), selected && (_jsxs("span", { className: "mt-1 inline-flex items-center gap-1 self-start font-medium text-primary text-xs", children: [_jsx(Check, { className: "h-3 w-3" }), _jsx("span", { children: messages.common.selected })] }))] }));
111
+ }
112
+ function Inclusion({ ok, icon, label }) {
113
+ return (_jsxs("li", { className: "flex items-center gap-2 text-xs", children: [ok ? (_jsx(Check, { className: "h-3.5 w-3.5 shrink-0 text-emerald-600" })) : (_jsx(X, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground/60" })), _jsxs("span", { className: "flex items-center gap-1.5 text-foreground", children: [icon, _jsx("span", { className: cn(!ok && "text-muted-foreground"), children: label })] })] }));
114
+ }
115
+ function TierIcon({ tier }) {
116
+ switch (tier) {
117
+ case "plus":
118
+ case "premium":
119
+ return _jsx(Crown, { className: "h-3.5 w-3.5 text-amber-500" });
120
+ case "standard":
121
+ return _jsx(Sparkles, { className: "h-3.5 w-3.5 text-primary" });
122
+ default:
123
+ return _jsx(Briefcase, { className: "h-3.5 w-3.5 text-muted-foreground" });
124
+ }
125
+ }
126
+ function buildPassengerRows(passengers, counts, messages) {
127
+ if (passengers.length > 0) {
128
+ return passengers.map((p, idx) => ({
129
+ passengerId: p.passengerId,
130
+ label: nameOrFallback(p, idx, messages),
131
+ }));
132
+ }
133
+ const out = [];
134
+ for (let i = 1; i <= counts.adults; i++) {
135
+ out.push({
136
+ passengerId: `pax_adult_${i}`,
137
+ label: `${messages.common.passengerTypeLabels.adult} ${i}`,
138
+ });
139
+ }
140
+ for (let i = 1; i <= (counts.children ?? 0); i++) {
141
+ out.push({
142
+ passengerId: `pax_child_${i}`,
143
+ label: `${messages.common.passengerTypeLabels.child} ${i}`,
144
+ });
145
+ }
146
+ for (let i = 1; i <= (counts.infants ?? 0); i++) {
147
+ out.push({
148
+ passengerId: `pax_infant_${i}`,
149
+ label: `${messages.common.passengerTypeLabels.infant} ${i}`,
150
+ });
151
+ }
152
+ return out;
153
+ }
154
+ function nameOrFallback(p, idx, messages) {
155
+ const full = `${p.firstName} ${p.lastName}`.trim();
156
+ if (full)
157
+ return full;
158
+ return `${messages.common.passengerTypeLabels[p.type]} ${idx + 1}`;
159
+ }
160
+ function formatMoney(amount, currency) {
161
+ const n = Number(amount);
162
+ if (!Number.isFinite(n))
163
+ return `${amount} ${currency}`;
164
+ return new Intl.NumberFormat(undefined, {
165
+ style: "currency",
166
+ currency,
167
+ maximumFractionDigits: 0,
168
+ }).format(n);
169
+ }
@@ -0,0 +1,19 @@
1
+ import type { FlightOffer } from "@voyant-travel/flights/contract/types";
2
+ export interface FlightFiltersValue {
3
+ /** Selected carrier IATA codes (empty = no carrier filter). */
4
+ carriers: string[];
5
+ /** Max stops on any single itinerary. `null` = no cap. */
6
+ maxStops: number | null;
7
+ /** Inclusive price ceiling (in offer's currency). `null` = no cap. */
8
+ maxPrice: number | null;
9
+ }
10
+ export declare const EMPTY_FLIGHT_FILTERS: FlightFiltersValue;
11
+ export interface FlightFiltersBarProps {
12
+ value: FlightFiltersValue;
13
+ onChange: (next: FlightFiltersValue) => void;
14
+ /** Live offer set — used to derive carrier facet buckets and the price/stops range. */
15
+ offers: FlightOffer[];
16
+ carrierName?: (iataCode: string) => string | undefined;
17
+ }
18
+ export declare function FlightFiltersBar({ value, onChange, offers, carrierName }: FlightFiltersBarProps): import("react/jsx-runtime").JSX.Element;
19
+ //# sourceMappingURL=flight-filters-bar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-filters-bar.d.ts","sourceRoot":"","sources":["../../src/components/flight-filters-bar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAA;AAoBxE,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,eAAO,MAAM,oBAAoB,EAAE,kBAIlC,CAAA;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,kBAAkB,CAAA;IACzB,QAAQ,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC5C,uFAAuF;IACvF,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;CACvD;AAED,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,qBAAqB,2CAmD/F"}
@@ -0,0 +1,98 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { formatMessage } from "@voyant-travel/i18n";
4
+ import { Button } from "@voyant-travel/ui/components/button";
5
+ import { Checkbox } from "@voyant-travel/ui/components/checkbox";
6
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@voyant-travel/ui/components/command";
7
+ import { Input } from "@voyant-travel/ui/components/input";
8
+ import { Popover, PopoverContent, PopoverTrigger } from "@voyant-travel/ui/components/popover";
9
+ import { PlusCircle, X } from "lucide-react";
10
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
11
+ import { AirlineLogo } from "./airline-logo.js";
12
+ export const EMPTY_FLIGHT_FILTERS = {
13
+ carriers: [],
14
+ maxStops: null,
15
+ maxPrice: null,
16
+ };
17
+ export function FlightFiltersBar({ value, onChange, offers, carrierName }) {
18
+ const messages = useFlightsUiMessagesOrDefault();
19
+ const carrierBuckets = deriveCarrierBuckets(offers);
20
+ const stopsBuckets = deriveStopsBuckets(offers);
21
+ const hasSelections = value.carriers.length > 0 || value.maxStops != null || value.maxPrice != null;
22
+ return (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(CarrierFilter, { buckets: carrierBuckets, selected: value.carriers, carrierName: carrierName, messages: messages.flightFiltersBar, onToggle: (code) => onChange({
23
+ ...value,
24
+ carriers: value.carriers.includes(code)
25
+ ? value.carriers.filter((c) => c !== code)
26
+ : [...value.carriers, code],
27
+ }), onClear: () => onChange({ ...value, carriers: [] }) }), _jsx(StopsFilter, { buckets: stopsBuckets, selected: value.maxStops, onSelect: (maxStops) => onChange({ ...value, maxStops }), messages: messages }), _jsx(PriceFilter, { value: value.maxPrice, onChange: (maxPrice) => onChange({ ...value, maxPrice }), messages: messages.flightFiltersBar }), hasSelections && (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 px-2 text-muted-foreground hover:text-foreground", onClick: () => onChange(EMPTY_FLIGHT_FILTERS), children: [_jsx(X, { className: "mr-1 h-3.5 w-3.5" }), messages.flightFiltersBar.clearAll] }))] }));
28
+ }
29
+ /**
30
+ * Shared inline trigger contents for the three filter popovers. Renders
31
+ * inside `<PopoverTrigger render={<Button .../>}>` as the button's
32
+ * children — base-ui merges its onClick / aria-expanded onto the Button
33
+ * directly. (Wrapping these in a separate component breaks the click flow
34
+ * because base-ui's prop-merge can't see through a custom wrapper.)
35
+ */
36
+ function TriggerContents({ label, count, preview, }) {
37
+ return (_jsxs(_Fragment, { children: [_jsx(PlusCircle, { className: "h-3.5 w-3.5" }), _jsx("span", { children: label }), preview, count != null && count > 0 && (_jsx("span", { className: "-mr-0.5 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-[11px] font-medium tabular-nums text-primary-foreground", children: count }))] }));
38
+ }
39
+ const TRIGGER_CLASS = "h-8 gap-2 border-dashed";
40
+ function CarrierFilter({ buckets, selected, carrierName, onToggle, onClear, messages, }) {
41
+ if (buckets.length === 0)
42
+ return null;
43
+ const selectedSet = new Set(selected);
44
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { render: _jsx(Button, { variant: "outline", size: "sm", className: TRIGGER_CLASS }), children: _jsx(TriggerContents, { label: messages.airlines, count: selected.length }) }), _jsx(PopoverContent, { className: "w-[260px] p-0", align: "start", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: messages.filterAirlinesPlaceholder }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: messages.noAirlines }), _jsx(CommandGroup, { children: buckets.map((b) => {
45
+ const isSelected = selectedSet.has(b.iataCode);
46
+ return (_jsxs(CommandItem, { onSelect: () => onToggle(b.iataCode), children: [_jsx(Checkbox, { checked: isSelected, tabIndex: -1, "aria-hidden": true, className: "mr-2 pointer-events-none" }), _jsx(AirlineLogo, { iataCode: b.iataCode, name: carrierName?.(b.iataCode), size: 20, className: "mr-2" }), _jsx("span", { className: "flex-1 truncate", children: carrierName?.(b.iataCode) ?? b.iataCode }), _jsx("span", { className: "ml-2 text-xs text-muted-foreground", children: b.count })] }, b.iataCode));
47
+ }) }), selected.length > 0 && (_jsxs(_Fragment, { children: [_jsx(CommandSeparator, {}), _jsx(CommandGroup, { children: _jsx(CommandItem, { onSelect: onClear, className: "justify-center text-center text-muted-foreground", children: messages.clearFilter }) })] }))] })] }) })] }));
48
+ }
49
+ function deriveCarrierBuckets(offers) {
50
+ const counts = new Map();
51
+ for (const offer of offers) {
52
+ const carriers = new Set();
53
+ for (const itin of offer.itineraries) {
54
+ for (const seg of itin.segments) {
55
+ carriers.add(seg.carrierCode);
56
+ }
57
+ }
58
+ for (const c of carriers)
59
+ counts.set(c, (counts.get(c) ?? 0) + 1);
60
+ }
61
+ return Array.from(counts.entries())
62
+ .map(([iataCode, count]) => ({ iataCode, count }))
63
+ .sort((a, b) => b.count - a.count);
64
+ }
65
+ function StopsFilter({ buckets, selected, onSelect, messages, }) {
66
+ if (buckets.length === 0)
67
+ return null;
68
+ const filterMessages = messages.flightFiltersBar;
69
+ const preview = selected != null ? (_jsx("span", { className: "text-muted-foreground", children: selected === 0 ? `· ${messages.common.stops.nonstop}` : `· ≤ ${selected}` })) : null;
70
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { render: _jsx(Button, { variant: "outline", size: "sm", className: TRIGGER_CLASS }), children: _jsx(TriggerContents, { label: filterMessages.stops, preview: preview }) }), _jsx(PopoverContent, { className: "w-[200px] p-0", align: "start", children: _jsx(Command, { children: _jsxs(CommandList, { children: [_jsx(CommandGroup, { children: buckets.map((b) => {
71
+ const isSelected = selected === b.stops;
72
+ return (_jsxs(CommandItem, { onSelect: () => onSelect(b.stops), children: [_jsx(Checkbox, { checked: isSelected, tabIndex: -1, "aria-hidden": true, className: "mr-2 pointer-events-none" }), _jsx("span", { className: "flex-1", children: b.stops === 0
73
+ ? messages.common.stops.nonstop
74
+ : formatMessage(b.stops === 1
75
+ ? messages.common.stops.upToOne
76
+ : messages.common.stops.upToMany, { count: b.stops }) }), _jsx("span", { className: "ml-2 text-xs text-muted-foreground", children: b.count })] }, b.stops));
77
+ }) }), selected != null && (_jsxs(_Fragment, { children: [_jsx(CommandSeparator, {}), _jsx(CommandGroup, { children: _jsx(CommandItem, { onSelect: () => onSelect(null), className: "justify-center text-center text-muted-foreground", children: filterMessages.clearFilter }) })] }))] }) }) })] }));
78
+ }
79
+ function deriveStopsBuckets(offers) {
80
+ const counts = new Map();
81
+ for (const offer of offers) {
82
+ const maxStops = Math.max(0, ...offer.itineraries.map((i) => i.segments.length - 1));
83
+ counts.set(maxStops, (counts.get(maxStops) ?? 0) + 1);
84
+ }
85
+ return Array.from(counts.entries())
86
+ .map(([stops, count]) => ({ stops, count }))
87
+ .sort((a, b) => a.stops - b.stops);
88
+ }
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+ // Price
91
+ // ─────────────────────────────────────────────────────────────────────────────
92
+ function PriceFilter({ value, onChange, messages, }) {
93
+ const preview = value != null ? _jsxs("span", { className: "text-muted-foreground", children: ["\u00B7 \u2264 ", value] }) : null;
94
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { render: _jsx(Button, { variant: "outline", size: "sm", className: TRIGGER_CLASS }), children: _jsx(TriggerContents, { label: messages.price, preview: preview }) }), _jsx(PopoverContent, { className: "w-[240px] p-3", align: "start", children: _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: messages.maximumPrice }), _jsx(Input, { type: "number", inputMode: "decimal", min: 0, placeholder: messages.noCap, defaultValue: value ?? "", className: "h-8", onBlur: (e) => {
95
+ const n = Number(e.target.value);
96
+ onChange(Number.isFinite(n) && n > 0 ? n : null);
97
+ } }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => onChange(null), disabled: value == null, className: "self-start", children: messages.clear })] }) })] }));
98
+ }
@@ -0,0 +1,28 @@
1
+ import type { Itinerary } from "@voyant-travel/flights/contract/types";
2
+ export interface FlightItineraryProps {
3
+ itinerary: Itinerary;
4
+ /** Optional label shown above the segment list (e.g. "Outbound", "Return"). */
5
+ label?: string;
6
+ /** Optional sub-label, typically the dated city pair: "BUH → LON · Mon, 13 Jul". */
7
+ sublabel?: string;
8
+ carrierName?: (iataCode: string) => string | undefined;
9
+ airportName?: (iataCode: string) => string | undefined;
10
+ aircraftName?: (iataCode: string) => string | undefined;
11
+ /** Compact rendering for use inside the ledger / right rail. */
12
+ compact?: boolean;
13
+ className?: string;
14
+ }
15
+ /**
16
+ * Carrier-aware itinerary renderer. One itinerary = one direction of travel
17
+ * (outbound, return, or one leg of an open-jaw). Surfaces:
18
+ * - per-segment carrier + flight number
19
+ * - operating-vs-marketing carrier ("Operated by …") for codeshares
20
+ * - layover dwell time chips between segments
21
+ * - aircraft per segment
22
+ * - total journey duration
23
+ *
24
+ * `compact` strips the per-segment cards down to a single timeline row —
25
+ * suitable for the booking ledger.
26
+ */
27
+ export declare function FlightItinerary({ itinerary, label, sublabel, carrierName, airportName, aircraftName, compact, className, }: FlightItineraryProps): import("react/jsx-runtime").JSX.Element | null;
28
+ //# sourceMappingURL=flight-itinerary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-itinerary.d.ts","sourceRoot":"","sources":["../../src/components/flight-itinerary.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,uCAAuC,CAAA;AASrF,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,SAAS,CAAA;IACpB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACtD,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACtD,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACvD,gEAAgE;IAChE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,KAAK,EACL,QAAQ,EACR,WAAW,EACX,WAAW,EACX,YAAY,EACZ,OAAO,EACP,SAAS,GACV,EAAE,oBAAoB,kDAiFtB"}