@voyantjs/flights-ui 0.35.0 → 0.37.1

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 (56) hide show
  1. package/dist/components/airport-combobox.d.ts.map +1 -1
  2. package/dist/components/airport-combobox.js +4 -2
  3. package/dist/components/flight-baggage-step.d.ts.map +1 -1
  4. package/dist/components/flight-baggage-step.js +29 -18
  5. package/dist/components/flight-billing-step.d.ts.map +1 -1
  6. package/dist/components/flight-billing-step.js +30 -24
  7. package/dist/components/flight-booking-journey.d.ts.map +1 -1
  8. package/dist/components/flight-booking-journey.js +15 -26
  9. package/dist/components/flight-booking-ledger.d.ts.map +1 -1
  10. package/dist/components/flight-booking-ledger.js +22 -12
  11. package/dist/components/flight-booking-page.js +12 -12
  12. package/dist/components/flight-booking-shell.d.ts.map +1 -1
  13. package/dist/components/flight-booking-shell.js +54 -29
  14. package/dist/components/flight-contact-form.d.ts.map +1 -1
  15. package/dist/components/flight-contact-form.js +7 -3
  16. package/dist/components/flight-fare-upsell-step.d.ts.map +1 -1
  17. package/dist/components/flight-fare-upsell-step.js +58 -30
  18. package/dist/components/flight-filters-bar.d.ts.map +1 -1
  19. package/dist/components/flight-filters-bar.js +21 -13
  20. package/dist/components/flight-itinerary.d.ts.map +1 -1
  21. package/dist/components/flight-itinerary.js +26 -6
  22. package/dist/components/flight-offer-detail.d.ts.map +1 -1
  23. package/dist/components/flight-offer-detail.js +23 -35
  24. package/dist/components/flight-offer-row.d.ts.map +1 -1
  25. package/dist/components/flight-offer-row.js +19 -15
  26. package/dist/components/flight-order-confirmation.d.ts.map +1 -1
  27. package/dist/components/flight-order-confirmation.js +20 -24
  28. package/dist/components/flight-passenger-form.d.ts.map +1 -1
  29. package/dist/components/flight-passenger-form.js +18 -14
  30. package/dist/components/flight-payment-selector.d.ts.map +1 -1
  31. package/dist/components/flight-payment-selector.js +4 -8
  32. package/dist/components/flight-payment-step.d.ts.map +1 -1
  33. package/dist/components/flight-payment-step.js +14 -15
  34. package/dist/components/flight-search-form.d.ts.map +1 -1
  35. package/dist/components/flight-search-form.js +4 -2
  36. package/dist/components/flight-seat-map.d.ts.map +1 -1
  37. package/dist/components/flight-seat-map.js +24 -19
  38. package/dist/components/flight-seats-step.js +26 -24
  39. package/dist/components/flight-services-step.d.ts.map +1 -1
  40. package/dist/components/flight-services-step.js +29 -16
  41. package/dist/components/pax-cabin-popover.d.ts.map +1 -1
  42. package/dist/components/pax-cabin-popover.js +8 -11
  43. package/dist/components/popular-routes.d.ts +0 -5
  44. package/dist/components/popular-routes.d.ts.map +1 -1
  45. package/dist/components/popular-routes.js +25 -43
  46. package/dist/i18n/en.d.ts +404 -0
  47. package/dist/i18n/en.d.ts.map +1 -1
  48. package/dist/i18n/en.js +460 -0
  49. package/dist/i18n/messages.d.ts +331 -0
  50. package/dist/i18n/messages.d.ts.map +1 -1
  51. package/dist/i18n/provider.d.ts +808 -0
  52. package/dist/i18n/provider.d.ts.map +1 -1
  53. package/dist/i18n/ro.d.ts +404 -0
  54. package/dist/i18n/ro.d.ts.map +1 -1
  55. package/dist/i18n/ro.js +460 -0
  56. package/package.json +15 -15
