@duffel/components 2.7.20 → 3.0.0-canary

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 (325) hide show
  1. package/.circleci/config.yml +67 -0
  2. package/.eslintrc.js +34 -0
  3. package/.github/renovate.json +16 -0
  4. package/.github/workflows/autoapprove.yml +18 -0
  5. package/.github/workflows/release.yml +86 -0
  6. package/.husky/post-commit +4 -0
  7. package/.husky/pre-commit +4 -0
  8. package/.nvmrc +1 -0
  9. package/.prettierignore +2 -0
  10. package/.storybook/Storyshots.test.js +3 -0
  11. package/.storybook/__snapshots__/Storyshots.test.js.snap +48133 -0
  12. package/.storybook/main.ts +33 -0
  13. package/.storybook/preview.tsx +28 -0
  14. package/.tool-versions +1 -0
  15. package/README.md +129 -16
  16. package/__mocks__/styleMock.js +6 -0
  17. package/babel.config.js +20 -0
  18. package/commitlint.config.js +4 -0
  19. package/config/esbuild.base.config.js +14 -0
  20. package/config/esbuild.cdn.config.js +50 -0
  21. package/config/esbuild.dev.config.js +45 -0
  22. package/config/esbuild.react.config.js +42 -0
  23. package/jest.config.ts +14 -0
  24. package/package.json +123 -192
  25. package/react-dist/components/AnimatedLoaderEllipsis.d.ts +2 -0
  26. package/react-dist/components/Button.d.ts +23 -0
  27. package/react-dist/components/Card.d.ts +14 -0
  28. package/react-dist/components/Counter.d.ts +10 -0
  29. package/react-dist/components/DuffelAncillaries.d.ts +3 -0
  30. package/react-dist/components/DuffelAncillariesCustomElement.d.ts +13 -0
  31. package/react-dist/components/ErrorBoundary.d.ts +13 -0
  32. package/react-dist/components/FetchOfferErrorState.d.ts +5 -0
  33. package/react-dist/components/Icon.d.ts +44 -0
  34. package/react-dist/components/IconButton.d.ts +16 -0
  35. package/react-dist/components/Modal.d.ts +11 -0
  36. package/react-dist/components/NonIdealState.d.ts +4 -0
  37. package/react-dist/components/Stamp.d.ts +7 -0
  38. package/react-dist/components/Tabs.d.ts +16 -0
  39. package/react-dist/components/bags/BaggageSelectionCard.d.ts +11 -0
  40. package/react-dist/components/bags/BaggageSelectionController.d.ts +13 -0
  41. package/react-dist/components/bags/BaggageSelectionModal.d.ts +11 -0
  42. package/react-dist/components/bags/BaggageSelectionModalBody.d.ts +11 -0
  43. package/react-dist/components/bags/BaggageSelectionModalBodyPassenger.d.ts +13 -0
  44. package/react-dist/components/bags/BaggageSelectionModalFooter.d.ts +14 -0
  45. package/react-dist/components/bags/BaggageSelectionModalHeader.d.ts +9 -0
  46. package/react-dist/components/bags/IncludedBaggageBanner.d.ts +7 -0
  47. package/react-dist/components/cancel_for_any_reason/CfarSelectionCard.d.ts +10 -0
  48. package/react-dist/components/cancel_for_any_reason/CfarSelectionModal.d.ts +11 -0
  49. package/react-dist/components/cancel_for_any_reason/CfarSelectionModalBody.d.ts +7 -0
  50. package/react-dist/components/cancel_for_any_reason/CfarSelectionModalBodyListItem.d.ts +4 -0
  51. package/react-dist/components/cancel_for_any_reason/CfarSelectionModalFooter.d.ts +11 -0
  52. package/react-dist/components/cancel_for_any_reason/CfarSelectionModalHeader.d.ts +2 -0
  53. package/react-dist/components/seats/Amenity.d.ts +6 -0
  54. package/react-dist/components/seats/DeckSelect.d.ts +15 -0
  55. package/react-dist/components/seats/Element.d.ts +15 -0
  56. package/react-dist/components/seats/EmptyElement.d.ts +2 -0
  57. package/react-dist/components/seats/ExitElement.d.ts +6 -0
  58. package/react-dist/components/seats/Legend.d.ts +12 -0
  59. package/react-dist/components/seats/Row.d.ts +13 -0
  60. package/react-dist/components/seats/RowSection.d.ts +17 -0
  61. package/react-dist/components/seats/SeatElement.d.ts +13 -0
  62. package/react-dist/components/seats/SeatInfo.d.ts +7 -0
  63. package/react-dist/components/seats/SeatMap.d.ts +12 -0
  64. package/react-dist/components/seats/SeatMapUnavailable.d.ts +2 -0
  65. package/react-dist/components/seats/SeatSelectionCard.d.ts +13 -0
  66. package/react-dist/components/seats/SeatSelectionModal.d.ts +13 -0
  67. package/react-dist/components/seats/SeatSelectionModalBody.d.ts +4 -0
  68. package/react-dist/components/seats/SeatSelectionModalFooter.d.ts +16 -0
  69. package/react-dist/components/seats/SeatSelectionModalHeader.d.ts +10 -0
  70. package/react-dist/components/seats/SeatUnavailable.d.ts +5 -0
  71. package/react-dist/index.d.ts +8 -0
  72. package/react-dist/index.js +36 -0
  73. package/react-dist/index.js.map +7 -0
  74. package/react-dist/lib/captureErrorInSentry.d.ts +1 -0
  75. package/react-dist/lib/compileCreateOrderPayload.d.ts +14 -0
  76. package/react-dist/lib/createPriceFormatters.d.ts +12 -0
  77. package/react-dist/lib/fetchFromDuffelAPI.d.ts +1 -0
  78. package/react-dist/lib/fetchFromFixtures.d.ts +4 -0
  79. package/react-dist/lib/formatAvailableServices.d.ts +12 -0
  80. package/react-dist/lib/formatDate.d.ts +2 -0
  81. package/react-dist/lib/formatSeatMaps.d.ts +4 -0
  82. package/react-dist/lib/getBaggageServiceDescription.d.ts +2 -0
  83. package/react-dist/lib/getCabinsForSegmentAndDeck.d.ts +2 -0
  84. package/react-dist/lib/getCurrencyForSeatMaps.d.ts +10 -0
  85. package/react-dist/lib/getCurrencyForServices.d.ts +11 -0
  86. package/react-dist/lib/getFirstSeatElementMatchingCriteria.d.ts +3 -0
  87. package/react-dist/lib/getPassengerBySegmentList.d.ts +6 -0
  88. package/react-dist/lib/getPassengerInitials.d.ts +1 -0
  89. package/react-dist/lib/getPassengerMapById.d.ts +3 -0
  90. package/react-dist/lib/getPassengerName.d.ts +3 -0
  91. package/react-dist/lib/getRowNumber.d.ts +2 -0
  92. package/react-dist/lib/getSegmentList.d.ts +2 -0
  93. package/react-dist/lib/getServicePriceMapById.d.ts +3 -0
  94. package/react-dist/lib/getSymbols.d.ts +2 -0
  95. package/react-dist/lib/getTotalAmountForServices.d.ts +6 -0
  96. package/react-dist/lib/getTotalQuantity.d.ts +2 -0
  97. package/react-dist/lib/hasService.d.ts +2 -0
  98. package/react-dist/lib/hasServiceOfSameMetadataTypeAlreadyBeenSelected.d.ts +3 -0
  99. package/react-dist/lib/hasWings.d.ts +2 -0
  100. package/react-dist/lib/isBaggageService.d.ts +2 -0
  101. package/react-dist/lib/isCancelForAnyReasonService.d.ts +2 -0
  102. package/react-dist/lib/isFixtureOfferId.d.ts +2 -0
  103. package/react-dist/lib/isPayloadComplete.d.ts +2 -0
  104. package/react-dist/lib/isSeatElement.d.ts +2 -0
  105. package/react-dist/lib/logging.d.ts +53 -0
  106. package/react-dist/lib/moneyStringFormatter.d.ts +8 -0
  107. package/react-dist/lib/offerIsExpired.d.ts +2 -0
  108. package/react-dist/lib/retrieveOffer.d.ts +2 -0
  109. package/react-dist/lib/retrieveOfferFromDuffelAPI.d.ts +1 -0
  110. package/react-dist/lib/retrieveSeatMaps.d.ts +2 -0
  111. package/react-dist/lib/retrieveSeatMapsFromDuffelAPI.d.ts +1 -0
  112. package/react-dist/lib/setBodyScrollability.d.ts +1 -0
  113. package/react-dist/lib/validateProps.d.ts +7 -0
  114. package/react-dist/lib/withPlural.d.ts +1 -0
  115. package/react-dist/types/Aircraft.d.ts +14 -0
  116. package/react-dist/types/Airline.d.ts +14 -0
  117. package/react-dist/types/Airport.d.ts +44 -0
  118. package/react-dist/types/City.d.ts +18 -0
  119. package/react-dist/types/CreateOrderPayload.d.ts +72 -0
  120. package/react-dist/types/CurrencyConversion.d.ts +10 -0
  121. package/react-dist/types/DuffelAncillariesProps.d.ts +70 -0
  122. package/react-dist/types/Offer.d.ts +711 -0
  123. package/react-dist/types/Order.d.ts +8 -0
  124. package/react-dist/types/Place.d.ts +8 -0
  125. package/react-dist/types/SeatMap.d.ts +190 -0
  126. package/react-dist/types/index.d.ts +11 -0
  127. package/scripts/generate-fixture.ts +195 -0
  128. package/scripts/upload-to-cdn.sh +39 -0
  129. package/scripts.tsconfig.json +11 -0
  130. package/src/components/AnimatedLoaderEllipsis.tsx +5 -0
  131. package/src/components/Button.tsx +62 -0
  132. package/src/components/Card.tsx +126 -0
  133. package/src/components/Counter.tsx +40 -0
  134. package/src/components/DuffelAncillaries.tsx +346 -0
  135. package/src/components/DuffelAncillariesCustomElement.tsx +124 -0
  136. package/src/components/ErrorBoundary.tsx +54 -0
  137. package/src/components/FetchOfferErrorState.tsx +35 -0
  138. package/src/components/Icon.tsx +151 -0
  139. package/src/components/IconButton.tsx +42 -0
  140. package/src/components/Modal.tsx +36 -0
  141. package/src/components/NonIdealState.tsx +28 -0
  142. package/src/components/Stamp.tsx +29 -0
  143. package/src/components/Tabs.tsx +36 -0
  144. package/src/components/bags/BaggageSelectionCard.tsx +96 -0
  145. package/src/components/bags/BaggageSelectionController.tsx +88 -0
  146. package/src/components/bags/BaggageSelectionModal.tsx +81 -0
  147. package/src/components/bags/BaggageSelectionModalBody.tsx +60 -0
  148. package/src/components/bags/BaggageSelectionModalBodyPassenger.tsx +122 -0
  149. package/src/components/bags/BaggageSelectionModalFooter.tsx +81 -0
  150. package/src/components/bags/BaggageSelectionModalHeader.tsx +76 -0
  151. package/src/components/bags/IncludedBaggageBanner.tsx +51 -0
  152. package/src/components/cancel_for_any_reason/CfarSelectionCard.tsx +90 -0
  153. package/src/components/cancel_for_any_reason/CfarSelectionModal.tsx +63 -0
  154. package/src/components/cancel_for_any_reason/CfarSelectionModalBody.tsx +56 -0
  155. package/src/components/cancel_for_any_reason/CfarSelectionModalBodyListItem.tsx +11 -0
  156. package/src/components/cancel_for_any_reason/CfarSelectionModalFooter.tsx +74 -0
  157. package/src/components/cancel_for_any_reason/CfarSelectionModalHeader.tsx +9 -0
  158. package/src/components/seats/Amenity.tsx +21 -0
  159. package/src/components/seats/DeckSelect.tsx +27 -0
  160. package/src/components/seats/Element.tsx +52 -0
  161. package/src/components/seats/EmptyElement.tsx +5 -0
  162. package/src/components/seats/ExitElement.tsx +17 -0
  163. package/src/components/seats/Legend.tsx +60 -0
  164. package/src/components/seats/Row.tsx +47 -0
  165. package/src/components/seats/RowSection.tsx +75 -0
  166. package/src/components/seats/SeatElement.tsx +120 -0
  167. package/src/components/seats/SeatInfo.tsx +32 -0
  168. package/src/components/seats/SeatMap.tsx +81 -0
  169. package/src/components/seats/SeatMapUnavailable.tsx +21 -0
  170. package/src/components/seats/SeatSelectionCard.tsx +103 -0
  171. package/src/components/seats/SeatSelectionModal.tsx +142 -0
  172. package/src/components/seats/SeatSelectionModalBody.tsx +13 -0
  173. package/src/components/seats/SeatSelectionModalFooter.tsx +82 -0
  174. package/src/components/seats/SeatSelectionModalHeader.tsx +87 -0
  175. package/src/components/seats/SeatUnavailable.tsx +14 -0
  176. package/src/examples/client-side/index.html +57 -0
  177. package/src/examples/full-stack/index.html +48 -0
  178. package/src/examples/full-stack/server.mjs +157 -0
  179. package/src/examples/just-typescript/README.md +37 -0
  180. package/src/examples/just-typescript/package.json +16 -0
  181. package/src/examples/just-typescript/src/index.html +23 -0
  182. package/src/examples/just-typescript/src/index.ts +35 -0
  183. package/src/examples/just-typescript/yarn.lock +154 -0
  184. package/src/examples/react-app/README.md +37 -0
  185. package/src/examples/react-app/package.json +20 -0
  186. package/src/examples/react-app/src/index.html +19 -0
  187. package/src/examples/react-app/src/index.tsx +43 -0
  188. package/src/examples/react-app/yarn.lock +219 -0
  189. package/src/fixtures/offers/off_0000AUde3KwTztSRK1cznH.json +497 -0
  190. package/src/fixtures/offers/off_0000AVx4lUFFKW8PsPeQeQ.json +307 -0
  191. package/src/fixtures/offers/off_1.json +497 -0
  192. package/src/fixtures/passengers/mock_passengers.ts +26 -0
  193. package/src/fixtures/seat-maps/off_0000AUde3KwTztSRK1cznH.json +6852 -0
  194. package/src/fixtures/seat-maps/off_0000AVx4lUFFKW8PsPeQeQ.json +1 -0
  195. package/src/fixtures/seat-maps/off_1.json +6852 -0
  196. package/src/index.ts +8 -0
  197. package/src/lib/captureErrorInSentry.ts +60 -0
  198. package/src/lib/compileCreateOrderPayload.ts +63 -0
  199. package/src/lib/createPriceFormatters.ts +73 -0
  200. package/src/lib/fetchFromDuffelAPI.ts +24 -0
  201. package/src/lib/fetchFromFixtures.ts +18 -0
  202. package/src/lib/formatAvailableServices.ts +91 -0
  203. package/src/lib/formatDate.ts +21 -0
  204. package/src/lib/formatSeatMaps.ts +81 -0
  205. package/src/lib/getBaggageServiceDescription.ts +44 -0
  206. package/src/lib/getCabinsForSegmentAndDeck.ts +4 -0
  207. package/src/lib/getCurrencyForSeatMaps.ts +22 -0
  208. package/src/lib/getCurrencyForServices.ts +24 -0
  209. package/src/lib/getFirstSeatElementMatchingCriteria.ts +22 -0
  210. package/src/lib/getPassengerBySegmentList.ts +10 -0
  211. package/src/lib/getPassengerInitials.ts +6 -0
  212. package/src/lib/getPassengerMapById.ts +17 -0
  213. package/src/lib/getPassengerName.ts +37 -0
  214. package/src/lib/getRowNumber.ts +16 -0
  215. package/src/lib/getSegmentList.ts +7 -0
  216. package/src/lib/getServicePriceMapById.ts +20 -0
  217. package/src/lib/getSymbols.ts +22 -0
  218. package/src/lib/getTotalAmountForServices.ts +72 -0
  219. package/src/lib/getTotalQuantity.ts +5 -0
  220. package/src/lib/hasService.ts +24 -0
  221. package/src/lib/hasServiceOfSameMetadataTypeAlreadyBeenSelected.ts +35 -0
  222. package/src/lib/hasWings.ts +8 -0
  223. package/src/lib/isBaggageService.ts +8 -0
  224. package/src/lib/isCancelForAnyReasonService.ts +9 -0
  225. package/src/lib/isFixtureOfferId.ts +4 -0
  226. package/src/lib/isPayloadComplete.ts +11 -0
  227. package/src/lib/isSeatElement.ts +10 -0
  228. package/src/lib/logging.ts +100 -0
  229. package/src/lib/moneyStringFormatter.ts +34 -0
  230. package/src/lib/offerIsExpired.ts +5 -0
  231. package/src/lib/retrieveOffer.ts +49 -0
  232. package/src/lib/retrieveOfferFromDuffelAPI.ts +13 -0
  233. package/src/lib/retrieveSeatMaps.ts +50 -0
  234. package/src/lib/retrieveSeatMapsFromDuffelAPI.ts +13 -0
  235. package/src/lib/setBodyScrollability.ts +7 -0
  236. package/src/lib/validateProps.ts +37 -0
  237. package/src/lib/withPlural.ts +8 -0
  238. package/src/stories/BaggageSelectionModalHeader.stories.tsx +21 -0
  239. package/src/stories/Button.stories.tsx +60 -0
  240. package/src/stories/DuffelAncillaries.stories.tsx +126 -0
  241. package/src/stories/Icon.stories.tsx +34 -0
  242. package/src/stories/IconButton.stories.tsx +25 -0
  243. package/src/styles/colors.css +22 -0
  244. package/src/styles/components/Amenity.css +23 -0
  245. package/src/styles/components/BaggageDisplay.css +25 -0
  246. package/src/styles/components/Button.css +161 -0
  247. package/src/styles/components/Card.css +52 -0
  248. package/src/styles/components/CfarSelectionModal.css +34 -0
  249. package/src/styles/components/Counter.css +18 -0
  250. package/src/styles/components/IconButton.css +63 -0
  251. package/src/styles/components/Legend.css +58 -0
  252. package/src/styles/components/Loader.css +37 -0
  253. package/src/styles/components/LoadingState.css +81 -0
  254. package/src/styles/components/Modal.css +83 -0
  255. package/src/styles/components/PassengerSelect.css +93 -0
  256. package/src/styles/components/PassengersLayout.css +90 -0
  257. package/src/styles/components/Row.css +70 -0
  258. package/src/styles/components/Seat.css +57 -0
  259. package/src/styles/components/SeatInfo.css +61 -0
  260. package/src/styles/components/SeatMap.css +24 -0
  261. package/src/styles/components/SeatSelect.css +92 -0
  262. package/src/styles/components/Segment.css +17 -0
  263. package/src/styles/components/SelectionSegment.css +10 -0
  264. package/src/styles/components/Summary.css +70 -0
  265. package/src/styles/components/Tabs.css +49 -0
  266. package/src/styles/flex.css +5 -0
  267. package/src/styles/font-families.css +47 -0
  268. package/src/styles/global.css +50 -0
  269. package/src/styles/margin.css +3 -0
  270. package/src/styles/spacing.css +18 -0
  271. package/src/styles/transitions.css +3 -0
  272. package/src/styles/typography.css +13 -0
  273. package/src/tests/components/DuffelAncillaries.test.tsx +342 -0
  274. package/src/tests/lib/createPriceFormatters.test.tsx +152 -0
  275. package/src/tests/lib/formatAvailableServices.test.tsx +79 -0
  276. package/src/tests/lib/formatSeatMaps.test.tsx +49 -0
  277. package/src/tests/lib/getCurrencyForServices.test.tsx +44 -0
  278. package/src/tests/lib/hasServiceOfSameMetadataTypeAlreadyBeenSelected.test.ts +86 -0
  279. package/src/tests/lib/logging.test.tsx +32 -0
  280. package/src/tests/lib/moneyStringFormatter.test.tsx +12 -0
  281. package/src/tests/lib/validateProps.test.tsx +57 -0
  282. package/src/types/Aircraft.ts +16 -0
  283. package/src/types/Airline.ts +16 -0
  284. package/src/types/Airport.ts +54 -0
  285. package/src/types/City.ts +21 -0
  286. package/src/types/CreateOrderPayload.ts +99 -0
  287. package/src/types/CurrencyConversion.ts +10 -0
  288. package/src/types/DuffelAncillariesProps.ts +108 -0
  289. package/src/types/Offer.ts +851 -0
  290. package/src/types/Order.ts +6 -0
  291. package/src/types/Place.ts +6 -0
  292. package/src/types/SeatMap.ts +231 -0
  293. package/src/types/index.ts +11 -0
  294. package/tsconfig.json +52 -0
  295. package/LICENSE +0 -21
  296. package/dist/AdditionalBaggage.esm.js +0 -1
  297. package/dist/AdditionalBaggage.js +0 -1
  298. package/dist/AdditionalBaggage.min.css +0 -408
  299. package/dist/AdditionalBaggage.umd.min.js +0 -2
  300. package/dist/AdditionalBaggage.umd.min.js.LICENSE.txt +0 -60
  301. package/dist/AdditionalBaggageSelection.esm.js +0 -1
  302. package/dist/AdditionalBaggageSelection.js +0 -1
  303. package/dist/AdditionalBaggageSelection.min.css +0 -744
  304. package/dist/AdditionalBaggageSelection.umd.min.js +0 -2
  305. package/dist/AdditionalBaggageSelection.umd.min.js.LICENSE.txt +0 -93
  306. package/dist/CardPayment.esm.js +0 -2
  307. package/dist/CardPayment.esm.js.LICENSE.txt +0 -6
  308. package/dist/CardPayment.js +0 -2
  309. package/dist/CardPayment.js.LICENSE.txt +0 -6
  310. package/dist/CardPayment.min.css +0 -233
  311. package/dist/CardPayment.umd.min.js +0 -2
  312. package/dist/CardPayment.umd.min.js.LICENSE.txt +0 -61
  313. package/dist/SeatSelection.esm.js +0 -1
  314. package/dist/SeatSelection.js +0 -1
  315. package/dist/SeatSelection.min.css +0 -1127
  316. package/dist/SeatSelection.umd.min.js +0 -2
  317. package/dist/SeatSelection.umd.min.js.LICENSE.txt +0 -60
  318. package/dist/duffel-components.d.ts +0 -1614
  319. package/dist/duffel-components.esm.js +0 -2
  320. package/dist/duffel-components.esm.js.LICENSE.txt +0 -6
  321. package/dist/duffel-components.js +0 -2
  322. package/dist/duffel-components.js.LICENSE.txt +0 -6
  323. package/dist/duffel-components.min.css +0 -1280
  324. package/dist/duffel-components.umd.min.js +0 -2
  325. package/dist/duffel-components.umd.min.js.LICENSE.txt +0 -102
