@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,27 @@
1
+ import type { AncillaryCatalog, AncillarySelection, FlightOffer, FlightPassenger, PassengerCounts } from "@voyant-travel/flights/contract/types";
2
+ type AssistancePicks = NonNullable<AncillarySelection["assistance"]>;
3
+ type ExtrasPicks = NonNullable<AncillarySelection["extras"]>;
4
+ export interface FlightServicesStepProps {
5
+ /** Per-leg catalogs — outbound required, return only when round-trip. */
6
+ outboundCatalog: AncillaryCatalog | null;
7
+ returnCatalog?: AncillaryCatalog | null;
8
+ outboundOffer: FlightOffer;
9
+ returnOffer?: FlightOffer;
10
+ passengers: FlightPassenger[];
11
+ passengerCounts: PassengerCounts;
12
+ assistance: AssistancePicks;
13
+ extras: ExtrasPicks;
14
+ onAssistanceChange: (next: AssistancePicks) => void;
15
+ onExtrasChange: (next: ExtrasPicks) => void;
16
+ loading?: boolean;
17
+ }
18
+ /**
19
+ * Combined services step covering special assistance (per-pax, trip-wide)
20
+ * and extras (per-pax, per-leg). Assistance is a flat checkbox list per
21
+ * passenger; extras are stepper-style line items so the operator can add
22
+ * multiples (e.g. two pets in cabin). Both are optional — passengers can
23
+ * leave the step blank and continue.
24
+ */
25
+ export declare function FlightServicesStep({ outboundCatalog, returnCatalog, outboundOffer, returnOffer, passengers, passengerCounts, assistance, extras, onAssistanceChange, onExtrasChange, loading, }: FlightServicesStepProps): import("react/jsx-runtime").JSX.Element;
26
+ export {};
27
+ //# sourceMappingURL=flight-services-step.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-services-step.d.ts","sourceRoot":"","sources":["../../src/components/flight-services-step.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,gBAAgB,EAEhB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,eAAe,EAChB,MAAM,uCAAuC,CAAA;AAS9C,KAAK,eAAe,GAAG,WAAW,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC,CAAA;AACpE,KAAK,WAAW,GAAG,WAAW,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC,CAAA;AAE5D,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,eAAe,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACxC,aAAa,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACvC,aAAa,EAAE,WAAW,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,UAAU,EAAE,eAAe,EAAE,CAAA;IAC7B,eAAe,EAAE,eAAe,CAAA;IAChC,UAAU,EAAE,eAAe,CAAA;IAC3B,MAAM,EAAE,WAAW,CAAA;IACnB,kBAAkB,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACnD,cAAc,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAA;IAC3C,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,eAAe,EACf,aAAa,EACb,aAAa,EACb,WAAW,EACX,UAAU,EACV,eAAe,EACf,UAAU,EACV,MAAM,EACN,kBAAkB,EAClB,cAAc,EACd,OAAO,GACR,EAAE,uBAAuB,2CA6FzB"}
@@ -0,0 +1,123 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } 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 { cn } from "@voyant-travel/ui/lib/utils";
7
+ import { Accessibility, Minus, Package, Plus, Sparkles } from "lucide-react";
8
+ import { useMemo } from "react";
9
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
10
+ /**
11
+ * Combined services step covering special assistance (per-pax, trip-wide)
12
+ * and extras (per-pax, per-leg). Assistance is a flat checkbox list per
13
+ * passenger; extras are stepper-style line items so the operator can add
14
+ * multiples (e.g. two pets in cabin). Both are optional — passengers can
15
+ * leave the step blank and continue.
16
+ */
17
+ export function FlightServicesStep({ outboundCatalog, returnCatalog, outboundOffer, returnOffer, passengers, passengerCounts, assistance, extras, onAssistanceChange, onExtrasChange, loading, }) {
18
+ const messages = useFlightsUiMessagesOrDefault();
19
+ const isRoundTrip = !!returnOffer;
20
+ const paxRows = useMemo(() => buildPassengerRows(passengers, passengerCounts, messages), [passengers, passengerCounts, messages]);
21
+ if (loading) {
22
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { className: "h-8 w-48 animate-pulse rounded bg-muted/40" }), _jsx("div", { className: "h-40 animate-pulse rounded-xl bg-muted/40" })] }));
23
+ }
24
+ if (!outboundCatalog) {
25
+ return (_jsx("div", { className: "rounded-xl border border-dashed p-6 text-center text-muted-foreground text-sm", children: messages.flightServicesStep.servicesUnavailable }));
26
+ }
27
+ const toggleAssistance = (passengerId, optionId, checked) => {
28
+ const filtered = assistance.filter((p) => !(p.passengerId === passengerId && p.optionId === optionId));
29
+ if (checked) {
30
+ onAssistanceChange([...filtered, { passengerId, optionId }]);
31
+ }
32
+ else {
33
+ onAssistanceChange(filtered);
34
+ }
35
+ };
36
+ const setExtraQty = (passengerId, sliceIndex, optionId, quantity) => {
37
+ const filtered = extras.filter((p) => !(p.passengerId === passengerId && p.sliceIndex === sliceIndex && p.optionId === optionId));
38
+ if (quantity > 0) {
39
+ onExtrasChange([...filtered, { passengerId, sliceIndex, optionId, quantity }]);
40
+ }
41
+ else {
42
+ onExtrasChange(filtered);
43
+ }
44
+ };
45
+ return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { children: [_jsx("h2", { className: "font-semibold text-base", children: messages.flightServicesStep.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: messages.flightServicesStep.description })] }), _jsx(AssistanceSection, { passengers: paxRows, options: outboundCatalog.assistance, value: assistance, onToggle: toggleAssistance, messages: messages }), _jsx(ExtrasSection, { legLabel: messages.common.legLabels.outbound, offer: outboundOffer, catalog: outboundCatalog, passengers: paxRows, sliceIndex: 0, value: extras, onSetQty: setExtraQty, messages: messages }), isRoundTrip && returnCatalog && returnOffer && (_jsx(ExtrasSection, { legLabel: messages.common.legLabels.return, offer: returnOffer, catalog: returnCatalog, passengers: paxRows, sliceIndex: 1, value: extras, onSetQty: setExtraQty, messages: messages }))] }));
46
+ }
47
+ function AssistanceSection({ passengers, options, value, onToggle, messages, }) {
48
+ return (_jsxs("section", { className: "rounded-xl border bg-card p-5 shadow-sm", children: [_jsxs("header", { className: "mb-4 flex items-center gap-2", children: [_jsx(Accessibility, { className: "h-4 w-4 text-muted-foreground" }), _jsx("h3", { className: "font-medium text-sm", children: messages.flightServicesStep.specialAssistance })] }), _jsx("div", { className: "flex flex-col gap-5", children: passengers.map((pax) => {
49
+ const paxPicks = value.filter((v) => v.passengerId === pax.passengerId);
50
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "font-medium text-sm", children: pax.label }), _jsx("span", { className: "text-[11px] text-muted-foreground", children: paxPicks.length === 0
51
+ ? messages.flightServicesStep.noAssistanceNeeded
52
+ : `${paxPicks.length} ${messages.common.selected}` })] }), _jsx("div", { className: "flex flex-wrap gap-2", children: options.map((opt) => {
53
+ const checked = paxPicks.some((p) => p.optionId === opt.id);
54
+ const id = `svc-${pax.passengerId}-${opt.id}`;
55
+ return (_jsxs("div", { className: cn("flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition-colors", checked
56
+ ? "border-primary bg-primary/5"
57
+ : "hover:border-primary/40 hover:bg-accent/30"), children: [_jsx(Checkbox, { id: id, checked: checked, onCheckedChange: (v) => onToggle(pax.passengerId, opt.id, !!v) }), _jsx("label", { htmlFor: id, className: "cursor-pointer", children: opt.label })] }, opt.id));
58
+ }) })] }, pax.passengerId));
59
+ }) })] }));
60
+ }
61
+ function ExtrasSection({ legLabel, offer, catalog, passengers, sliceIndex, value, onSetQty, messages, }) {
62
+ if (catalog.extras.length === 0)
63
+ return null;
64
+ const itin = offer.itineraries[0];
65
+ const first = itin?.segments[0];
66
+ const last = itin?.segments[itin.segments.length - 1];
67
+ return (_jsxs("section", { className: "rounded-xl border bg-card p-5 shadow-sm", children: [_jsxs("header", { className: "mb-4 flex items-baseline justify-between gap-2", children: [_jsxs("h3", { className: "flex items-center gap-2 font-medium text-sm", children: [_jsx(Sparkles, { className: "h-4 w-4 text-muted-foreground" }), formatMessage(messages.flightServicesStep.extras, { leg: legLabel })] }), first && last && (_jsxs("span", { className: "text-muted-foreground text-xs", children: [first.departure.iataCode, " \u2192 ", last.arrival.iataCode] }))] }), _jsx("div", { className: "flex flex-col gap-4", children: passengers.map((pax) => (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "font-medium text-sm", children: pax.label }), _jsx("ul", { className: "flex flex-col gap-1.5", children: catalog.extras.map((opt) => {
68
+ const pick = value.find((v) => v.passengerId === pax.passengerId &&
69
+ v.sliceIndex === sliceIndex &&
70
+ v.optionId === opt.id);
71
+ const qty = pick?.quantity ?? 0;
72
+ return (_jsx(ExtraRow, { option: opt, quantity: qty, onChange: (next) => onSetQty(pax.passengerId, sliceIndex, opt.id, next) }, opt.id));
73
+ }) })] }, pax.passengerId))) })] }));
74
+ }
75
+ function ExtraRow({ option, quantity, onChange, }) {
76
+ return (_jsxs("li", { className: "flex items-center justify-between gap-3 rounded-md border bg-card px-3 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Package, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate text-sm", children: option.label }), _jsx("span", { className: "font-mono text-[11px] text-muted-foreground", children: formatMoney(option.price.amount, option.price.currency) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "h-7 w-7", onClick: () => onChange(Math.max(0, quantity - 1)), disabled: quantity === 0, children: _jsx(Minus, { className: "h-3 w-3" }) }), _jsx("span", { className: "min-w-6 text-center font-medium text-sm tabular-nums", children: quantity }), _jsx(Button, { type: "button", variant: "outline", size: "icon", className: "h-7 w-7", onClick: () => onChange(Math.min(9, quantity + 1)), children: _jsx(Plus, { className: "h-3 w-3" }) })] })] }));
77
+ }
78
+ // ── Helpers ──────────────────────────────────────────────────────────────────
79
+ function buildPassengerRows(passengers, counts, messages) {
80
+ if (passengers.length > 0) {
81
+ return passengers.map((p) => ({
82
+ passengerId: p.passengerId,
83
+ label: nameOrFallback(p, messages),
84
+ }));
85
+ }
86
+ const out = [];
87
+ for (let i = 1; i <= counts.adults; i++) {
88
+ out.push({
89
+ passengerId: `pax_adult_${i}`,
90
+ label: `${messages.common.passengerTypeLabels.adult} ${i}`,
91
+ });
92
+ }
93
+ for (let i = 1; i <= (counts.children ?? 0); i++) {
94
+ out.push({
95
+ passengerId: `pax_child_${i}`,
96
+ label: `${messages.common.passengerTypeLabels.child} ${i}`,
97
+ });
98
+ }
99
+ for (let i = 1; i <= (counts.infants ?? 0); i++) {
100
+ out.push({
101
+ passengerId: `pax_infant_${i}`,
102
+ label: `${messages.common.passengerTypeLabels.infant} ${i}`,
103
+ });
104
+ }
105
+ return out;
106
+ }
107
+ function nameOrFallback(p, messages) {
108
+ const full = `${p.firstName} ${p.lastName}`.trim();
109
+ if (full)
110
+ return full;
111
+ const idx = p.passengerId.match(/_(\d+)$/)?.[1] ?? "1";
112
+ return `${messages.common.passengerTypeLabels[p.type]} ${idx}`;
113
+ }
114
+ function formatMoney(amount, currency) {
115
+ const n = Number(amount);
116
+ if (!Number.isFinite(n))
117
+ return `${amount} ${currency}`;
118
+ return new Intl.NumberFormat(undefined, {
119
+ style: "currency",
120
+ currency,
121
+ maximumFractionDigits: 0,
122
+ }).format(n);
123
+ }
@@ -0,0 +1,27 @@
1
+ import type { FlightOffer } from "@voyant-travel/flights/contract/types";
2
+ import type { FlightFiltersValue } from "./flight-filters-bar.js";
3
+ export declare function PickedLegBanner({ label, offer, carrierName, airportName, onChange, }: {
4
+ label: string;
5
+ offer: FlightOffer;
6
+ carrierName: (code: string) => string | undefined;
7
+ airportName: (code: string) => string | undefined;
8
+ onChange: () => void;
9
+ }): import("react/jsx-runtime").JSX.Element | null;
10
+ export declare function ReadyToBookPanel({ outbound, returnLeg, carrierName, airportName, onChangeOutbound, onChangeReturn, onContinue, }: {
11
+ outbound: FlightOffer;
12
+ returnLeg: FlightOffer;
13
+ carrierName: (code: string) => string | undefined;
14
+ airportName: (code: string) => string | undefined;
15
+ onChangeOutbound: () => void;
16
+ onChangeReturn: () => void;
17
+ onContinue: () => void;
18
+ }): import("react/jsx-runtime").JSX.Element;
19
+ export declare function CacheColdBanner({ message, onReset }: {
20
+ message: string;
21
+ onReset: () => void;
22
+ }): import("react/jsx-runtime").JSX.Element;
23
+ export declare function NoResults({ hasFilters }: {
24
+ hasFilters: boolean;
25
+ }): import("react/jsx-runtime").JSX.Element;
26
+ export declare function hasActiveFilters(filters: FlightFiltersValue): boolean;
27
+ //# sourceMappingURL=flights-page-panels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flights-page-panels.d.ts","sourceRoot":"","sources":["../../src/components/flights-page-panels.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAA;AAIxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,KAAK,EACL,WAAW,EACX,WAAW,EACX,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,WAAW,CAAA;IAClB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACjD,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACjD,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB,kDA4BA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,SAAS,EACT,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,UAAU,GACX,EAAE;IACD,QAAQ,EAAE,WAAW,CAAA;IACrB,SAAS,EAAE,WAAW,CAAA;IACtB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACjD,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACjD,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,UAAU,EAAE,MAAM,IAAI,CAAA;CACvB,2CAuCA;AAED,wBAAgB,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,2CAW7F;AAED,wBAAgB,SAAS,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,OAAO,CAAA;CAAE,2CAOhE;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAErE"}
@@ -0,0 +1,40 @@
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, Pencil } from "lucide-react";
5
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
6
+ import { FlightItinerary } from "./flight-itinerary.js";
7
+ export function PickedLegBanner({ label, offer, carrierName, airportName, onChange, }) {
8
+ const messages = useFlightsUiMessagesOrDefault().flightsPage;
9
+ const itinerary = offer.itineraries[0];
10
+ if (!itinerary)
11
+ return null;
12
+ return (_jsxs("div", { className: "rounded-xl border bg-card p-4 shadow-sm", children: [_jsxs("div", { className: "mb-3 flex items-center justify-between gap-3", children: [_jsx("span", { className: "font-medium text-[11px] uppercase tracking-wider text-emerald-700", children: label }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "font-semibold text-base tabular-nums", children: formatMoney(offer.totalPrice.amount, offer.totalPrice.currency) }), _jsxs(Button, { variant: "ghost", size: "sm", onClick: onChange, children: [_jsx(Pencil, { className: "mr-1 h-3.5 w-3.5" }), messages.change] })] })] }), _jsx(FlightItinerary, { itinerary: itinerary, compact: true, carrierName: carrierName, airportName: airportName })] }));
13
+ }
14
+ export function ReadyToBookPanel({ outbound, returnLeg, carrierName, airportName, onChangeOutbound, onChangeReturn, onContinue, }) {
15
+ const messages = useFlightsUiMessagesOrDefault().flightsPage;
16
+ const total = Number(outbound.totalPrice.amount) + Number(returnLeg.totalPrice.amount);
17
+ const currency = outbound.totalPrice.currency;
18
+ return (_jsxs("section", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(PickedLegBanner, { label: messages.selectedOutbound, offer: outbound, carrierName: carrierName, airportName: airportName, onChange: onChangeOutbound }), _jsx(PickedLegBanner, { label: messages.selectedReturn, offer: returnLeg, carrierName: carrierName, airportName: airportName, onChange: onChangeReturn })] }), _jsxs("div", { className: "flex flex-col items-stretch gap-3 rounded-xl border bg-card p-5 shadow-sm md:flex-row md:items-center md:justify-between", children: [_jsxs("div", { className: "flex flex-col leading-tight", children: [_jsx("span", { className: "font-medium text-[11px] uppercase tracking-wider text-muted-foreground", children: messages.tripTotal }), _jsx("span", { className: "font-semibold text-2xl tabular-nums", children: formatMoney(total.toFixed(2), currency) }), _jsx("span", { className: "text-muted-foreground text-xs", children: messages.tripTotalDescription })] }), _jsxs(Button, { size: "lg", onClick: onContinue, className: "md:px-8", children: [messages.continueToBooking, _jsx(ChevronRight, { className: "ml-1 h-4 w-4" })] })] })] }));
19
+ }
20
+ export function CacheColdBanner({ message, onReset }) {
21
+ const messages = useFlightsUiMessagesOrDefault().flightsPage;
22
+ return (_jsxs("div", { className: "rounded-md border border-dashed bg-card p-6 text-center text-muted-foreground text-sm", children: [_jsx("p", { children: message }), _jsxs(Button, { className: "mt-3", variant: "outline", onClick: onReset, children: [_jsx(ChevronLeft, { className: "mr-1 h-4 w-4" }), messages.pickOutboundAgain] })] }));
23
+ }
24
+ export function NoResults({ hasFilters }) {
25
+ const messages = useFlightsUiMessagesOrDefault().flightsPage;
26
+ return (_jsx("div", { className: "rounded-lg border border-dashed p-8 text-center text-muted-foreground text-sm", children: hasFilters ? messages.noFilteredResults : messages.noRouteResults }));
27
+ }
28
+ export function hasActiveFilters(filters) {
29
+ return filters.carriers.length > 0 || filters.maxStops != null || filters.maxPrice != null;
30
+ }
31
+ function formatMoney(amount, currency) {
32
+ const n = Number(amount);
33
+ if (!Number.isFinite(n))
34
+ return `${amount} ${currency}`;
35
+ return new Intl.NumberFormat(undefined, {
36
+ style: "currency",
37
+ currency,
38
+ maximumFractionDigits: 0,
39
+ }).format(n);
40
+ }
@@ -0,0 +1,39 @@
1
+ import type { CabinClass, PassengerCounts } from "@voyant-travel/flights/contract/types";
2
+ import type { TripType } from "./flight-search-form.js";
3
+ import type { PopularRoute } from "./popular-routes.js";
4
+ export type FlowStage = "outbound" | "return" | "ready";
5
+ export interface FlightsPageSearchParams {
6
+ tripType?: TripType;
7
+ from?: string;
8
+ to?: string;
9
+ depart?: string;
10
+ ret?: string;
11
+ leg?: "outbound" | "return";
12
+ outboundOfferId?: string;
13
+ returnOfferId?: string;
14
+ pax_a?: number;
15
+ pax_c?: number;
16
+ pax_i?: number;
17
+ cabin?: CabinClass;
18
+ carriers?: string[];
19
+ maxStops?: number;
20
+ maxPrice?: number;
21
+ page?: number;
22
+ }
23
+ export interface FlightsPageSearchChangeOptions {
24
+ replace?: boolean;
25
+ }
26
+ export interface FlightBookingNavigationTarget {
27
+ outboundOfferId: string;
28
+ returnOfferId?: string;
29
+ passengers: PassengerCounts;
30
+ cabin: CabinClass;
31
+ }
32
+ export interface FlightsPageProps {
33
+ search: FlightsPageSearchParams;
34
+ onSearchChange: (next: FlightsPageSearchParams, options?: FlightsPageSearchChangeOptions) => void;
35
+ onBookOffer: (target: FlightBookingNavigationTarget) => void;
36
+ routes?: PopularRoute[];
37
+ className?: string;
38
+ }
39
+ //# sourceMappingURL=flights-page-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flights-page-types.d.ts","sourceRoot":"","sources":["../../src/components/flights-page-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAA;AACxF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEvD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAA;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,8BAA8B;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,6BAA6B;IAC5C,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,eAAe,CAAA;IAC3B,KAAK,EAAE,UAAU,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,uBAAuB,CAAA;IAC/B,cAAc,EAAE,CAAC,IAAI,EAAE,uBAAuB,EAAE,OAAO,CAAC,EAAE,8BAA8B,KAAK,IAAI,CAAA;IACjG,WAAW,EAAE,CAAC,MAAM,EAAE,6BAA6B,KAAK,IAAI,CAAA;IAC5D,MAAM,CAAC,EAAE,YAAY,EAAE,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { QueryClient } from "@tanstack/react-query";
2
+ import type { FlightOffer, FlightSearchRequest } from "@voyant-travel/flights/contract/types";
3
+ import type { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
4
+ import { type FlightFiltersValue } from "./flight-filters-bar.js";
5
+ import type { FlightsPageSearchParams, FlowStage } from "./flights-page-types.js";
6
+ export declare const PAGE_SIZE = 20;
7
+ export declare const EMPTY_REQUEST_FOR_DISABLED: FlightSearchRequest;
8
+ export declare function legHeading(messages: ReturnType<typeof useFlightsUiMessagesOrDefault>["flightsPage"], stage: FlowStage, isRoundTrip: boolean, from?: string, to?: string): string;
9
+ export declare function selectLabel(messages: ReturnType<typeof useFlightsUiMessagesOrDefault>["flightsPage"], stage: FlowStage, isRoundTrip: boolean): string;
10
+ export declare function readOfferFromCache(qc: QueryClient, offerId: string): FlightOffer | null;
11
+ export declare function urlToBaseRequest(search: FlightsPageSearchParams, leg: "outbound" | "return"): FlightSearchRequest | null;
12
+ export declare function urlToFilters(search: FlightsPageSearchParams): FlightFiltersValue;
13
+ export declare function composeRequest(base: FlightSearchRequest, filters: FlightFiltersValue, page: number): FlightSearchRequest;
14
+ //# sourceMappingURL=flights-page-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flights-page-utils.d.ts","sourceRoot":"","sources":["../../src/components/flights-page-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAA;AAE7F,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAA;AAErE,OAAO,EAAwB,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAEjF,eAAO,MAAM,SAAS,KAAK,CAAA;AAE3B,eAAO,MAAM,0BAA0B,EAAE,mBAIxC,CAAA;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,UAAU,CAAC,OAAO,6BAA6B,CAAC,CAAC,aAAa,CAAC,EACzE,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,OAAO,EACpB,IAAI,CAAC,EAAE,MAAM,EACb,EAAE,CAAC,EAAE,MAAM,GACV,MAAM,CASR;AAED,wBAAgB,WAAW,CACzB,QAAQ,EAAE,UAAU,CAAC,OAAO,6BAA6B,CAAC,CAAC,aAAa,CAAC,EACzE,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,OAAO,GACnB,MAAM,CAKR;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAGvF;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,uBAAuB,EAC/B,GAAG,EAAE,UAAU,GAAG,QAAQ,GACzB,mBAAmB,GAAG,IAAI,CAmB5B;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,uBAAuB,GAAG,kBAAkB,CAOhF;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,MAAM,GACX,mBAAmB,CAerB"}
@@ -0,0 +1,79 @@
1
+ import { formatMessage } from "@voyant-travel/i18n";
2
+ import { flightsQueryKeys } from "../index.js";
3
+ import { EMPTY_FLIGHT_FILTERS } from "./flight-filters-bar.js";
4
+ export const PAGE_SIZE = 20;
5
+ export const EMPTY_REQUEST_FOR_DISABLED = {
6
+ slices: [],
7
+ passengers: { adults: 1 },
8
+ cabin: "economy",
9
+ };
10
+ export function legHeading(messages, stage, isRoundTrip, from, to) {
11
+ if (!isRoundTrip)
12
+ return messages.availableFlights;
13
+ if (stage === "outbound") {
14
+ return formatMessage(messages.outboundHeading, { from: from ?? "?", to: to ?? "?" });
15
+ }
16
+ if (stage === "return") {
17
+ return formatMessage(messages.returnHeading, { from: to ?? "?", to: from ?? "?" });
18
+ }
19
+ return messages.tripHeading;
20
+ }
21
+ export function selectLabel(messages, stage, isRoundTrip) {
22
+ if (!isRoundTrip)
23
+ return messages.bookThisFlight;
24
+ if (stage === "outbound")
25
+ return messages.selectOutbound;
26
+ if (stage === "return")
27
+ return messages.selectReturn;
28
+ return messages.continueToBooking;
29
+ }
30
+ export function readOfferFromCache(qc, offerId) {
31
+ const cached = qc.getQueryData(flightsQueryKeys.offerDetail(offerId));
32
+ return cached?.offer ?? null;
33
+ }
34
+ export function urlToBaseRequest(search, leg) {
35
+ if (!search.from || !search.to || !search.depart)
36
+ return null;
37
+ const isRoundTrip = (search.tripType ?? "round_trip") === "round_trip";
38
+ if (leg === "return" && (!isRoundTrip || !search.ret))
39
+ return null;
40
+ const slice = leg === "outbound"
41
+ ? { origin: search.from, destination: search.to, departureDate: search.depart }
42
+ : { origin: search.to, destination: search.from, departureDate: search.ret };
43
+ return {
44
+ slices: [slice],
45
+ passengers: {
46
+ adults: search.pax_a ?? 1,
47
+ children: search.pax_c ?? 0,
48
+ infants: search.pax_i ?? 0,
49
+ },
50
+ cabin: search.cabin ?? "economy",
51
+ };
52
+ }
53
+ export function urlToFilters(search) {
54
+ return {
55
+ ...EMPTY_FLIGHT_FILTERS,
56
+ carriers: search.carriers ?? [],
57
+ maxStops: search.maxStops ?? null,
58
+ maxPrice: search.maxPrice ?? null,
59
+ };
60
+ }
61
+ export function composeRequest(base, filters, page) {
62
+ const searchOptions = {};
63
+ if (filters.carriers.length > 0)
64
+ searchOptions.includeCarriers = filters.carriers;
65
+ if (filters.maxStops === 0)
66
+ searchOptions.directOnly = true;
67
+ else if (filters.maxStops != null)
68
+ searchOptions.maxStops = filters.maxStops;
69
+ if (filters.maxPrice != null)
70
+ searchOptions.maxPrice = filters.maxPrice;
71
+ return {
72
+ ...base,
73
+ ...(Object.keys(searchOptions).length > 0 ? { searchOptions } : {}),
74
+ pagination: {
75
+ limit: PAGE_SIZE,
76
+ ...(page > 1 ? { cursor: String(page) } : {}),
77
+ },
78
+ };
79
+ }
@@ -0,0 +1,4 @@
1
+ import type { FlightsPageProps } from "./flights-page-types.js";
2
+ export type { FlightBookingNavigationTarget, FlightsPageProps, FlightsPageSearchChangeOptions, FlightsPageSearchParams, } from "./flights-page-types.js";
3
+ export declare function FlightsPage({ search, onSearchChange, onBookOffer, routes, className, }: FlightsPageProps): import("react/jsx-runtime").JSX.Element;
4
+ //# sourceMappingURL=flights-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flights-page.d.ts","sourceRoot":"","sources":["../../src/components/flights-page.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,gBAAgB,EAAa,MAAM,yBAAyB,CAAA;AAE1E,YAAY,EACV,6BAA6B,EAC7B,gBAAgB,EAChB,8BAA8B,EAC9B,uBAAuB,GACxB,MAAM,yBAAyB,CAAA;AAqBhC,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,cAAc,EACd,WAAW,EACX,MAA+B,EAC/B,SAAS,GACV,EAAE,gBAAgB,2CAyYlB"}
@@ -0,0 +1,209 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useQueryClient } from "@tanstack/react-query";
4
+ import { formatMessage } from "@voyant-travel/i18n";
5
+ import { Badge } from "@voyant-travel/ui/components/badge";
6
+ import { Button } from "@voyant-travel/ui/components/button";
7
+ import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, } from "@voyant-travel/ui/components/sheet";
8
+ import { cn } from "@voyant-travel/ui/lib/utils";
9
+ import { ChevronLeft, ChevronRight, Plane } from "lucide-react";
10
+ import { useMemo, useState } from "react";
11
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
12
+ import { flightsQueryKeys, useAirlines, useAirports, useFlightSearch } from "../index.js";
13
+ import { FlightFiltersBar } from "./flight-filters-bar.js";
14
+ import { FlightOfferDetail } from "./flight-offer-detail.js";
15
+ import { FlightOfferRow } from "./flight-offer-row.js";
16
+ import { FlightSearchForm } from "./flight-search-form.js";
17
+ import { CacheColdBanner, hasActiveFilters, NoResults, PickedLegBanner, ReadyToBookPanel, } from "./flights-page-panels.js";
18
+ import { composeRequest, EMPTY_REQUEST_FOR_DISABLED, legHeading, PAGE_SIZE, readOfferFromCache, selectLabel, urlToBaseRequest, urlToFilters, } from "./flights-page-utils.js";
19
+ import { DEFAULT_POPULAR_ROUTES, PopularRoutes } from "./popular-routes.js";
20
+ export function FlightsPage({ search, onSearchChange, onBookOffer, routes = DEFAULT_POPULAR_ROUTES, className, }) {
21
+ const messages = useFlightsUiMessagesOrDefault().flightsPage;
22
+ const qc = useQueryClient();
23
+ const [openOffer, setOpenOffer] = useState(null);
24
+ const isRoundTrip = (search.tripType ?? "round_trip") === "round_trip";
25
+ const stage = (() => {
26
+ if (!isRoundTrip)
27
+ return "outbound";
28
+ if (!search.outboundOfferId)
29
+ return "outbound";
30
+ if (!search.returnOfferId)
31
+ return "return";
32
+ return "ready";
33
+ })();
34
+ const outboundOffer = useMemo(() => (search.outboundOfferId ? readOfferFromCache(qc, search.outboundOfferId) : null), [search.outboundOfferId, qc]);
35
+ const returnOffer = useMemo(() => (search.returnOfferId ? readOfferFromCache(qc, search.returnOfferId) : null), [search.returnOfferId, qc]);
36
+ const airlinesQuery = useAirlines();
37
+ const airportsQuery = useAirports({ limit: 200 });
38
+ const carrierMap = useMemo(() => {
39
+ const map = new Map();
40
+ for (const airline of airlinesQuery.data?.data ?? []) {
41
+ map.set(airline.iataCode, airline.name);
42
+ }
43
+ return map;
44
+ }, [airlinesQuery.data]);
45
+ const airportMap = useMemo(() => {
46
+ const map = new Map();
47
+ for (const airport of airportsQuery.data?.data ?? []) {
48
+ map.set(airport.iataCode, `${airport.city} (${airport.iataCode})`);
49
+ }
50
+ return map;
51
+ }, [airportsQuery.data]);
52
+ const carrierName = (code) => carrierMap.get(code);
53
+ const airportName = (code) => airportMap.get(code);
54
+ const baseRequest = useMemo(() => urlToBaseRequest(search, stage === "ready" ? "outbound" : stage), [search, stage]);
55
+ const filters = useMemo(() => urlToFilters(search), [search]);
56
+ const searchEnabled = baseRequest != null && stage !== "ready";
57
+ const request = useMemo(() => baseRequest && stage !== "ready"
58
+ ? composeRequest(baseRequest, filters, search.page ?? 1)
59
+ : null, [baseRequest, filters, search.page, stage]);
60
+ const flightSearchQuery = useFlightSearch(request ?? EMPTY_REQUEST_FOR_DISABLED, {
61
+ enabled: searchEnabled,
62
+ });
63
+ const offers = flightSearchQuery.data?.offers ?? [];
64
+ const meta = flightSearchQuery.data?.pagination;
65
+ const total = meta?.total ?? offers.length;
66
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
67
+ const page = search.page ?? 1;
68
+ const rangeStart = total === 0 ? 0 : (page - 1) * PAGE_SIZE + 1;
69
+ const rangeEnd = Math.min(page * PAGE_SIZE, total);
70
+ const passengers = useMemo(() => ({
71
+ adults: search.pax_a ?? 1,
72
+ children: search.pax_c ?? 0,
73
+ infants: search.pax_i ?? 0,
74
+ }), [search.pax_a, search.pax_c, search.pax_i]);
75
+ const cabin = search.cabin ?? "economy";
76
+ const pickOffer = (offer) => {
77
+ qc.setQueryData(flightsQueryKeys.offerDetail(offer.offerId), { offer });
78
+ if (!isRoundTrip) {
79
+ onBookOffer({
80
+ outboundOfferId: offer.offerId,
81
+ passengers,
82
+ cabin,
83
+ });
84
+ return;
85
+ }
86
+ if (stage === "outbound") {
87
+ onSearchChange({
88
+ ...search,
89
+ leg: "return",
90
+ outboundOfferId: offer.offerId,
91
+ returnOfferId: undefined,
92
+ page: 1,
93
+ });
94
+ return;
95
+ }
96
+ if (stage === "return") {
97
+ onSearchChange({
98
+ ...search,
99
+ returnOfferId: offer.offerId,
100
+ });
101
+ }
102
+ };
103
+ const proceedToBooking = () => {
104
+ if (!search.outboundOfferId)
105
+ return;
106
+ onBookOffer({
107
+ outboundOfferId: search.outboundOfferId,
108
+ returnOfferId: search.returnOfferId,
109
+ passengers,
110
+ cabin,
111
+ });
112
+ };
113
+ const changeOutbound = () => {
114
+ onSearchChange({
115
+ ...search,
116
+ leg: "outbound",
117
+ outboundOfferId: undefined,
118
+ returnOfferId: undefined,
119
+ page: 1,
120
+ });
121
+ };
122
+ const changeReturn = () => {
123
+ onSearchChange({
124
+ ...search,
125
+ leg: "return",
126
+ returnOfferId: undefined,
127
+ page: 1,
128
+ });
129
+ };
130
+ const handleSubmit = (next) => {
131
+ const first = next.slices[0];
132
+ const second = next.slices[1];
133
+ onSearchChange({
134
+ tripType: next.slices.length === 2 ? "round_trip" : "one_way",
135
+ from: first?.origin,
136
+ to: first?.destination,
137
+ depart: first?.departureDate,
138
+ ret: second?.departureDate,
139
+ pax_a: next.passengers.adults,
140
+ pax_c: next.passengers.children ?? 0,
141
+ pax_i: next.passengers.infants ?? 0,
142
+ cabin: next.cabin ?? "economy",
143
+ leg: "outbound",
144
+ outboundOfferId: undefined,
145
+ returnOfferId: undefined,
146
+ page: 1,
147
+ });
148
+ };
149
+ const handleFiltersChange = (nextFilters) => {
150
+ onSearchChange({
151
+ ...search,
152
+ carriers: nextFilters.carriers.length > 0 ? nextFilters.carriers : undefined,
153
+ maxStops: nextFilters.maxStops ?? undefined,
154
+ maxPrice: nextFilters.maxPrice ?? undefined,
155
+ page: 1,
156
+ }, { replace: true });
157
+ };
158
+ const setPage = (next) => {
159
+ onSearchChange({ ...search, page: next }, { replace: true });
160
+ };
161
+ const formInitial = useMemo(() => {
162
+ if (!search.from || !search.to || !search.depart)
163
+ return undefined;
164
+ const slices = [{ origin: search.from, destination: search.to, departureDate: search.depart }];
165
+ if (isRoundTrip && search.ret) {
166
+ slices.push({ origin: search.to, destination: search.from, departureDate: search.ret });
167
+ }
168
+ return {
169
+ slices,
170
+ passengers,
171
+ cabin,
172
+ tripType: search.tripType,
173
+ };
174
+ }, [
175
+ cabin,
176
+ isRoundTrip,
177
+ passengers,
178
+ search.depart,
179
+ search.from,
180
+ search.ret,
181
+ search.to,
182
+ search.tripType,
183
+ ]);
184
+ const hasSearchInput = baseRequest != null;
185
+ return (_jsxs("div", { className: cn("mx-auto flex w-full max-w-screen-2xl flex-col gap-5 px-6 py-6 lg:px-8", className), children: [_jsxs("header", { children: [_jsx("h1", { className: "font-semibold text-2xl", children: messages.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: messages.description })] }), _jsx(FlightSearchForm, { onSearch: handleSubmit, loading: flightSearchQuery.isFetching, initial: formInitial }, hasSearchInput
186
+ ? `${search.from}-${search.to}-${search.depart}-${search.ret ?? ""}`
187
+ : "empty"), !hasSearchInput && _jsx(PopularRoutes, { routes: routes, onSelect: handleSubmit }), stage === "ready" && outboundOffer && returnOffer && (_jsx(ReadyToBookPanel, { outbound: outboundOffer, returnLeg: returnOffer, carrierName: carrierName, airportName: airportName, onChangeOutbound: changeOutbound, onChangeReturn: changeReturn, onContinue: proceedToBooking })), stage === "ready" && (!outboundOffer || !returnOffer) && (_jsx(CacheColdBanner, { message: messages.pickedOfferMissing, onReset: changeOutbound })), stage === "return" && !outboundOffer && (_jsx(CacheColdBanner, { message: messages.outboundOfferMissing, onReset: changeOutbound })), hasSearchInput && stage !== "ready" && flightSearchQuery.isError && (_jsx("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 p-4 text-destructive text-sm", children: flightSearchQuery.error instanceof Error
188
+ ? flightSearchQuery.error.message
189
+ : messages.searchFailed })), hasSearchInput && stage !== "ready" && !flightSearchQuery.isError && (_jsxs(_Fragment, { children: [_jsx(FlightFiltersBar, { value: filters, onChange: handleFiltersChange, offers: offers, carrierName: carrierName }), stage === "return" && outboundOffer && (_jsx(PickedLegBanner, { label: messages.selectedOutbound, offer: outboundOffer, carrierName: carrierName, airportName: airportName, onChange: changeOutbound })), _jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsx("h2", { className: "font-medium text-base", children: legHeading(messages, stage, isRoundTrip, search.from, search.to) }), _jsx("span", { className: "text-muted-foreground text-sm", children: flightSearchQuery.isFetching
190
+ ? messages.searching
191
+ : total === 0
192
+ ? messages.zeroOffers
193
+ : formatMessage(messages.offersSummary, {
194
+ start: String(rangeStart),
195
+ end: String(rangeEnd),
196
+ total: String(total),
197
+ plural: total === 1 ? "" : "s",
198
+ }) })] }), flightSearchQuery.isFetching ? (_jsx("div", { className: "flex flex-col gap-2", children: Array.from({ length: 4 }).map((_, index) => (_jsx("div", { className: "h-24 animate-pulse rounded-lg border bg-muted/40" }, index))) })) : offers.length === 0 ? (_jsx(NoResults, { hasFilters: hasActiveFilters(filters) })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex flex-col gap-2", children: offers.map((offer) => (_jsx(FlightOfferRow, { offer: offer, carrierName: carrierName, onClick: (nextOffer) => setOpenOffer(nextOffer), onSelect: pickOffer, selectLabel: selectLabel(messages, stage, isRoundTrip) }, offer.offerId))) }), totalPages > 1 && (_jsxs("div", { className: "mt-2 flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-muted-foreground text-sm", children: formatMessage(messages.pageSummary, {
199
+ page: String(page),
200
+ totalPages: String(totalPages),
201
+ }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setPage(Math.max(1, page - 1)), disabled: page <= 1, children: [_jsx(ChevronLeft, { className: "mr-1 h-4 w-4" }), messages.previous] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setPage(Math.min(totalPages, page + 1)), disabled: page >= totalPages || meta?.hasMore === false, children: [messages.next, _jsx(ChevronRight, { className: "ml-1 h-4 w-4" })] })] })] }))] }))] })), _jsx(Sheet, { open: openOffer != null, onOpenChange: (open) => {
202
+ if (!open)
203
+ setOpenOffer(null);
204
+ }, children: _jsx(SheetContent, { className: "w-full p-0 sm:max-w-2xl", children: _jsxs("div", { className: "flex h-full flex-col", children: [_jsx(SheetHeader, { className: "border-b px-6 py-5", children: _jsxs(SheetTitle, { className: "flex items-center gap-2 text-base", children: [_jsx(Plane, { className: "h-4 w-4" }), messages.flightOffer, openOffer?.validatingCarrier && (_jsx(Badge, { variant: "secondary", children: openOffer.validatingCarrier }))] }) }), _jsx("div", { className: "flex-1 overflow-y-auto px-6 py-5", children: openOffer && (_jsx(FlightOfferDetail, { offer: openOffer, carrierName: carrierName, airportName: airportName })) }), openOffer && (_jsx(SheetFooter, { className: "border-t bg-muted/20 px-6 py-3", children: _jsx("div", { className: "flex w-full items-center justify-end gap-2", children: _jsx(Button, { onClick: () => {
205
+ const offer = openOffer;
206
+ setOpenOffer(null);
207
+ pickOffer(offer);
208
+ }, children: selectLabel(messages, stage, isRoundTrip) }) }) }))] }) }) })] }));
209
+ }
@@ -0,0 +1,16 @@
1
+ import type { PassengerPrefill } from "./flight-passenger-form.js";
2
+ export interface PassengerContactPickerProps {
3
+ /** Called when the user picks a CRM person; fields get merged into the passenger card. */
4
+ onPick: (prefill: PassengerPrefill) => void;
5
+ /** Optional CRM route callback. When omitted, the "add contact" action is hidden. */
6
+ onAddContact?: () => void;
7
+ /** Exposes the selected CRM person id to parent flows such as saved-payment lookup. */
8
+ onPersonSelected?: (personId: string | null) => void;
9
+ }
10
+ /**
11
+ * Passenger card "Pick from contacts" trigger. Opens a popover with a
12
+ * searchable list of CRM people and maps the picked person into the
13
+ * `PassengerPrefill` shape expected by `FlightPassengerForm`.
14
+ */
15
+ export declare function PassengerContactPicker({ onPick, onAddContact, onPersonSelected, }: PassengerContactPickerProps): import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=passenger-contact-picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passenger-contact-picker.d.ts","sourceRoot":"","sources":["../../src/components/passenger-contact-picker.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAElE,MAAM,WAAW,2BAA2B;IAC1C,0FAA0F;IAC1F,MAAM,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAA;IAC3C,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;IACzB,uFAAuF;IACvF,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CACrD;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,YAAY,EACZ,gBAAgB,GACjB,EAAE,2BAA2B,2CA0F7B"}