@@ -1 +1 @@
1
- {"version":3,"file":"airport-combobox.d.ts","sourceRoot":"","sources":["../../src/components/airport-combobox.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,UAAU,EAAoB,MAAM,yBAAyB,CAAA;AAe3E,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAA;IACnE,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,WAAuB,EACvB,SAAS,EACT,QAAQ,GACT,EAAE,oBAAoB,2CAwEtB"}
1
+ {"version":3,"file":"airport-combobox.d.ts","sourceRoot":"","sources":["../../src/components/airport-combobox.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,UAAU,EAAoB,MAAM,yBAAyB,CAAA;AAgB3E,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAA;IACnE,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,WAAW,EACX,SAAS,EACT,QAAQ,GACT,EAAE,oBAAoB,2CA2EtB"}
@@ -7,6 +7,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@voyantjs/ui/components
7
7
  import { cn } from "@voyantjs/ui/lib/utils";
8
8
  import { ChevronDown, MapPin } from "lucide-react";
9
9
  import { useState } from "react";
10
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
10
11
  /**
11
12
  * Single-line typeahead airport picker. Trigger reads as one of:
12
13
  * - placeholder (no selection)
@@ -15,13 +16,14 @@ import { useState } from "react";
15
16
  *
16
17
  * Backed by `useAirportSearch` (debounced server query).
17
18
  */
18
- export function AirportCombobox({ value, onChange, placeholder = "Airport", className, disabled, }) {
19
+ export function AirportCombobox({ value, onChange, placeholder, className, disabled, }) {
20
+ const messages = useFlightsUiMessagesOrDefault().airportCombobox;
19
21
  const [open, setOpen] = useState(false);
20
22
  const [input, setInput] = useState("");
21
23
  const search = useAirportSearch(input, { enabled: open, limit: 30 });
22
24
  const airports = search.data?.data ?? [];
23
25
  const selected = value ? airports.find((a) => a.iataCode === value) : null;
24
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { type: "button", variant: "outline", disabled: disabled, className: cn("h-10 justify-between gap-2 px-3", className) }), children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [_jsx(MapPin, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), value ? (_jsxs("span", { className: "truncate text-sm", children: [_jsx("span", { className: "font-mono font-medium", children: value }), selected && (_jsx("span", { className: "ml-1.5 font-normal text-muted-foreground", children: selected.city }))] })) : (_jsx("span", { className: "truncate text-sm text-muted-foreground", children: placeholder }))] }), _jsx(ChevronDown, { className: "h-4 w-4 shrink-0 text-muted-foreground" })] }), _jsx(PopoverContent, { className: "w-[320px] p-0", align: "start", children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { value: input, onValueChange: setInput, placeholder: "Type city or IATA code\u2026" }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: search.isLoading ? "Searching…" : "No airports." }), _jsx(CommandGroup, { children: airports.map((a) => (_jsxs(CommandItem, { value: `${a.iataCode} ${a.city} ${a.name}`, onSelect: () => {
26
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { type: "button", variant: "outline", disabled: disabled, className: cn("h-10 justify-between gap-2 px-3", className) }), children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [_jsx(MapPin, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), value ? (_jsxs("span", { className: "truncate text-sm", children: [_jsx("span", { className: "font-mono font-medium", children: value }), selected && (_jsx("span", { className: "ml-1.5 font-normal text-muted-foreground", children: selected.city }))] })) : (_jsx("span", { className: "truncate text-sm text-muted-foreground", children: placeholder ?? messages.placeholder }))] }), _jsx(ChevronDown, { className: "h-4 w-4 shrink-0 text-muted-foreground" })] }), _jsx(PopoverContent, { className: "w-[320px] p-0", align: "start", children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { value: input, onValueChange: setInput, placeholder: messages.searchPlaceholder }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: search.isLoading ? messages.searching : messages.empty }), _jsx(CommandGroup, { children: airports.map((a) => (_jsxs(CommandItem, { value: `${a.iataCode} ${a.city} ${a.name}`, onSelect: () => {
25
27
  onChange(a.iataCode, a);
26
28
  setOpen(false);
27
29
  setInput("");
@@ -1 +1 @@
1
- {"version":3,"file":"flight-baggage-step.d.ts","sourceRoot":"","sources":["../../src/components/flight-baggage-step.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,gBAAgB,EAChB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,eAAe,EAChB,MAAM,kCAAkC,CAAA;AAOzC,KAAK,YAAY,GAAG,WAAW,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAA;AAG9D,MAAM,WAAW,sBAAsB;IACrC,2EAA2E;IAC3E,eAAe,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACxC,aAAa,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACvC,6CAA6C;IAC7C,aAAa,EAAE,WAAW,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;OAGG;IACH,UAAU,EAAE,eAAe,EAAE,CAAA;IAC7B,yDAAyD;IACzD,eAAe,EAAE,eAAe,CAAA;IAChC,KAAK,EAAE,YAAY,CAAA;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAA;IACtC,gFAAgF;IAChF,qBAAqB,EAAE,OAAO,CAAA;IAC9B,6BAA6B,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtD,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,eAAe,EACf,aAAa,EACb,aAAa,EACb,WAAW,EACX,UAAU,EACV,eAAe,EACf,KAAK,EACL,QAAQ,EACR,qBAAqB,EACrB,6BAA6B,EAC7B,OAAO,GACR,EAAE,sBAAsB,2CA0FxB"}
1
+ {"version":3,"file":"flight-baggage-step.d.ts","sourceRoot":"","sources":["../../src/components/flight-baggage-step.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,gBAAgB,EAChB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,eAAe,EAChB,MAAM,kCAAkC,CAAA;AASzC,KAAK,YAAY,GAAG,WAAW,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAA;AAG9D,MAAM,WAAW,sBAAsB;IACrC,2EAA2E;IAC3E,eAAe,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACxC,aAAa,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACvC,6CAA6C;IAC7C,aAAa,EAAE,WAAW,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;OAGG;IACH,UAAU,EAAE,eAAe,EAAE,CAAA;IAC7B,yDAAyD;IACzD,eAAe,EAAE,eAAe,CAAA;IAChC,KAAK,EAAE,YAAY,CAAA;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAA;IACtC,gFAAgF;IAChF,qBAAqB,EAAE,OAAO,CAAA;IAC9B,6BAA6B,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtD,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,eAAe,EACf,aAAa,EACb,aAAa,EACb,WAAW,EACX,UAAU,EACV,eAAe,EACf,KAAK,EACL,QAAQ,EACR,qBAAqB,EACrB,6BAA6B,EAC7B,OAAO,GACR,EAAE,sBAAsB,2CA0FxB"}
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { formatMessage } from "@voyantjs/i18n";
3
4
  import { Checkbox } from "@voyantjs/ui/components/checkbox";
4
5
  import { Label } from "@voyantjs/ui/components/label";
5
6
  import { cn } from "@voyantjs/ui/lib/utils";
6
7
  import { Briefcase, CheckCircle2, Luggage } from "lucide-react";
7
8
  import { useMemo } from "react";
9
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
8
10
  /**
9
11
  * Wizz-style baggage step. Tiered grid (10/20/26/32 kg with "Recommended"
10
12
  * highlight) per passenger per leg, plus a "skip checked bag" path. The
@@ -12,13 +14,14 @@ import { useMemo } from "react";
12
14
  * leg — kept on by default per LCC convention.
13
15
  */
14
16
  export function FlightBaggageStep({ outboundCatalog, returnCatalog, outboundOffer, returnOffer, passengers, passengerCounts, value, onChange, sameForBothDirections, onSameForBothDirectionsChange, loading, }) {
17
+ const messages = useFlightsUiMessagesOrDefault();
15
18
  const isRoundTrip = !!returnOffer;
16
- const paxRows = useMemo(() => buildPassengerRows(passengers, passengerCounts), [passengers, passengerCounts]);
19
+ const paxRows = useMemo(() => buildPassengerRows(passengers, passengerCounts, messages), [passengers, passengerCounts, messages]);
17
20
  if (loading) {
18
21
  return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { className: "h-8 w-64 animate-pulse rounded bg-muted/40" }), _jsx("div", { className: "h-40 animate-pulse rounded-xl bg-muted/40" }), _jsx("div", { className: "h-40 animate-pulse rounded-xl bg-muted/40" })] }));
19
22
  }
20
23
  if (!outboundCatalog) {
21
- return (_jsx("div", { className: "rounded-xl border border-dashed p-6 text-center text-muted-foreground text-sm", children: "Bags couldn't be loaded for this offer." }));
24
+ return (_jsx("div", { className: "rounded-xl border border-dashed p-6 text-center text-muted-foreground text-sm", children: messages.flightBaggageStep.unavailable }));
22
25
  }
23
26
  const setPick = (next, removeMatch) => {
24
27
  const filtered = value.filter((p) => !(p.passengerId === removeMatch.passengerId && p.sliceIndex === removeMatch.sliceIndex));
@@ -32,57 +35,65 @@ export function FlightBaggageStep({ outboundCatalog, returnCatalog, outboundOffe
32
35
  }
33
36
  onChange(updated);
34
37
  };
35
- 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: "Add checked baggage" }), _jsx("p", { className: "text-muted-foreground text-sm", children: "Carry-on under the seat is included. Pick a checked bag tier per passenger or skip this step." })] }), isRoundTrip && (_jsxs("div", { className: "flex shrink-0 items-center gap-2 text-sm", children: [_jsx(Checkbox, { id: "baggage-same-for-both", checked: sameForBothDirections, onCheckedChange: (v) => onSameForBothDirectionsChange(!!v) }), _jsx("label", { htmlFor: "baggage-same-for-both", className: "cursor-pointer", children: "Same for both directions" })] }))] }), _jsx(BaggageLegSection, { legLabel: "Outbound", catalog: outboundCatalog, offer: outboundOffer, passengers: paxRows, sliceIndex: 0, value: value, onPick: setPick }), isRoundTrip && returnCatalog && !sameForBothDirections && (_jsx(BaggageLegSection, { legLabel: "Return", catalog: returnCatalog, offer: returnOffer ?? outboundOffer, passengers: paxRows, sliceIndex: 1, value: value, onPick: setPick }))] }));
38
+ 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.flightBaggageStep.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: messages.flightBaggageStep.description })] }), isRoundTrip && (_jsxs("div", { className: "flex shrink-0 items-center gap-2 text-sm", children: [_jsx(Checkbox, { id: "baggage-same-for-both", checked: sameForBothDirections, onCheckedChange: (v) => onSameForBothDirectionsChange(!!v) }), _jsx("label", { htmlFor: "baggage-same-for-both", className: "cursor-pointer", children: messages.flightBaggageStep.sameForBothDirections })] }))] }), _jsx(BaggageLegSection, { legLabel: messages.common.legLabels.outbound, catalog: outboundCatalog, offer: outboundOffer, passengers: paxRows, sliceIndex: 0, value: value, messages: messages, onPick: setPick }), isRoundTrip && returnCatalog && !sameForBothDirections && (_jsx(BaggageLegSection, { legLabel: messages.common.legLabels.return, catalog: returnCatalog, offer: returnOffer ?? outboundOffer, passengers: paxRows, sliceIndex: 1, value: value, messages: messages, onPick: setPick }))] }));
36
39
  }
37
- function BaggageLegSection({ legLabel, catalog, offer, passengers, sliceIndex, value, onPick, }) {
40
+ function BaggageLegSection({ legLabel, catalog, offer, passengers, sliceIndex, value, onPick, messages, }) {
38
41
  const itin = offer.itineraries[0];
39
42
  const first = itin?.segments[0];
40
43
  const last = itin?.segments[itin.segments.length - 1];
41
- 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: "font-medium text-sm", children: [_jsx(Luggage, { className: "mr-1.5 inline h-3.5 w-3.5 -translate-y-px text-muted-foreground" }), legLabel, " bags"] }), first && last && (_jsxs("span", { className: "text-muted-foreground text-xs", children: [first.departure.iataCode, " \u2192 ", last.arrival.iataCode, " \u00B7 ", formatDate(first.departure.at)] }))] }), _jsx("div", { className: "flex flex-col gap-5", children: passengers.map((pax) => {
44
+ 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: "font-medium text-sm", children: [_jsx(Luggage, { className: "mr-1.5 inline h-3.5 w-3.5 -translate-y-px text-muted-foreground" }), formatMessage(messages.flightBaggageStep.bags, { leg: legLabel })] }), first && last && (_jsxs("span", { className: "text-muted-foreground text-xs", children: [first.departure.iataCode, " \u2192 ", last.arrival.iataCode, " \u00B7 ", formatDate(first.departure.at)] }))] }), _jsx("div", { className: "flex flex-col gap-5", children: passengers.map((pax) => {
42
45
  const pick = value.find((p) => p.passengerId === pax.passengerId && p.sliceIndex === sliceIndex);
43
- return (_jsx(PaxBaggageRow, { pax: pax, options: catalog.baggage, selectedOptionId: pick?.optionId ?? null, onSelect: (optionId) => onPick(optionId
46
+ return (_jsx(PaxBaggageRow, { pax: pax, options: catalog.baggage, selectedOptionId: pick?.optionId ?? null, messages: messages, onSelect: (optionId) => onPick(optionId
44
47
  ? { passengerId: pax.passengerId, sliceIndex, optionId, quantity: 1 }
45
48
  : null, { passengerId: pax.passengerId, sliceIndex, optionId: "" }) }, pax.passengerId));
46
49
  }) })] }));
47
50
  }