@@ -0,0 +1,142 @@
1
+ import { getCurrencyForSeatMaps } from "@lib/getCurrencyForSeatMaps";
2
+ import { getPassengerBySegmentList } from "@lib/getPassengerBySegmentList";
3
+ import { getPassengerMapById } from "@lib/getPassengerMapById";
4
+ import { getPassengerName } from "@lib/getPassengerName";
5
+ import { getSegmentList } from "@lib/getSegmentList";
6
+ import { getServicePriceMapById } from "@lib/getServicePriceMapById";
7
+ import React from "react";
8
+ import {
9
+ CreateOrderPayload,
10
+ CreateOrderPayloadSeatService,
11
+ } from "../../types/CreateOrderPayload";
12
+ import { Offer } from "../../types/Offer";
13
+ import { SeatMap } from "../../types/SeatMap";
14
+ import { Modal } from "../Modal";
15
+ import { SeatSelectionModalBody } from "./SeatSelectionModalBody";
16
+ import { SeatSelectionModalFooter } from "./SeatSelectionModalFooter";
17
+ import { SeatSelectionModalHeader } from "./SeatSelectionModalHeader";
18
+
19
+ export interface SeatSelectionModalProps {
20
+ isOpen: boolean;
21
+ offer?: Offer;
22
+ seatMaps?: SeatMap[];
23
+ selectedServices: CreateOrderPayloadSeatService[];
24
+ passengers: CreateOrderPayload["passengers"];
25
+ onClose: (selectedServices: CreateOrderPayloadSeatService[]) => void;
26
+ }
27
+
28
+ export const SeatSelectionModal: React.FC<SeatSelectionModalProps> = ({
29
+ isOpen,
30
+ offer,
31
+ passengers,
32
+ seatMaps,
33
+ selectedServices,
34
+ onClose,
35
+ }) => {
36
+ const [currentPermutationIndex, setCurrentPermutationIndex] =
37
+ React.useState(0);
38
+
39
+ const [selectedServicesState, setSelectedServicesState] =
40
+ React.useState<CreateOrderPayloadSeatService[]>(selectedServices);
41
+ const selectedServicesStateMap = selectedServicesState.reduce(
42
+ (all, service) => ({ ...all, [service.id]: service }),
43
+ {} as Record<string, CreateOrderPayloadSeatService>
44
+ );
45
+
46
+ if (!offer || !seatMaps) return null;
47
+
48
+ const segments = getSegmentList(offer);
49
+ const passengerMapById = getPassengerMapById(passengers);
50
+ const servicePricesMap = getServicePriceMapById(offer.available_services);
51
+ const segmentAndPassengerPermutations = getPassengerBySegmentList(segments);
52
+ const {
53
+ passenger: { passenger_id: currentPassengerId },
54
+ passengerIndex: currentPassengerIndex,
55
+ segment: { id: currentSegmentId },
56
+ } = segmentAndPassengerPermutations[currentPermutationIndex];
57
+
58
+ const currentSegment = segments.find(({ id }) => id === currentSegmentId)!;
59
+ const currentPassenger = passengerMapById[currentPassengerId];
60
+ const currentSeatMap = seatMaps.find(
61
+ (seatMap) => seatMap.segment_id === currentSegmentId
62
+ )!;
63
+
64
+ const currentPassengerName = getPassengerName(
65
+ currentPassenger,
66
+ offer.passengers[currentPassengerIndex],
67
+ currentPassengerIndex + 1
68
+ );
69
+
70
+ const onSeatToggle = (seatServiceToToggle: CreateOrderPayloadSeatService) => {
71
+ let newSeatServices = new Array<CreateOrderPayloadSeatService>();
72
+
73
+ for (const selectedServiceFromState of selectedServicesState) {
74
+ const hasClickedSeatToToggleOff =
75
+ selectedServiceFromState.id === seatServiceToToggle.id &&
76
+ seatServiceToToggle.quantity === 0;
77
+
78
+ const isSelectedServiceFromStateForTheSameSegmentAndPassengerPermutation =
79
+ selectedServiceFromState.serviceInformation?.segmentId ===
80
+ currentSegmentId &&
81
+ selectedServiceFromState.serviceInformation?.passengerId ===
82
+ currentPassengerId;
83
+
84
+ if (
85
+ !hasClickedSeatToToggleOff &&
86
+ !isSelectedServiceFromStateForTheSameSegmentAndPassengerPermutation
87
+ ) {
88
+ newSeatServices = [...newSeatServices, selectedServiceFromState];
89
+ }
90
+ }
91
+
92
+ if (seatServiceToToggle.quantity > 0) {
93
+ newSeatServices = [...newSeatServices, seatServiceToToggle];
94
+ }
95
+
96
+ setSelectedServicesState(newSeatServices);
97
+ };
98
+
99
+ const currencyToUse =
100
+ getCurrencyForSeatMaps(seatMaps) ?? offer.total_currency;
101
+
102
+ return (
103
+ <Modal isOpen={isOpen} onClose={() => onClose(selectedServicesState)}>
104
+ <SeatSelectionModalHeader
105
+ segmentAndPassengerPermutationsCount={
106
+ segmentAndPassengerPermutations.length
107
+ }
108
+ currentSegment={currentSegment}
109
+ currentPassengerName={currentPassengerName}
110
+ currentSegmentAndPassengerPermutationsIndex={currentPermutationIndex}
111
+ setCurrentSegmentAndPassengerPermutationsIndex={
112
+ setCurrentPermutationIndex
113
+ }
114
+ />
115
+ <SeatSelectionModalBody
116
+ selectedServicesMap={selectedServicesStateMap}
117
+ seatMap={currentSeatMap}
118
+ onSeatToggled={onSeatToggle}
119
+ currentPassengerId={currentPassengerId}
120
+ currentPassengerName={currentPassengerName}
121
+ currentSegmentId={currentSegmentId}
122
+ />
123
+ <SeatSelectionModalFooter
124
+ seatMaps={seatMaps}
125
+ currency={currencyToUse}
126
+ selectedServices={selectedServicesState}
127
+ servicePrices={servicePricesMap}
128
+ isFirstSegment={currentPermutationIndex === 0}
129
+ isLastSegment={
130
+ currentPermutationIndex + 1 === segmentAndPassengerPermutations.length
131
+ }
132
+ onNextSegmentButtonClicked={() => {
133
+ setCurrentPermutationIndex(currentPermutationIndex + 1);
134
+ }}
135
+ onPreviousSegmentButtonClicked={() => {
136
+ setCurrentPermutationIndex(currentPermutationIndex - 1);
137
+ }}
138
+ onClose={() => onClose(selectedServicesState)}
139
+ />
140
+ </Modal>
141
+ );
142
+ };
@@ -0,0 +1,13 @@
1
+ import { ModalBody } from "@components/Modal";
2
+ import React from "react";
3
+ import { SeatMap, SeatMapProps } from "./SeatMap";
4
+
5
+ export type SeatSelectionModalBodyProps = SeatMapProps;
6
+
7
+ export const SeatSelectionModalBody: React.FC<SeatSelectionModalBodyProps> = (
8
+ props
9
+ ) => (
10
+ <ModalBody>
11
+ <SeatMap {...props} />
12
+ </ModalBody>
13
+ );
@@ -0,0 +1,82 @@
1
+ import { ServicePriceMapById } from "@lib/getServicePriceMapById";
2
+ import { getTotalAmountForServicesWithPriceMap } from "@lib/getTotalAmountForServices";
3
+ import { getTotalQuantity } from "@lib/getTotalQuantity";
4
+ import { moneyStringFormatter } from "@lib/moneyStringFormatter";
5
+ import { withPlural } from "@lib/withPlural";
6
+ import React from "react";
7
+ import { CreateOrderPayloadServices } from "../../types/CreateOrderPayload";
8
+ import { SeatMap } from "../../types/SeatMap";
9
+ import { Button } from "../Button";
10
+
11
+ export interface SeatSelectionModalFooterProps {
12
+ currency: string;
13
+ selectedServices: CreateOrderPayloadServices;
14
+ servicePrices: ServicePriceMapById;
15
+
16
+ isFirstSegment: boolean;
17
+ isLastSegment: boolean;
18
+
19
+ onNextSegmentButtonClicked: () => void;
20
+ onPreviousSegmentButtonClicked: () => void;
21
+ onClose: () => void;
22
+ seatMaps: SeatMap[];
23
+ }
24
+
25
+ export const SeatSelectionModalFooter: React.FC<
26
+ SeatSelectionModalFooterProps
27
+ > = ({
28
+ selectedServices,
29
+ servicePrices,
30
+ currency,
31
+ isFirstSegment,
32
+ isLastSegment,
33
+ onNextSegmentButtonClicked,
34
+ onPreviousSegmentButtonClicked,
35
+ onClose,
36
+ seatMaps,
37
+ }) => {
38
+ const totalQuantity = getTotalQuantity(selectedServices);
39
+ const totalAmount = getTotalAmountForServicesWithPriceMap(
40
+ servicePrices,
41
+ selectedServices,
42
+ seatMaps
43
+ );
44
+ const totalAmountLabel = moneyStringFormatter(currency)(totalAmount);
45
+
46
+ return (
47
+ <div style={{ padding: "16px 24px 24px" }}>
48
+ <div className="flex--space-between">
49
+ <div>Price for {withPlural(totalQuantity, "seat", "seats")}</div>
50
+ <div className="h3--semibold">+ {totalAmountLabel}</div>
51
+ </div>
52
+
53
+ <div
54
+ style={{
55
+ marginTop: "16px",
56
+ display: "grid",
57
+ columnGap: "12px",
58
+ gridTemplateColumns: "repeat(2, 1fr)",
59
+ }}
60
+ >
61
+ <Button
62
+ size={48}
63
+ disabled={isFirstSegment}
64
+ variant="outlined"
65
+ onClick={() => onPreviousSegmentButtonClicked()}
66
+ >
67
+ Back
68
+ </Button>
69
+
70
+ <Button
71
+ size={48}
72
+ data-testid="confirm-selection-for-seats"
73
+ onClick={() =>
74
+ isLastSegment ? onClose() : onNextSegmentButtonClicked()
75
+ }
76
+ >
77
+ {isLastSegment ? "Confirm" : "Next"}
78
+ </Button>
79
+ </div>
80
+ </div>
81
+ );
82
+ };
@@ -0,0 +1,87 @@
1
+ import { formatDateString } from "@lib/formatDate";
2
+ import React from "react";
3
+ import { OfferSliceSegment } from "../../types/Offer";
4
+
5
+ export interface SeatSelectionModalHeaderProps {
6
+ segmentAndPassengerPermutationsCount: number;
7
+ currentSegment: OfferSliceSegment;
8
+ currentPassengerName: string;
9
+
10
+ currentSegmentAndPassengerPermutationsIndex: number;
11
+ setCurrentSegmentAndPassengerPermutationsIndex: (index: number) => void;
12
+ }
13
+
14
+ export const SeatSelectionModalHeader: React.FC<
15
+ SeatSelectionModalHeaderProps
16
+ > = ({
17
+ segmentAndPassengerPermutationsCount,
18
+ currentSegmentAndPassengerPermutationsIndex,
19
+ currentSegment,
20
+ currentPassengerName,
21
+ setCurrentSegmentAndPassengerPermutationsIndex,
22
+ }) => (
23
+ <div style={{ padding: "24px 24px 16px" }}>
24
+ <div style={{ display: "flex", columnGap: "4px" }}>
25
+ {Array(segmentAndPassengerPermutationsCount)
26
+ .fill(0)
27
+ .map((_, index) =>
28
+ index === currentSegmentAndPassengerPermutationsIndex ? (
29
+ <ActiveSegment key={`segment_${index}`} />
30
+ ) : (
31
+ <InactiveSegment
32
+ key={`segment_${index}`}
33
+ onClick={() =>
34
+ setCurrentSegmentAndPassengerPermutationsIndex(index)
35
+ }
36
+ />
37
+ )
38
+ )}
39
+ </div>
40
+ <h2 className="h3--semibold" style={{ marginBlock: "12px 0px" }}>
41
+ Flight to {currentSegment.destination.iata_code}
42
+ <span
43
+ className="p2--regular"
44
+ style={{
45
+ color: "var(--GREY-600)",
46
+ marginLeft: "8px",
47
+ }}
48
+ >
49
+ {formatDateString(currentSegment.departing_at)}
50
+ </span>
51
+ </h2>
52
+ <p
53
+ className="h3--semibold"
54
+ style={{ color: `var(--GREY-600)`, marginBlock: "0 4px" }}
55
+ >
56
+ {currentPassengerName}
57
+ </p>
58
+ </div>
59
+ );
60
+
61
+ const InactiveSegment: React.FC<{
62
+ style?: React.CSSProperties;
63
+ onClick?: React.MouseEventHandler<HTMLButtonElement>;
64
+ }> = ({ onClick, style }) => (
65
+ <button
66
+ onClick={onClick}
67
+ style={{
68
+ border: "none",
69
+ width: "4px",
70
+ height: "4px",
71
+ padding: "0",
72
+ borderRadius: "4px",
73
+ backgroundColor: "rgba(var(--ACCENT), var(--ACCENT-LIGHT-200))",
74
+ transition: "background-color 0.3s var(--TRANSITION-CUBIC-BEZIER)",
75
+ ...style,
76
+ }}
77
+ />
78
+ );
79
+
80
+ const ActiveSegment = () => (
81
+ <InactiveSegment
82
+ onClick={undefined}
83
+ style={{
84
+ backgroundColor: "rgb(var(--ACCENT))",
85
+ }}
86
+ />
87
+ );
@@ -0,0 +1,14 @@
1
+ import * as React from "react";
2
+ import { SeatMapCabinRowSectionElementSeat } from "../../types/SeatMap";
3
+ import { Icon } from "../Icon";
4
+
5
+ export const SeatUnavailable: React.FC<{
6
+ seat: SeatMapCabinRowSectionElementSeat;
7
+ }> = ({ seat }) => (
8
+ <span
9
+ className="map-element map-element__seat"
10
+ aria-label={`${seat.designator} ${seat.name || "Seat"} Unavailable`}
11
+ >
12
+ <Icon name="close" size={14} />
13
+ </span>
14
+ );
@@ -0,0 +1,57 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Checkout page example</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link
7
+ rel="icon"
8
+ type="image/png"
9
+ sizes="96x96"
10
+ href="https://duffel.com/images/favicon/favicon-96x96.png"
11
+ />
12
+
13
+ <!-- 1. This script loads duffel-components -->
14
+ <script src="http://localhost:8000/ancillaries/index.js"></script>
15
+ </head>
16
+
17
+ <body style="font-family: sans-serif">
18
+ <h1>Checkout page</h1>
19
+
20
+ <!-- 2. Add the custom element to your markup where you want to render the ancillaries card -->
21
+ <duffel-ancillaries></duffel-ancillaries>
22
+ </body>
23
+ <script>
24
+ const duffelAncillariesElement =
25
+ document.querySelector("duffel-ancillaries");
26
+
27
+ // 3. Render the component with the required data, you can safely call this function as many times as you want. E.g. when your passenger data changes.
28
+ duffelAncillariesElement.render({
29
+ offer_id: "fixture_off_1",
30
+ services: ["bags", "seats", "cancel_for_any_reason"],
31
+ passengers: [
32
+ {
33
+ given_name: "Mae",
34
+ family_name: "Jemison",
35
+ gender: "F",
36
+ title: "dr",
37
+ born_on: "1956-10-17",
38
+ email: "m.jemison@nasa.gov",
39
+ phone_number: "+16177562626",
40
+ },
41
+ {
42
+ given_name: "Dorothy",
43
+ family_name: "Green",
44
+ gender: "F",
45
+ title: "dr",
46
+ born_on: "1942-10-17",
47
+ },
48
+ ],
49
+ debug: true,
50
+ });
51
+
52
+ // 4. Listen to 'onPayloadReady' event on the component. `event.detail.data` contains the payload you need to send to Duffel's API to create an order.
53
+ duffelAncillariesElement.addEventListener("onPayloadReady", (event) =>
54
+ console.log("onPayloadReady\n", event.detail)
55
+ );
56
+ </script>
57
+ </html>
@@ -0,0 +1,48 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Checkout page example</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link
7
+ rel="icon"
8
+ type="image/png"
9
+ sizes="96x96"
10
+ href="https://duffel.com/images/favicon/favicon-96x96.png"
11
+ />
12
+
13
+ <!-- 1. This script loads duffel-components -->
14
+ <script src="http://localhost:8000/ancillaries/index.js"></script>
15
+ </head>
16
+
17
+ <body style="font-family: sans-serif">
18
+ <h1>Checkout page</h1>
19
+
20
+ <!-- 2. Add the custom element to your markup where you want to render the ancillaries card -->
21
+ <duffel-ancillaries></duffel-ancillaries>
22
+ </body>
23
+ <script>
24
+ const duffelAncillariesElement =
25
+ document.querySelector("duffel-ancillaries");
26
+
27
+ // 3. Render the component with the required data, you can safely call this function as many times as you want. E.g. when your passenger data changes.
28
+ duffelAncillariesElement.render({
29
+ offer_id: "__OFFER_ID__",
30
+ client_key: "__CLIENT_KEY__",
31
+ passengers: "__PASSENGERS__",
32
+ services: ["seats", "bags", "cancel_for_any_reason"],
33
+ debug: true,
34
+ });
35
+
36
+ // 4. Listen to 'onPayloadReady' event on the component. `event.detail.data` contains the payload you need to send to Duffel's API to create an order.
37
+ duffelAncillariesElement.addEventListener("onPayloadReady", (event) => {
38
+ console.log("onPayloadReady\n", event.detail);
39
+ const body = JSON.stringify({ data: event.detail.data });
40
+
41
+ // 5. Send the order creation payload to your server to create an order with the Duffel API.
42
+ fetch("/book", { method: "POST", body })
43
+ .then((response) => response.json())
44
+ .then(({ data }) => console.log("Order created", data))
45
+ .catch((error) => console.error("Order creation failed", error));
46
+ });
47
+ </script>
48
+ </html>
@@ -0,0 +1,157 @@
1
+ import dotenv from "dotenv";
2
+ import { readFileSync } from "fs";
3
+ import http from "http";
4
+
5
+ /* https://nodejs.org/api/cli.html#node_tls_reject_unauthorizedvalue */
6
+ process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
7
+
8
+ dotenv.config({ path: ".env.local" });
9
+
10
+ if (process.env.DUFFEL_API_URL === undefined) {
11
+ throw new Error("process.env.DUFFEL_API_URL is required but missing");
12
+ }
13
+
14
+ if (process.env.DUFFEL_API_TOKEN === undefined) {
15
+ throw new Error("process.env.DUFFEL_API_TOKEN is required but missing");
16
+ }
17
+
18
+ const makeMockDateInTheFuture = (daysAhead) => {
19
+ const now = new Date(Date.now());
20
+ now.setDate(now.getDate() + daysAhead);
21
+ return now;
22
+ };
23
+
24
+ const duffelHeaders = {
25
+ "Duffel-Version": "v1",
26
+ "Accept-Encoding": "gzip",
27
+ Accept: "application/json",
28
+ "Content-Type": "application/json",
29
+ Authorization: `Bearer ${process.env.DUFFEL_API_TOKEN}`,
30
+ };
31
+
32
+ let searchRoundTripOnDuffelResultCache = null;
33
+ const searchRoundTripOnDuffel = async (origin, destination) => {
34
+ if (searchRoundTripOnDuffelResultCache !== null) {
35
+ return searchRoundTripOnDuffelResultCache;
36
+ }
37
+
38
+ const payload = {
39
+ data: {
40
+ slices: [
41
+ {
42
+ origin,
43
+ destination,
44
+ departure_date: makeMockDateInTheFuture(7)
45
+ .toISOString()
46
+ .split("T")[0],
47
+ },
48
+ {
49
+ origin: destination,
50
+ destination: origin,
51
+ departure_date: makeMockDateInTheFuture(14)
52
+ .toISOString()
53
+ .split("T")[0],
54
+ },
55
+ ],
56
+ passengers: [{ type: "adult" }],
57
+ requested_sources: ["duffel_airways"],
58
+ },
59
+ };
60
+
61
+ const { data: offerRequest } = await (
62
+ await fetch(
63
+ process.env.DUFFEL_API_URL + "/air/offer_requests?return_offers=true",
64
+ {
65
+ method: "POST",
66
+ body: JSON.stringify(payload),
67
+ headers: duffelHeaders,
68
+ }
69
+ )
70
+ ).json();
71
+
72
+ searchRoundTripOnDuffelResultCache = offerRequest;
73
+ return searchRoundTripOnDuffelResultCache;
74
+ };
75
+
76
+ const createOrderOnDuffel = async (request, response) => {
77
+ const createOrderOnDuffelResponse = await fetch(
78
+ process.env.DUFFEL_API_URL + "/air/orders",
79
+ {
80
+ method: "POST",
81
+ headers: duffelHeaders,
82
+ body: request,
83
+ duplex: "half",
84
+ }
85
+ );
86
+
87
+ response.writeHead(createOrderOnDuffelResponse.status, {
88
+ "Content-type": "application/json",
89
+ });
90
+ response.write(await createOrderOnDuffelResponse.text());
91
+ response.end();
92
+ };
93
+
94
+ const ROUTES = {
95
+ "/": async function index(request, response) {
96
+ const offerRequest = await searchRoundTripOnDuffel("JFK", "MIA");
97
+ const offer = offerRequest.offers[0];
98
+
99
+ if (!offer) {
100
+ response.writeHead(404);
101
+ response.end(http.STATUS_CODES[404]);
102
+ return;
103
+ }
104
+
105
+ const passengers = [
106
+ {
107
+ id: offer.passengers[0].id,
108
+ given_name: "Mae",
109
+ family_name: "Jemison",
110
+ gender: "F",
111
+ title: "dr",
112
+ born_on: "1956-10-17",
113
+ email: "m.jemison@nasa.gov",
114
+ phone_number: "+16177562626",
115
+ },
116
+ ];
117
+
118
+ const template = readFileSync("src/examples/full-stack/index.html", {
119
+ encoding: "utf-8",
120
+ });
121
+
122
+ const withOfferId = template.replace("__OFFER_ID__", offer.id);
123
+ const withclientKey = withOfferId.replace(
124
+ "__CLIENT_KEY__",
125
+ offerRequest.client_key
126
+ );
127
+ const withPassengers = withclientKey.replace(
128
+ `"__PASSENGERS__"`,
129
+ `${JSON.stringify(passengers)}`
130
+ );
131
+
132
+ response.writeHead(200);
133
+ response.end(withPassengers);
134
+ },
135
+ "/book": async function book(request, response) {
136
+ if (request.method != "POST") {
137
+ response.writeHead(404);
138
+ response.end(http.STATUS_CODES[404]);
139
+ return;
140
+ }
141
+
142
+ await createOrderOnDuffel(request, response);
143
+ },
144
+ };
145
+
146
+ http
147
+ .createServer(function (request, response) {
148
+ if (request.url in ROUTES) {
149
+ return ROUTES[request.url](request, response);
150
+ }
151
+
152
+ response.writeHead(404);
153
+ response.end(http.STATUS_CODES[404]);
154
+ })
155
+ .listen(6262);
156
+
157
+ console.log(`\nšŸ„ Serving example on http://localhost:6262`);
@@ -0,0 +1,37 @@
1
+ # duffel-components typescript example
2
+
3
+ ## TL;DR
4
+
5
+ Start on the root of the `duffel-components` repository:
6
+
7
+ ```sh
8
+ # build duffel-components for react env:
9
+ yarn react-build
10
+
11
+ # change directory to example folder
12
+ cd src/examples/just-typescript
13
+
14
+ # cleanup last install and build
15
+ rm -rf node_modules && rm -rf dist
16
+
17
+ # install new version
18
+ yarn
19
+
20
+ # build and watch example
21
+ yarn build
22
+
23
+ # open example
24
+ open src/index.html -a "Safari"
25
+ ```
26
+
27
+ ## Build duffel-components
28
+
29
+ First, navigate to the root folder of the duffel-components repository and run `yarn react-build` to build the package. It should output a react-dist folder on the root. This folder is the one we reference on `src/examples/just-typescript/package.json` dependencies under `duffel-components`
30
+
31
+ ## Install dependencies
32
+
33
+ Once the package is built, you can cd into this directory and run `yarn` to install all dependencies. If there are changes to `react-dist` since your last install, you'll need to `rm -rf node_modules`, otherwise the updates build will not be installed.
34
+
35
+ ## Run example
36
+
37
+ Finally, run `yarn dev` to build `src/examples/just-typescript/src/index.tsx`. This will produce `src/examples/just-typescript/dist` folder that is referenced by `src/examples/just-typescript/src/index.html`. You can then open `src/examples/just-typescript/src/index.html` on your browser to see the example up and running.
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "just-typescript",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "build": "esbuild src/index.ts --bundle --outfile=dist/index.js"
7
+ },
8
+ "dependencies": {
9
+ "@types/node": "20.2.5",
10
+ "typescript": "5.0.4",
11
+ "duffel-components": "../../../../duffel-components/react-dist"
12
+ },
13
+ "devDependencies": {
14
+ "esbuild": "^0.17.19"
15
+ }
16
+ }
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link
7
+ rel="icon"
8
+ type="image/png"
9
+ sizes="96x96"
10
+ href="https://duffel.com/images/favicon/favicon-96x96.png"
11
+ />
12
+
13
+ <script src="../dist/index.js" defer></script>
14
+ <title>React app example</title>
15
+ </head>
16
+ <body>
17
+ <h1 style="margin-bottom: 2rem">
18
+ Duffel ancillaries component react-app example
19
+ </h1>
20
+
21
+ <duffel-ancillaries></duffel-ancillaries>
22
+ </body>
23
+ </html>