48
- function PaxBaggageRow({ pax, options, selectedOptionId, onSelect, }) {
49
- return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(Label, { className: "font-medium text-sm", children: pax.label }), selectedOptionId == null && (_jsx("span", { className: "text-[11px] text-muted-foreground", children: "No checked bag" }))] }), _jsx("div", { className: "grid grid-cols-2 gap-2 md:grid-cols-4", children: options.map((opt) => {
51
+ function PaxBaggageRow({ pax, options, selectedOptionId, onSelect, messages, }) {
52
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(Label, { className: "font-medium text-sm", children: pax.label }), selectedOptionId == null && (_jsx("span", { className: "text-[11px] text-muted-foreground", children: messages.flightBaggageStep.noCheckedBag }))] }), _jsx("div", { className: "grid grid-cols-2 gap-2 md:grid-cols-4", children: options.map((opt) => {
50
53
  const isSelected = selectedOptionId === opt.id;
51
54
  return (_jsxs("button", { type: "button", onClick: () => onSelect(isSelected ? null : opt.id), className: cn("relative flex flex-col items-center justify-center gap-1.5 rounded-lg border bg-card p-3 text-center transition-colors", isSelected
52
55
  ? "border-primary ring-2 ring-primary/20"
53
- : "hover:border-primary/40 hover:bg-accent/30", opt.recommended && !isSelected && "border-primary/40"), children: [opt.recommended && (_jsx("span", { className: "-translate-y-1/2 absolute top-0 left-1/2 -translate-x-1/2 rounded-full bg-primary px-2 py-0.5 font-medium text-[9px] text-primary-foreground uppercase tracking-wider", children: "Recommended" })), isSelected && (_jsx(CheckCircle2, { className: "absolute top-2 right-2 h-3.5 w-3.5 text-primary" })), _jsx(Briefcase, { className: "h-7 w-7 text-muted-foreground" }), _jsx("span", { className: "font-semibold text-base", children: opt.weightKg ? `${opt.weightKg} kg` : opt.label }), _jsx("span", { className: "font-mono text-[11px] text-muted-foreground", children: opt.price.amount === "0.00"
54
- ? "Included"
56
+ : "hover:border-primary/40 hover:bg-accent/30", opt.recommended && !isSelected && "border-primary/40"), children: [opt.recommended && (_jsx("span", { className: "-translate-y-1/2 absolute top-0 left-1/2 -translate-x-1/2 rounded-full bg-primary px-2 py-0.5 font-medium text-[9px] text-primary-foreground uppercase tracking-wider", children: messages.common.recommended })), isSelected && (_jsx(CheckCircle2, { className: "absolute top-2 right-2 h-3.5 w-3.5 text-primary" })), _jsx(Briefcase, { className: "h-7 w-7 text-muted-foreground" }), _jsx("span", { className: "font-semibold text-base", children: opt.weightKg ? `${opt.weightKg} kg` : opt.label }), _jsx("span", { className: "font-mono text-[11px] text-muted-foreground", children: opt.price.amount === "0.00"
57
+ ? messages.common.included
55
58
  : `+${formatMoney(opt.price.amount, opt.price.currency)}` })] }, opt.id));
56
59
  }) })] }));
57
60
  }
58
61
  // ── Helpers ──────────────────────────────────────────────────────────────────
59
- function buildPassengerRows(passengers, counts) {
62
+ function buildPassengerRows(passengers, counts, messages) {
60
63
  if (passengers.length > 0) {
61
64
  return passengers.map((p) => ({
62
65
  passengerId: p.passengerId,
63
- label: nameOrFallback(p),
66
+ label: nameOrFallback(p, messages),
64
67
  }));
65
68
  }
66
69
  // Synthesize from counts when passengers haven't been filled yet.
67
70
  const out = [];
68
71
  for (let i = 1; i <= counts.adults; i++) {
69
- out.push({ passengerId: `pax_adult_${i}`, label: `Adult ${i}` });
72
+ out.push({
73
+ passengerId: `pax_adult_${i}`,
74
+ label: `${messages.common.passengerTypeLabels.adult} ${i}`,
75
+ });
70
76
  }
71
77
  for (let i = 1; i <= (counts.children ?? 0); i++) {
72
- out.push({ passengerId: `pax_child_${i}`, label: `Child ${i}` });
78
+ out.push({
79
+ passengerId: `pax_child_${i}`,
80
+ label: `${messages.common.passengerTypeLabels.child} ${i}`,
81
+ });
73
82
  }
74
83
  for (let i = 1; i <= (counts.infants ?? 0); i++) {
75
- out.push({ passengerId: `pax_infant_${i}`, label: `Infant ${i}` });
84
+ out.push({
85
+ passengerId: `pax_infant_${i}`,
86
+ label: `${messages.common.passengerTypeLabels.infant} ${i}`,
87
+ });
76
88
  }
77
89
  return out;
78
90
  }
79
- function nameOrFallback(p) {
91
+ function nameOrFallback(p, messages) {
80
92
  const full = `${p.firstName} ${p.lastName}`.trim();
81
93
  if (full)
82
94
  return full;
83
95
  const idx = p.passengerId.match(/_(\d+)$/)?.[1] ?? "1";
84
- const cap = p.type[0]?.toUpperCase() + p.type.slice(1);
85
- return `${cap} ${idx}`;
96
+ return `${messages.common.passengerTypeLabels[p.type]} ${idx}`;
86
97
  }
87
98
  function formatMoney(amount, currency) {
88
99
  const n = Number(amount);
@@ -1 +1 @@
1
- {"version":3,"file":"flight-billing-step.d.ts","sourceRoot":"","sources":["../../src/components/flight-billing-step.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAEhD,mEAAmE;AACnE,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,SAAS,CAAA;AAEhD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,WAAW,CAAA;IACjB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,gEAAgE;AAChE,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,YAAY,CAAA;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAA;IACtC;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,wBAAwB,EAAE,CAAA;IAC/C;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,KAAK,SAAS,CAAA;IACnF;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,KAAK,SAAS,CAAA;CACjF;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,EACR,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,GAChB,EAAE,sBAAsB,2CAiExB;AA8ND,wBAAgB,iBAAiB,IAAI,YAAY,CAUhD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI,CAc9D"}
1
+ {"version":3,"file":"flight-billing-step.d.ts","sourceRoot":"","sources":["../../src/components/flight-billing-step.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAIhD,mEAAmE;AACnE,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,SAAS,CAAA;AAEhD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,WAAW,CAAA;IACjB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,gEAAgE;AAChE,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,YAAY,CAAA;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAA;IACtC;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,wBAAwB,EAAE,CAAA;IAC/C;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,KAAK,SAAS,CAAA;IACnF;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,KAAK,SAAS,CAAA;CACjF;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,EACR,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,GAChB,EAAE,sBAAsB,2CAiExB;AAkPD,wBAAgB,iBAAiB,IAAI,YAAY,CAUhD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI,CAe9D"}
@@ -12,6 +12,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@voyantjs/ui/component
12
12
  import { cn } from "@voyantjs/ui/lib/utils";
13
13
  import { Building2, ChevronDown, User, Users } from "lucide-react";
14
14
  import { useState } from "react";
15
+ import { flightsUiEn } from "../i18n/en.js";
16
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
15
17
  /**
16
18
  * Two-tab billing step with Privat (personal) + Companie (business / VAT)
17
19
  * shapes. Address fields are structured (line1/city/postal/country) so the
@@ -20,32 +22,35 @@ import { useState } from "react";
20
22
  * stays decoupled from the CRM data layer.
21
23
  */
22
24
  export function FlightBillingStep({ value, onChange, eligiblePassengers, renderPersonPicker, renderOrgPicker, }) {
25
+ const messages = useFlightsUiMessagesOrDefault();
23
26
  const apply = (prefill) => onChange({ ...value, ...prefill });
24
27
  const set = (patch) => onChange({ ...value, ...patch });
25
28
  const hasPassengerOptions = (eligiblePassengers?.length ?? 0) > 0;
26
- return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { children: [_jsx("h2", { className: "font-semibold text-base", children: "Billing" }), _jsx("p", { className: "text-muted-foreground text-sm", children: "The receipt and tax documents will be issued in this name." })] }), _jsxs(Tabs, { value: value.mode, onValueChange: (v) => set({ mode: v ?? "personal" }), children: [_jsxs(TabsList, { children: [_jsxs(TabsTrigger, { value: "personal", children: [_jsx(User, { className: "mr-1.5 h-3.5 w-3.5" }), "Personal"] }), _jsxs(TabsTrigger, { value: "company", children: [_jsx(Building2, { className: "mr-1.5 h-3.5 w-3.5" }), "Company"] })] }), _jsxs(TabsContent, { value: "personal", className: "mt-5 flex flex-col gap-4", children: [(hasPassengerOptions || renderPersonPicker) && (_jsxs("div", { className: "flex flex-wrap justify-end gap-2", children: [hasPassengerOptions && (_jsx(PassengerPickerTrigger, { passengers: eligiblePassengers ?? [], onPick: (p) => apply({
29
+ return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { children: [_jsx("h2", { className: "font-semibold text-base", children: messages.flightBillingStep.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: messages.flightBillingStep.description })] }), _jsxs(Tabs, { value: value.mode, onValueChange: (v) => set({ mode: v ?? "personal" }), children: [_jsxs(TabsList, { children: [_jsxs(TabsTrigger, { value: "personal", children: [_jsx(User, { className: "mr-1.5 h-3.5 w-3.5" }), messages.flightBillingStep.tabs.personal] }), _jsxs(TabsTrigger, { value: "company", children: [_jsx(Building2, { className: "mr-1.5 h-3.5 w-3.5" }), messages.flightBillingStep.tabs.company] })] }), _jsxs(TabsContent, { value: "personal", className: "mt-5 flex flex-col gap-4", children: [(hasPassengerOptions || renderPersonPicker) && (_jsxs("div", { className: "flex flex-wrap justify-end gap-2", children: [hasPassengerOptions && (_jsx(PassengerPickerTrigger, { passengers: eligiblePassengers ?? [], messages: messages, onPick: (p) => apply({
27
30
  mode: "personal",
28
31
  firstName: p.firstName,
29
32
  ...(p.middleName ? { middleName: p.middleName } : {}),
30
33
  lastName: p.lastName,
31
- }) })), renderPersonPicker?.(apply)] })), _jsx(NameRow, { value: value, onChange: set }), _jsx(ContactRow, { value: value, onChange: set }), _jsx(AddressBlock, { value: value, onChange: set }), _jsx(SaveDefaultRow, { value: value, onChange: set })] }), _jsxs(TabsContent, { value: "company", className: "mt-5 flex flex-col gap-4", children: [renderOrgPicker && _jsx("div", { className: "flex justify-end", children: renderOrgPicker(apply) }), _jsx(CompanyRow, { value: value, onChange: set }), _jsx(ContactRow, { value: value, onChange: set, workPhone: true }), _jsx(AddressBlock, { value: value, onChange: set }), _jsx(SaveDefaultRow, { value: value, onChange: set })] })] })] }));
34
+ }) })), renderPersonPicker?.(apply)] })), _jsx(NameRow, { value: value, onChange: set, messages: messages }), _jsx(ContactRow, { value: value, onChange: set, messages: messages }), _jsx(AddressBlock, { value: value, onChange: set, messages: messages }), _jsx(SaveDefaultRow, { value: value, onChange: set, messages: messages })] }), _jsxs(TabsContent, { value: "company", className: "mt-5 flex flex-col gap-4", children: [renderOrgPicker && _jsx("div", { className: "flex justify-end", children: renderOrgPicker(apply) }), _jsx(CompanyRow, { value: value, onChange: set, messages: messages }), _jsx(ContactRow, { value: value, onChange: set, workPhone: true, messages: messages }), _jsx(AddressBlock, { value: value, onChange: set, messages: messages }), _jsx(SaveDefaultRow, { value: value, onChange: set, messages: messages })] })] })] }));
32
35
  }
33
36
  // ─────────────────────────────────────────────────────────────────────────────
34
- function NameRow({ value, onChange, }) {
35
- return (_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsx(Field, { label: "First name", required: true, children: _jsx(Input, { value: value.firstName, onChange: (e) => onChange({ firstName: e.target.value }) }) }), _jsx(Field, { label: "Last name", required: true, children: _jsx(Input, { value: value.lastName, onChange: (e) => onChange({ lastName: e.target.value }) }) })] }));
37
+ function NameRow({ value, onChange, messages, }) {
38
+ return (_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsx(Field, { label: messages.flightBillingStep.fields.firstName, required: true, children: _jsx(Input, { value: value.firstName, onChange: (e) => onChange({ firstName: e.target.value }) }) }), _jsx(Field, { label: messages.flightBillingStep.fields.lastName, required: true, children: _jsx(Input, { value: value.lastName, onChange: (e) => onChange({ lastName: e.target.value }) }) })] }));
36
39
  }
37
- function CompanyRow({ value, onChange, }) {
38
- return (_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsx(Field, { label: "Company name", required: true, children: _jsx(Input, { value: value.companyName ?? "", onChange: (e) => onChange({ companyName: e.target.value }) }) }), _jsx(Field, { label: "Tax id / VAT number", required: true, children: _jsx(Input, { value: value.vatNumber ?? "", onChange: (e) => onChange({ vatNumber: e.target.value }), placeholder: "e.g. RO43917962" }) })] }));
40
+ function CompanyRow({ value, onChange, messages, }) {
41
+ return (_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsx(Field, { label: messages.flightBillingStep.fields.companyName, required: true, children: _jsx(Input, { value: value.companyName ?? "", onChange: (e) => onChange({ companyName: e.target.value }) }) }), _jsx(Field, { label: messages.flightBillingStep.fields.vatNumber, required: true, children: _jsx(Input, { value: value.vatNumber ?? "", onChange: (e) => onChange({ vatNumber: e.target.value }), placeholder: messages.flightBillingStep.placeholders.vatNumber }) })] }));
39
42
  }
40
- function ContactRow({ value, onChange, workPhone, }) {
41
- return (_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsx(Field, { label: "Email", required: true, children: _jsx(Input, { type: "email", value: value.email, onChange: (e) => onChange({ email: e.target.value }) }) }), _jsx(Field, { label: workPhone ? "Work phone" : "Phone", children: _jsx(PhoneInput, { value: (value.phone ?? ""), onChange: (v) => onChange({ phone: v ? String(v) : undefined }), defaultCountry: "RO", international: true }) })] }));
43
+ function ContactRow({ value, onChange, workPhone, messages, }) {
44
+ return (_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsx(Field, { label: messages.flightBillingStep.fields.email, required: true, children: _jsx(Input, { type: "email", value: value.email, onChange: (e) => onChange({ email: e.target.value }) }) }), _jsx(Field, { label: workPhone
45
+ ? messages.flightBillingStep.fields.workPhone
46
+ : messages.flightBillingStep.fields.phone, children: _jsx(PhoneInput, { value: (value.phone ?? ""), onChange: (v) => onChange({ phone: v ? String(v) : undefined }), defaultCountry: "RO", international: true }) })] }));
42
47
  }
43
- function AddressBlock({ value, onChange, }) {
44
- return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(Field, { label: "Street address", required: true, children: _jsx(Input, { value: value.line1, onChange: (e) => onChange({ line1: e.target.value }), placeholder: "Street + number" }) }), _jsx(Field, { label: "Address line 2", children: _jsx(Input, { value: value.line2 ?? "", onChange: (e) => onChange({ line2: e.target.value }), placeholder: "Apartment, suite, etc." }) }), _jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-3", children: [_jsx(Field, { label: "City", required: true, children: _jsx(Input, { value: value.city, onChange: (e) => onChange({ city: e.target.value }) }) }), _jsx(Field, { label: "Postal code", children: _jsx(Input, { value: value.postalCode ?? "", onChange: (e) => onChange({ postalCode: e.target.value }) }) }), _jsx(Field, { label: "Country", required: true, children: _jsx(CountryCombobox, { value: value.countryCode || null, onChange: (code) => onChange({ countryCode: code ?? "" }) }) })] })] }));
48
+ function AddressBlock({ value, onChange, messages, }) {
49
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(Field, { label: messages.flightBillingStep.fields.streetAddress, required: true, children: _jsx(Input, { value: value.line1, onChange: (e) => onChange({ line1: e.target.value }), placeholder: messages.flightBillingStep.placeholders.streetAddress }) }), _jsx(Field, { label: messages.flightBillingStep.fields.addressLine2, children: _jsx(Input, { value: value.line2 ?? "", onChange: (e) => onChange({ line2: e.target.value }), placeholder: messages.flightBillingStep.placeholders.addressLine2 }) }), _jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-3", children: [_jsx(Field, { label: messages.flightBillingStep.fields.city, required: true, children: _jsx(Input, { value: value.city, onChange: (e) => onChange({ city: e.target.value }) }) }), _jsx(Field, { label: messages.flightBillingStep.fields.postalCode, children: _jsx(Input, { value: value.postalCode ?? "", onChange: (e) => onChange({ postalCode: e.target.value }) }) }), _jsx(Field, { label: messages.flightBillingStep.fields.country, required: true, children: _jsx(CountryCombobox, { value: value.countryCode || null, onChange: (code) => onChange({ countryCode: code ?? "" }) }) })] })] }));
45
50
  }
46
- function SaveDefaultRow({ value, onChange, }) {
51
+ function SaveDefaultRow({ value, onChange, messages, }) {
47
52
  const id = "billing-save-default";
48
- return (_jsxs("div", { className: "flex items-center gap-2 text-muted-foreground text-sm", children: [_jsx(Checkbox, { id: id, checked: !!value.saveAsDefault, onCheckedChange: (v) => onChange({ saveAsDefault: !!v }) }), _jsx("label", { htmlFor: id, className: "cursor-pointer", children: "Save these details as the default for this contact" })] }));
53
+ return (_jsxs("div", { className: "flex items-center gap-2 text-muted-foreground text-sm", children: [_jsx(Checkbox, { id: id, checked: !!value.saveAsDefault, onCheckedChange: (v) => onChange({ saveAsDefault: !!v }) }), _jsx("label", { htmlFor: id, className: "cursor-pointer", children: messages.flightBillingStep.saveDefault })] }));
49
54
  }
50
55
  function Field({ label, required, children, }) {
51
56
  return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: cn("text-[11px] uppercase tracking-wider text-muted-foreground"), children: [label, required && _jsx("span", { className: "ml-0.5 text-destructive", children: "*" })] }), children] }));
@@ -57,16 +62,16 @@ function Field({ label, required, children, }) {
57
62
  * upstream — the operator can click one to copy their first/middle/last name
58
63
  * into the billing recipient. Self-contained: doesn't depend on CRM data.
59
64
  */
60
- function PassengerPickerTrigger({ passengers, onPick, }) {
65
+ function PassengerPickerTrigger({ passengers, messages, onPick, }) {
61
66
  const [open, setOpen] = useState(false);
62
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "gap-2" }), children: [_jsx(Users, { className: "h-3.5 w-3.5" }), "Pick from passengers", _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsx(PopoverContent, { className: "w-[320px] p-0", align: "end", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: "Search passengers\u2026" }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "No matching passengers." }), _jsx(CommandGroup, { children: passengers.map((p) => {
67
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "gap-2" }), children: [_jsx(Users, { className: "h-3.5 w-3.5" }), messages.flightBillingStep.pickFromPassengers, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsx(PopoverContent, { className: "w-[320px] p-0", align: "end", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: messages.flightBillingStep.placeholders.searchPassengers }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: messages.flightBillingStep.noMatchingPassengers }), _jsx(CommandGroup, { children: passengers.map((p) => {
63
68
  const fullName = [p.firstName, p.middleName, p.lastName]
64
69
  .filter((s) => s?.trim())
65
70
  .join(" ");
66
71
  return (_jsx(CommandItem, { value: fullName, onSelect: () => {
67
72
  onPick(p);
68
73
  setOpen(false);
69
- }, children: _jsx("span", { className: "truncate font-medium text-sm", children: fullName || "—" }) }, p.id));
74
+ }, children: _jsx("span", { className: "truncate font-medium text-sm", children: fullName || messages.common.noValue }) }, p.id));
70
75
  }) })] })] }) })] }));
71
76
  }
72
77
  export function emptyBillingValue() {
@@ -85,27 +90,28 @@ export function emptyBillingValue() {
85
90
  * when valid. Drives the journey's Continue gate.
86
91
  */
87
92
  export function validateBilling(v) {
93
+ const messages = flightsUiEn.flightBillingStep.validation;
88
94
  if (!v.email.trim())
89
- return "Email is required";
95
+ return messages.emailRequired;
90
96
  if (!/^\S+@\S+\.\S+$/.test(v.email.trim()))
91
- return "Email looks invalid";
97
+ return messages.emailInvalid;
92
98
  if (!v.line1.trim())
93
- return "Street address is required";
99
+ return messages.streetAddressRequired;
94
100
  if (!v.city.trim())
95
- return "City is required";
101
+ return messages.cityRequired;
96
102
  if (!v.countryCode.trim())
97
- return "Country is required";
103
+ return messages.countryRequired;
98
104
  if (v.mode === "personal") {
99
105
  if (!v.firstName.trim())
100
- return "First name is required";
106
+ return messages.firstNameRequired;
101
107
  if (!v.lastName.trim())
102
- return "Last name is required";
108
+ return messages.lastNameRequired;
103
109
  }
104
110
  else {
105
111
  if (!v.companyName?.trim())
106
- return "Company name is required";
112
+ return messages.companyNameRequired;
107
113
  if (!v.vatNumber?.trim())
108
- return "VAT / tax number is required";
114
+ return messages.vatNumberRequired;
109
115
  }
110
116
  return null;
111
117
  }
@@ -1 +1 @@
1
- {"version":3,"file":"flight-booking-journey.d.ts","sourceRoot":"","sources":["../../src/components/flight-booking-journey.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,WAAW,EACX,WAAW,EAEX,eAAe,EAEhB,MAAM,kCAAkC,CAAA;AAYzC,OAAO,EAEL,KAAK,wBAAwB,EAE9B,MAAM,4BAA4B,CAAA;AAYnC,MAAM,WAAW,yBAAyB;IACxC,mEAAmE;IACnE,KAAK,EAAE,WAAW,CAAA;IAClB,gDAAgD;IAChD,UAAU,EAAE,eAAe,CAAA;IAC3B,2DAA2D;IAC3D,MAAM,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CAAA;IAC1E,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACvC,oCAAoC;IACpC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,wEAAwE;IACxE,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;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,wBAAwB,CAAC,cAAc,CAAC,CAAA;CACjE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,UAAU,EACV,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,WAAW,EACX,qBAAqB,GACtB,EAAE,yBAAyB,kDAmH3B"}
1
+ {"version":3,"file":"flight-booking-journey.d.ts","sourceRoot":"","sources":["../../src/components/flight-booking-journey.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,WAAW,EACX,WAAW,EAEX,eAAe,EAEhB,MAAM,kCAAkC,CAAA;AAYzC,OAAO,EAEL,KAAK,wBAAwB,EAE9B,MAAM,4BAA4B,CAAA;AAYnC,MAAM,WAAW,yBAAyB;IACxC,mEAAmE;IACnE,KAAK,EAAE,WAAW,CAAA;IAClB,gDAAgD;IAChD,UAAU,EAAE,eAAe,CAAA;IAC3B,2DAA2D;IAC3D,MAAM,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CAAA;IAC1E,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACvC,oCAAoC;IACpC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,wEAAwE;IACxE,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;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,wBAAwB,CAAC,cAAc,CAAC,CAAA;CACjE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,UAAU,EACV,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,WAAW,EACX,qBAAqB,GACtB,EAAE,yBAAyB,kDA2H3B"}
@@ -4,15 +4,16 @@ import { Button } from "@voyantjs/ui/components/button";
4
4
  import { cn } from "@voyantjs/ui/lib/utils";
5
5
  import { Check, ChevronLeft, ChevronRight } from "lucide-react";
6
6
  import { useState } from "react";
7
+ import { useFlightsUiI18nOrDefault } from "../i18n/index.js";
7
8
  import { FlightContactForm, validateContact, } from "./flight-contact-form.js";
8
9
  import { FlightOfferDetail } from "./flight-offer-detail.js";
9
10
  import { FlightPassengerForm, validatePassengers, } from "./flight-passenger-form.js";
10
11
  import { FlightPaymentSelector } from "./flight-payment-selector.js";
11
12
  const STEPS = [
12
- { id: "review", label: "Review offer" },
13
- { id: "passengers", label: "Passengers" },
14
- { id: "contact", label: "Contact & payment" },
15
- { id: "confirm", label: "Confirm" },
13
+ { id: "review" },
14
+ { id: "passengers" },
15
+ { id: "contact" },
16
+ { id: "confirm" },
16
17
  ];
17
18
  /**
18
19
  * Multi-step booking journey: Review → Passengers → Contact + Payment →
@@ -21,6 +22,8 @@ const STEPS = [
21
22
  * can't advance with incomplete data.
22
23
  */
23
24
  export function FlightBookingJourney({ offer, passengers, onBook, onBooked, onCancel, carrierName, airportName, renderPassengerPicker, }) {
25
+ const i18n = useFlightsUiI18nOrDefault();
26
+ const messages = i18n.messages.flightBookingJourney;
24
27
  const [stepIdx, setStepIdx] = useState(0);
25
28
  const [paxList, setPaxList] = useState([]);
26
29
  const [contact, setContact] = useState({});
@@ -70,45 +73,31 @@ export function FlightBookingJourney({ offer, passengers, onBook, onBooked, onCa
70
73
  setSubmitting(false);
71
74
  }
72
75
  };
73
- return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(Stepper, { currentIdx: stepIdx }), error && (_jsx("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive", children: error })), _jsxs("div", { className: "flex flex-col gap-4", children: [step.id === "review" && (_jsxs("div", { className: "rounded-xl border bg-card p-5 shadow-sm", children: [_jsx("h2", { className: "mb-4 text-base font-semibold", children: "Review your selected flight" }), _jsx(FlightOfferDetail, { offer: offer, carrierName: carrierName, airportName: airportName })] })), step.id === "passengers" && (_jsx(FlightPassengerForm, { counts: passengers, value: paxList, onChange: setPaxList, renderPicker: renderPassengerPicker })), step.id === "contact" && (_jsxs(_Fragment, { children: [_jsx(FlightContactForm, { value: contact, onChange: setContact }), _jsx(FlightPaymentSelector, { value: payment, onChange: setPayment })] })), step.id === "confirm" && (_jsx(ConfirmSummary, { offer: offer, passengers: paxList, contact: contact, payment: payment }))] }), _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 ? "Back to results" : "Back"] }), step.id === "confirm" ? (_jsx(Button, { onClick: submit, disabled: submitting, children: submitting ? "Booking…" : "Confirm booking" })) : (_jsxs(Button, { onClick: goNext, disabled: !canContinue, children: ["Continue", _jsx(ChevronRight, { className: "ml-1 h-4 w-4" })] }))] })] }));
76
+ return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(Stepper, { currentIdx: stepIdx, messages: messages.steps }), error && (_jsx("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive", children: error })), _jsxs("div", { className: "flex flex-col gap-4", children: [step.id === "review" && (_jsxs("div", { className: "rounded-xl border bg-card p-5 shadow-sm", children: [_jsx("h2", { className: "mb-4 text-base font-semibold", children: messages.reviewTitle }), _jsx(FlightOfferDetail, { offer: offer, carrierName: carrierName, airportName: airportName })] })), step.id === "passengers" && (_jsx(FlightPassengerForm, { counts: passengers, value: paxList, onChange: setPaxList, renderPicker: renderPassengerPicker })), step.id === "contact" && (_jsxs(_Fragment, { children: [_jsx(FlightContactForm, { value: contact, onChange: setContact }), _jsx(FlightPaymentSelector, { value: payment, onChange: setPayment })] })), step.id === "confirm" && (_jsx(ConfirmSummary, { offer: offer, passengers: paxList, contact: contact, payment: payment, i18n: i18n }))] }), _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 ? messages.backToResults : messages.back] }), step.id === "confirm" ? (_jsx(Button, { onClick: submit, disabled: submitting, children: submitting ? messages.booking : messages.confirmBooking })) : (_jsxs(Button, { onClick: goNext, disabled: !canContinue, children: [messages.continue, _jsx(ChevronRight, { className: "ml-1 h-4 w-4" })] }))] })] }));
74
77
  }
75
78
  // ─────────────────────────────────────────────────────────────────────────────
76
79
  // Stepper indicator
77
80
  // ─────────────────────────────────────────────────────────────────────────────
78
- function Stepper({ currentIdx }) {
81
+ function Stepper({ currentIdx, messages, }) {
79
82
  return (_jsx("ol", { className: "flex items-center gap-2", children: STEPS.map((s, i) => {
80
83
  const isActive = i === currentIdx;
81
84
  const isComplete = i < currentIdx;
82
- return (_jsxs("li", { className: "flex flex-1 items-center gap-2", children: [_jsx("div", { className: cn("flex h-7 w-7 shrink-0 items-center justify-center rounded-full border text-xs font-medium tabular-nums", isComplete && "border-primary bg-primary text-primary-foreground", isActive && !isComplete && "border-primary text-primary", !isActive && !isComplete && "border-border text-muted-foreground"), children: isComplete ? _jsx(Check, { className: "h-3.5 w-3.5" }) : i + 1 }), _jsx("span", { className: cn("truncate text-sm", isActive ? "font-medium text-foreground" : "text-muted-foreground"), children: s.label }), i < STEPS.length - 1 && _jsx("div", { className: "h-px flex-1 bg-border" })] }, s.id));
85
+ return (_jsxs("li", { className: "flex flex-1 items-center gap-2", children: [_jsx("div", { className: cn("flex h-7 w-7 shrink-0 items-center justify-center rounded-full border text-xs font-medium tabular-nums", isComplete && "border-primary bg-primary text-primary-foreground", isActive && !isComplete && "border-primary text-primary", !isActive && !isComplete && "border-border text-muted-foreground"), children: isComplete ? _jsx(Check, { className: "h-3.5 w-3.5" }) : i + 1 }), _jsx("span", { className: cn("truncate text-sm", isActive ? "font-medium text-foreground" : "text-muted-foreground"), children: messages[s.id] }), i < STEPS.length - 1 && _jsx("div", { className: "h-px flex-1 bg-border" })] }, s.id));
83
86
  }) }));
84
87
  }
85
88
  // ─────────────────────────────────────────────────────────────────────────────
86
89
  // Final confirmation summary
87
90
  // ─────────────────────────────────────────────────────────────────────────────
88
- function ConfirmSummary({ offer, passengers, contact, payment, }) {
89
- return (_jsxs("div", { className: "rounded-xl border bg-card p-5 shadow-sm", children: [_jsx("h2", { className: "mb-4 text-base font-semibold", children: "Confirm booking" }), _jsx(Row, { label: "Total", children: _jsx("span", { className: "font-semibold tabular-nums", children: formatMoney(offer.totalPrice.amount, offer.totalPrice.currency) }) }), _jsx(Row, { label: "Passengers", children: passengers.length }), _jsx(Row, { label: "Contact", children: contact.email ?? "—" }), _jsx(Row, { label: "Payment", children: _jsx("span", { className: "capitalize", children: payment.type.replace("_", " ") }) }), offer.expiresAt && (_jsx(Row, { label: "Offer expires", children: _jsx("time", { dateTime: offer.expiresAt, children: formatDateTime(offer.expiresAt) }) })), _jsx("p", { className: "mt-4 text-xs text-muted-foreground", children: "Submitting will hold seats with the connector and (depending on the chosen payment intent) either issue tickets immediately or open a ticketing window. Once confirmed, the booking appears under the order id below." })] }));
91
+ function ConfirmSummary({ offer, passengers, contact, payment, i18n, }) {
92
+ const messages = i18n.messages.flightBookingJourney;
93
+ return (_jsxs("div", { className: "rounded-xl border bg-card p-5 shadow-sm", children: [_jsx("h2", { className: "mb-4 text-base font-semibold", children: messages.confirmBooking }), _jsx(Row, { label: messages.rows.total, children: _jsx("span", { className: "font-semibold tabular-nums", children: formatMoney(offer.totalPrice.amount, offer.totalPrice.currency, i18n) }) }), _jsx(Row, { label: messages.rows.passengers, children: passengers.length }), _jsx(Row, { label: messages.rows.contact, children: contact.email ?? i18n.messages.common.noValue }), _jsx(Row, { label: messages.rows.payment, children: _jsx("span", { className: "capitalize", children: payment.type.replace("_", " ") }) }), offer.expiresAt && (_jsx(Row, { label: messages.rows.offerExpires, children: _jsx("time", { dateTime: offer.expiresAt, children: i18n.formatDateTime(offer.expiresAt) }) })), _jsx("p", { className: "mt-4 text-xs text-muted-foreground", children: messages.confirmDescription })] }));
90
94
  }
91
95
  function Row({ label, children }) {
92
96
  return (_jsxs("div", { className: "flex items-baseline justify-between border-b py-2 text-sm last:border-b-0", children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("span", { children: children })] }));
93
97
  }
94
- function formatMoney(amount, currency) {
98
+ function formatMoney(amount, currency, i18n) {
95
99
  const n = Number(amount);
96
100
  if (!Number.isFinite(n))
97
101
  return `${amount} ${currency}`;
98
- return new Intl.NumberFormat(undefined, {
99
- style: "currency",
100
- currency,
101
- maximumFractionDigits: 0,
102
- }).format(n);
103
- }
104
- function formatDateTime(iso) {
105
- const d = new Date(iso);
106
- if (Number.isNaN(d.getTime()))
107
- return iso;
108
- return new Intl.DateTimeFormat(undefined, {
109
- day: "numeric",
110
- month: "short",
111
- hour: "2-digit",
112
- minute: "2-digit",
113
- }).format(d);
102
+ return i18n.formatCurrency(n, currency, { maximumFractionDigits: 0 });
114
103
  }
@@ -1 +1 @@
1
- {"version":3,"file":"flight-booking-ledger.d.ts","sourceRoot":"","sources":["../../src/components/flight-booking-ledger.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAA;AAO3F;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,WAAW,CAAA;IACrB,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,KAAK,CAAA;IACd,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,wBAAwB,CAAA;IACnC,UAAU,EAAE,eAAe,CAAA;IAC3B,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,4DAA4D;IAC5D,cAAc,CAAC,EAAE,cAAc,EAAE,CAAA;IACjC,YAAY,CAAC,EAAE,cAAc,EAAE,CAAA;IAC/B,oEAAoE;IACpE,GAAG,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IACnF,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;IACzB,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,CAAA;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,YAAY,GACZ,MAAM,GACN,OAAO,GACP,UAAU,GACV,WAAW,GACX,SAAS,GACT,SAAS,CAAA;AAEb;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,UAAU,EACV,WAAW,EACX,WAAW,EACX,cAAc,EACd,YAAY,EACZ,GAAG,EACH,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,SAAS,GACV,EAAE,wBAAwB,2CAuD1B"}
1
+ {"version":3,"file":"flight-booking-ledger.d.ts","sourceRoot":"","sources":["../../src/components/flight-booking-ledger.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAA;AAQ3F;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,WAAW,CAAA;IACrB,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,KAAK,CAAA;IACd,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,wBAAwB,CAAA;IACnC,UAAU,EAAE,eAAe,CAAA;IAC3B,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,4DAA4D;IAC5D,cAAc,CAAC,EAAE,cAAc,EAAE,CAAA;IACjC,YAAY,CAAC,EAAE,cAAc,EAAE,CAAA;IAC/B,oEAAoE;IACpE,GAAG,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IACnF,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;IACzB,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,CAAA;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,YAAY,GACZ,MAAM,GACN,OAAO,GACP,UAAU,GACV,WAAW,GACX,SAAS,GACT,SAAS,CAAA;AAEb;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,UAAU,EACV,WAAW,EACX,WAAW,EACX,cAAc,EACd,YAAY,EACZ,GAAG,EACH,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,SAAS,GACV,EAAE,wBAAwB,2CA+D1B"}
@@ -1,8 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { formatMessage } from "@voyantjs/i18n";
3
4
  import { Button } from "@voyantjs/ui/components/button";
4
5
  import { cn } from "@voyantjs/ui/lib/utils";
5
6
  import { Check, Pencil, Plane, Users } from "lucide-react";
7
+ import { useFlightsUiI18nOrDefault } from "../i18n/index.js";
6
8
  import { AirlineLogo } from "./airline-logo.js";
7
9
  /**
8
10
  * Sticky right-rail price ledger. Mirrors the running total + per-leg
@@ -12,12 +14,17 @@ import { AirlineLogo } from "./airline-logo.js";
12
14
  * they were collected.
13
15
  */
14
16
  export function FlightBookingLedger({ selection, passengers, carrierName, airportName, outboundExtras, returnExtras, cta, onEditOutbound, onEditReturn, completedSections, className, }) {
17
+ const i18n = useFlightsUiI18nOrDefault();
18
+ const messages = i18n.messages;
15
19
  const total = computeTotal(selection, outboundExtras, returnExtras);
16
20
  const paxTotal = (passengers.adults ?? 0) + (passengers.children ?? 0) + (passengers.infants ?? 0);
17
- return (_jsxs("aside", { className: cn("flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-4 shadow-sm", className), children: [_jsx(LegBlock, { label: selection.return ? "Outbound" : "Flight", offer: selection.outbound, carrierName: carrierName, airportName: airportName, extras: outboundExtras, onEdit: onEditOutbound, complete: completedSections?.has("flights") }), selection.return && (_jsx(LegBlock, { label: "Return", offer: selection.return, carrierName: carrierName, airportName: airportName, extras: returnExtras, onEdit: onEditReturn, complete: completedSections?.has("flights") })), _jsx(SectionRow, { icon: _jsx(Users, { className: "h-3.5 w-3.5" }), label: "Passengers", right: `${paxTotal} pax`, complete: completedSections?.has("passengers") }), _jsx(PlaceholderSections, { completed: completedSections }), _jsxs("div", { className: "mt-2 flex items-center justify-between border-t pt-3", children: [_jsx("span", { className: "font-medium text-sm", children: "Total" }), _jsx("span", { className: "font-semibold text-lg tabular-nums", children: formatMoney(total.amount, total.currency) })] }), cta && (_jsx(Button, { className: "w-full", onClick: cta.onClick, disabled: cta.disabled || cta.loading, children: cta.loading ? "Working…" : cta.label }))] }));
21
+ return (_jsxs("aside", { className: cn("flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-4 shadow-sm", className), children: [_jsx(LegBlock, { label: selection.return
22
+ ? messages.flightBookingLedger.outbound
23
+ : messages.flightBookingLedger.flight, offer: selection.outbound, carrierName: carrierName, airportName: airportName, extras: outboundExtras, onEdit: onEditOutbound, complete: completedSections?.has("flights"), i18n: i18n }), selection.return && (_jsx(LegBlock, { label: messages.flightBookingLedger.return, offer: selection.return, carrierName: carrierName, airportName: airportName, extras: returnExtras, onEdit: onEditReturn, complete: completedSections?.has("flights"), i18n: i18n })), _jsx(SectionRow, { icon: _jsx(Users, { className: "h-3.5 w-3.5" }), label: messages.flightBookingLedger.passengers, right: `${paxTotal} ${messages.common.pax}`, complete: completedSections?.has("passengers") }), _jsx(PlaceholderSections, { completed: completedSections, messages: messages }), _jsxs("div", { className: "mt-2 flex items-center justify-between border-t pt-3", children: [_jsx("span", { className: "font-medium text-sm", children: messages.common.total }), _jsx("span", { className: "font-semibold text-lg tabular-nums", children: formatMoney(total.amount, total.currency, i18n) })] }), cta && (_jsx(Button, { className: "w-full", onClick: cta.onClick, disabled: cta.disabled || cta.loading, children: cta.loading ? messages.flightBookingLedger.working : cta.label }))] }));
18
24
  }
19
25
  // ─────────────────────────────────────────────────────────────────────────────
20
- function LegBlock({ label, offer, carrierName, airportName, extras, onEdit, complete, }) {
26
+ function LegBlock({ label, offer, carrierName, airportName, extras, onEdit, complete, i18n, }) {
27
+ const messages = i18n.messages;
21
28
  const itin = offer.itineraries[0];
22
29
  if (!itin)
23
30
  return null;
@@ -28,12 +35,12 @@ function LegBlock({ label, offer, carrierName, airportName, extras, onEdit, comp
28
35
  return null;
29
36
  const carriers = Array.from(new Set(segs.map((s) => s.carrierCode)));
30
37
  const stops = segs.length - 1;
31
- return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [complete ? (_jsx(Check, { className: "h-3.5 w-3.5 text-emerald-600" })) : (_jsx(Plane, { className: "h-3.5 w-3.5 text-muted-foreground" })), _jsx("span", { className: "font-medium text-[11px] uppercase tracking-wider text-muted-foreground", children: label })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-semibold text-sm tabular-nums", children: formatMoney(offer.totalPrice.amount, offer.totalPrice.currency) }), onEdit && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-6 px-1.5 text-muted-foreground", onClick: onEdit, children: _jsx(Pencil, { className: "h-3 w-3" }) }))] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex shrink-0 items-center -space-x-1", children: carriers.map((code) => (_jsx(AirlineLogo, { iataCode: code, name: carrierName?.(code), size: 18 }, code))) }), _jsxs("div", { className: "flex min-w-0 flex-col leading-tight", children: [_jsxs("span", { className: "truncate font-medium text-sm", children: [airportName?.(first.departure.iataCode) ?? first.departure.iataCode, " \u2192", " ", airportName?.(last.arrival.iataCode) ?? last.arrival.iataCode] }), _jsxs("span", { className: "text-[11px] text-muted-foreground", children: [formatDate(first.departure.at), " \u00B7 ", formatTime(first.departure.at), " \u2013", " ", formatTime(last.arrival.at), " \u00B7", " ", stops === 0 ? "Nonstop" : `${stops} stop${stops > 1 ? "s" : ""}`] })] })] }), extras && extras.length > 0 && (_jsx("ul", { className: "flex flex-col gap-1 border-t pt-2", children: extras.map((x, i) => (_jsxs("li", { className: "flex items-center justify-between text-muted-foreground text-xs", children: [_jsx("span", { children: x.label }), _jsx("span", { className: "tabular-nums", children: x.amount ? formatMoney(x.amount.amount, x.amount.currency) : (x.meta ?? "") })] }, i))) }))] }));
38
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [complete ? (_jsx(Check, { className: "h-3.5 w-3.5 text-emerald-600" })) : (_jsx(Plane, { className: "h-3.5 w-3.5 text-muted-foreground" })), _jsx("span", { className: "font-medium text-[11px] uppercase tracking-wider text-muted-foreground", children: label })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-semibold text-sm tabular-nums", children: formatMoney(offer.totalPrice.amount, offer.totalPrice.currency, i18n) }), onEdit && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-6 px-1.5 text-muted-foreground", onClick: onEdit, children: _jsx(Pencil, { className: "h-3 w-3" }) }))] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex shrink-0 items-center -space-x-1", children: carriers.map((code) => (_jsx(AirlineLogo, { iataCode: code, name: carrierName?.(code), size: 18 }, code))) }), _jsxs("div", { className: "flex min-w-0 flex-col leading-tight", children: [_jsxs("span", { className: "truncate font-medium text-sm", children: [airportName?.(first.departure.iataCode) ?? first.departure.iataCode, " \u2192", " ", airportName?.(last.arrival.iataCode) ?? last.arrival.iataCode] }), _jsxs("span", { className: "text-[11px] text-muted-foreground", children: [formatDate(first.departure.at), " \u00B7 ", formatTime(first.departure.at), " \u2013", " ", formatTime(last.arrival.at), " \u00B7 ", formatStops(stops, messages)] })] })] }), extras && extras.length > 0 && (_jsx("ul", { className: "flex flex-col gap-1 border-t pt-2", children: extras.map((x, i) => (_jsxs("li", { className: "flex items-center justify-between text-muted-foreground text-xs", children: [_jsx("span", { children: x.label }), _jsx("span", { className: "tabular-nums", children: x.amount ? formatMoney(x.amount.amount, x.amount.currency, i18n) : (x.meta ?? "") })] }, i))) }))] }));
32
39
  }
33
40
  function SectionRow({ icon, label, right, complete, }) {
34
41
  return (_jsxs("div", { className: "flex items-center justify-between border-t pt-3", children: [_jsxs("span", { className: "flex items-center gap-1.5 font-medium text-[11px] uppercase tracking-wider text-muted-foreground", children: [complete ? (_jsx(Check, { className: "h-3.5 w-3.5 text-emerald-600" })) : (_jsx("span", { className: "text-muted-foreground", children: icon })), label] }), right && _jsx("span", { className: "text-muted-foreground text-xs", children: right })] }));
35
42
  }
36
- function PlaceholderSections({ completed }) {
43
+ function PlaceholderSections({ completed, messages, }) {
37
44
  // Phase 1 only renders Passengers above; later phases will replace this with
38
45
  // bags/seats/services/documents/billing rows. The shape stays consistent so
39
46
  // the ledger doesn't need to change later.
@@ -41,9 +48,9 @@ function PlaceholderSections({ completed }) {
41
48
  return null;
42
49
  const items = [];
43
50
  if (completed.has("billing"))
44
- items.push({ id: "billing", label: "Billing" });
51
+ items.push({ id: "billing", label: messages.flightBookingLedger.billing });
45
52
  if (completed.has("payment"))
46
- items.push({ id: "payment", label: "Payment" });
53
+ items.push({ id: "payment", label: messages.flightBookingLedger.payment });
47
54
  if (items.length === 0)
48
55
  return null;
49
56
  return (_jsx(_Fragment, { children: items.map((it) => (_jsx(SectionRow, { icon: _jsx("span", {}), label: it.label, complete: completed.has(it.id) }, it.id))) }));
@@ -66,15 +73,11 @@ function num(v) {
66
73
  const n = Number(v);
67
74
  return Number.isFinite(n) ? n : 0;
68
75
  }
69
- function formatMoney(amount, currency) {
76
+ function formatMoney(amount, currency, i18n) {
70
77
  const n = Number(amount);
71
78
  if (!Number.isFinite(n))
72
79
  return `${amount} ${currency}`;
73
- return new Intl.NumberFormat(undefined, {
74
- style: "currency",
75
- currency,
76
- maximumFractionDigits: 0,
77
- }).format(n);
80
+ return i18n.formatCurrency(n, currency, { maximumFractionDigits: 0 });
78
81
  }
79
82
  function formatTime(iso) {
80
83
  const d = new Date(iso);
@@ -82,6 +85,13 @@ function formatTime(iso) {
82
85
  return iso;
83
86
  return new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit" }).format(d);
84
87
  }
88
+ function formatStops(stops, messages) {
89
+ if (stops === 0)
90
+ return messages.common.stops.nonstop;
91
+ return formatMessage(stops === 1 ? messages.common.stops.oneStop : messages.common.stops.manyStops, {
92
+ count: stops,
93
+ });
94
+ }
85
95
  function formatDate(iso) {
86
96
  const d = new Date(iso);
87
97
  if (Number.isNaN(d.getTime()))
@@ -37,7 +37,7 @@ export function FlightBookingPage({ outboundOfferId, returnOfferId, passengers,
37
37
  if (cancelled)
38
38
  return null;
39
39
  if (!result.valid)
40
- return result.invalidReason ?? "This offer is no longer available.";
40
+ return result.invalidReason ?? messages.offerUnavailable;
41
41
  setter(result.offer);
42
42
  return null;
43
43
  }
@@ -65,14 +65,14 @@ export function FlightBookingPage({ outboundOfferId, returnOfferId, passengers,
65
65
  returnCatalog: returnAncillaries.data?.catalog ?? null,
66
66
  loading: outboundAncillaries.isLoading || (returnLeg != null && returnAncillaries.isLoading),
67
67
  };
68
- const seatMaps = useSeatMapFetcher({ outbound, returnLeg, enabled: pricedReady });
68
+ const seatMaps = useSeatMapFetcher({ outbound, returnLeg, enabled: pricedReady, messages });
69
69
  const savedMethodsQuery = useSavedPaymentMethods(selectedPersonId, {
70
70
  enabled: !!selectedPersonId,
71
71
  });
72
72
  const savedPaymentMethods = {
73
73
  methods: (savedMethodsQuery.data?.data ?? []).map((method) => ({
74
74
  id: method.id,
75
- label: [brandHumanLabel(method.brand), method.last4 ? `....${method.last4}` : null]
75
+ label: [brandHumanLabel(method.brand, messages), method.last4 ? `....${method.last4}` : null]
76
76
  .filter(Boolean)
77
77
  .join(" "),
78
78
  provider: null,
@@ -103,18 +103,18 @@ export function FlightBookingPage({ outboundOfferId, returnOfferId, passengers,
103
103
  const defaultPassengerPicker = (_slot, onPicked) => (_jsx(PassengerContactPicker, { onPick: onPicked, onAddContact: onAddPassengerContact, onPersonSelected: setSelectedPersonId }));
104
104
  return (_jsxs("div", { className: cn("mx-auto flex w-full max-w-screen-2xl flex-col gap-6 px-6 py-6 lg:px-8", className), children: [_jsxs("header", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "font-semibold text-2xl", children: messages.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: selection.return ? messages.descriptionTrip : messages.descriptionOffer })] }), _jsxs(Button, { variant: "ghost", onClick: onBackToSearch, children: [_jsx(ChevronLeft, { className: "mr-1 h-4 w-4" }), messages.backToResults] })] }), _jsx(FlightBookingShell, { selection: selection, passengers: passengers, carrierName: carrierName, airportName: airportName, ancillaries: ancillaries, seatMaps: seatMaps, savedPaymentMethods: savedPaymentMethods, paymentCapabilities: paymentCapabilities, documentsRequired: documentsRequired, renderPassengerPicker: renderPassengerPicker ?? defaultPassengerPicker, renderBillingPersonPicker: (apply) => renderBillingPersonPicker ? (renderBillingPersonPicker(apply, { onPersonSelected: setSelectedPersonId })) : (_jsx(BillingPersonPicker, { apply: apply, onPersonSelected: setSelectedPersonId })), renderBillingOrgPicker: (apply) => renderBillingOrgPicker ? (renderBillingOrgPicker(apply)) : (_jsx(BillingOrgPicker, { apply: apply })), onSaveBillingDefaults: onSaveBillingDefaults, onCancel: onBackToSearch, onEditOutbound: onEditOutbound ?? onBackToSearch, onEditReturn: onEditReturn ?? onBackToSearch, onBook: onBook, onBooked: onBooked })] }));
105
105
  }
106
- function brandHumanLabel(brand) {
106
+ function brandHumanLabel(brand, messages) {
107
107
  switch (brand) {
108
108
  case "visa":
109
- return "Visa";
109
+ return messages.paymentBrandLabels.visa;
110
110
  case "mastercard":
111
- return "Mastercard";
111
+ return messages.paymentBrandLabels.mastercard;
112
112
  case "amex":
113
- return "Amex";
113
+ return messages.paymentBrandLabels.amex;
114
114
  case "revolut":
115
- return "Revolut Pay";
115
+ return messages.paymentBrandLabels.revolut;
116
116
  case "bank_transfer":
117
- return "Bank transfer";
117
+ return messages.paymentBrandLabels.bank_transfer;
118
118
  default:
119
119
  return brand;
120
120
  }
@@ -133,7 +133,7 @@ function detectInternational(offer) {
133
133
  return false;
134
134
  return first.departure.iataCode.slice(0, 1) !== last.arrival.iataCode.slice(0, 1);
135
135
  }
136
- function useSeatMapFetcher({ outbound, returnLeg, enabled, }) {
136
+ function useSeatMapFetcher({ outbound, returnLeg, enabled, messages, }) {
137
137
  const client = useVoyantFlightsContext();
138
138
  const segmentInputs = useMemo(() => {
139
139
  const list = [];
@@ -170,6 +170,6 @@ function useSeatMapFetcher({ outbound, returnLeg, enabled, }) {
170
170
  return map;
171
171
  }, [segmentInputs, results]);
172
172
  return useMemo(() => ({
173
- getSeatMap: ({ segmentId }) => slotsBySegment.get(segmentId) ?? { seatMap: null, error: "Segment not found" },
174
- }), [slotsBySegment]);
173
+ getSeatMap: ({ segmentId }) => slotsBySegment.get(segmentId) ?? { seatMap: null, error: messages.segmentNotFound },
174
+ }), [slotsBySegment, messages.segmentNotFound]);
175
175
  }