mautourco-components 0.2.5 → 0.2.7

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 (297) hide show
  1. package/README.md +190 -190
  2. package/dist/components/atoms/Avatar/Avatar.d.ts +14 -14
  3. package/dist/components/atoms/Avatar/Avatar.js +31 -31
  4. package/dist/components/atoms/Button/Button.css +320 -320
  5. package/dist/components/atoms/Button/Button.d.ts +27 -27
  6. package/dist/components/atoms/Button/Button.js +35 -35
  7. package/dist/components/atoms/Checkbox/Checkbox.d.ts +13 -13
  8. package/dist/components/atoms/Checkbox/Checkbox.js +39 -39
  9. package/dist/components/atoms/Icon/Icon.d.ts +10 -10
  10. package/dist/components/atoms/Icon/Icon.js +123 -123
  11. package/dist/components/atoms/Icon/icons/ArrivalIcon.d.ts +8 -8
  12. package/dist/components/atoms/Icon/icons/ArrivalIcon.js +31 -31
  13. package/dist/components/atoms/Icon/icons/BuildingIcon.d.ts +8 -8
  14. package/dist/components/atoms/Icon/icons/BuildingIcon.js +36 -36
  15. package/dist/components/atoms/Icon/icons/CalendarIcon.d.ts +12 -12
  16. package/dist/components/atoms/Icon/icons/CalendarIcon.js +41 -41
  17. package/dist/components/atoms/Icon/icons/CalendarOutlineIcon.d.ts +8 -8
  18. package/dist/components/atoms/Icon/icons/CalendarOutlineIcon.js +36 -36
  19. package/dist/components/atoms/Icon/icons/CarIcon.d.ts +8 -8
  20. package/dist/components/atoms/Icon/icons/CarIcon.js +30 -30
  21. package/dist/components/atoms/Icon/icons/Check.d.ts +8 -8
  22. package/dist/components/atoms/Icon/icons/Check.js +30 -30
  23. package/dist/components/atoms/Icon/icons/CheckCircleIcon.d.ts +8 -8
  24. package/dist/components/atoms/Icon/icons/CheckCircleIcon.js +30 -30
  25. package/dist/components/atoms/Icon/icons/Chevron.d.ts +9 -9
  26. package/dist/components/atoms/Icon/icons/Chevron.js +54 -54
  27. package/dist/components/atoms/Icon/icons/ChevronDownIcon.d.ts +8 -8
  28. package/dist/components/atoms/Icon/icons/ChevronDownIcon.js +30 -30
  29. package/dist/components/atoms/Icon/icons/Close.d.ts +8 -8
  30. package/dist/components/atoms/Icon/icons/Close.js +30 -30
  31. package/dist/components/atoms/Icon/icons/DeleteIcon.d.ts +8 -8
  32. package/dist/components/atoms/Icon/icons/DeleteIcon.js +30 -30
  33. package/dist/components/atoms/Icon/icons/DepartureIcon.d.ts +8 -8
  34. package/dist/components/atoms/Icon/icons/DepartureIcon.js +30 -30
  35. package/dist/components/atoms/Icon/icons/EyeIcon.d.ts +8 -8
  36. package/dist/components/atoms/Icon/icons/EyeIcon.js +30 -30
  37. package/dist/components/atoms/Icon/icons/FacebookIcon.d.ts +8 -8
  38. package/dist/components/atoms/Icon/icons/FacebookIcon.js +36 -36
  39. package/dist/components/atoms/Icon/icons/HomeIcon.d.ts +8 -8
  40. package/dist/components/atoms/Icon/icons/HomeIcon.js +25 -25
  41. package/dist/components/atoms/Icon/icons/InfoIcon.d.ts +8 -8
  42. package/dist/components/atoms/Icon/icons/InfoIcon.js +30 -30
  43. package/dist/components/atoms/Icon/icons/LinkedInIcon.d.ts +8 -8
  44. package/dist/components/atoms/Icon/icons/LinkedInIcon.js +36 -36
  45. package/dist/components/atoms/Icon/icons/MapPinIcon.d.ts +8 -8
  46. package/dist/components/atoms/Icon/icons/MapPinIcon.js +30 -30
  47. package/dist/components/atoms/Icon/icons/MautoucoLogo.d.ts +8 -8
  48. package/dist/components/atoms/Icon/icons/MautoucoLogo.js +37 -37
  49. package/dist/components/atoms/Icon/icons/MenuIcon.d.ts +8 -8
  50. package/dist/components/atoms/Icon/icons/MenuIcon.js +37 -37
  51. package/dist/components/atoms/Icon/icons/MinusIcon.d.ts +8 -8
  52. package/dist/components/atoms/Icon/icons/MinusIcon.js +25 -25
  53. package/dist/components/atoms/Icon/icons/MoreIcon.d.ts +8 -8
  54. package/dist/components/atoms/Icon/icons/MoreIcon.js +30 -30
  55. package/dist/components/atoms/Icon/icons/PlaneIcon.d.ts +8 -8
  56. package/dist/components/atoms/Icon/icons/PlaneIcon.js +36 -36
  57. package/dist/components/atoms/Icon/icons/PlusIcon.d.ts +8 -8
  58. package/dist/components/atoms/Icon/icons/PlusIcon.js +25 -25
  59. package/dist/components/atoms/Icon/icons/Search.d.ts +8 -8
  60. package/dist/components/atoms/Icon/icons/Search.js +30 -30
  61. package/dist/components/atoms/Icon/icons/Settings.d.ts +8 -8
  62. package/dist/components/atoms/Icon/icons/Settings.js +30 -30
  63. package/dist/components/atoms/Icon/icons/ShipIcon.d.ts +8 -8
  64. package/dist/components/atoms/Icon/icons/ShipIcon.js +36 -36
  65. package/dist/components/atoms/Icon/icons/StrollerIcon.d.ts +8 -8
  66. package/dist/components/atoms/Icon/icons/StrollerIcon.js +30 -30
  67. package/dist/components/atoms/Icon/icons/TwitterIcon.d.ts +8 -8
  68. package/dist/components/atoms/Icon/icons/TwitterIcon.js +36 -36
  69. package/dist/components/atoms/Icon/icons/User.d.ts +8 -8
  70. package/dist/components/atoms/Icon/icons/User.js +30 -30
  71. package/dist/components/atoms/Icon/icons/UserIcon.d.ts +12 -12
  72. package/dist/components/atoms/Icon/icons/UserIcon.js +41 -41
  73. package/dist/components/atoms/Icon/icons/Youtube.d.ts +8 -8
  74. package/dist/components/atoms/Icon/icons/Youtube.js +36 -36
  75. package/dist/components/atoms/Illustration/Illustration.d.ts +14 -14
  76. package/dist/components/atoms/Illustration/Illustration.js +33 -33
  77. package/dist/components/atoms/Illustration/illustrations.d.ts +51 -51
  78. package/dist/components/atoms/Illustration/illustrations.js +97 -97
  79. package/dist/components/atoms/Inputs/DropdownInput/DropdownInput.d.ts +12 -12
  80. package/dist/components/atoms/Inputs/DropdownInput/DropdownInput.js +53 -53
  81. package/dist/components/atoms/Inputs/Input/Input.d.ts +15 -15
  82. package/dist/components/atoms/Inputs/Input/Input.js +27 -27
  83. package/dist/components/atoms/Inputs/Textarea/Textarea.d.ts +14 -14
  84. package/dist/components/atoms/Inputs/Textarea/Textarea.js +15 -15
  85. package/dist/components/atoms/Link/Link.d.ts +44 -44
  86. package/dist/components/atoms/Link/Link.js +76 -76
  87. package/dist/components/atoms/RatingStar/RatingStar.d.ts +40 -40
  88. package/dist/components/atoms/RatingStar/RatingStar.js +54 -54
  89. package/dist/components/atoms/SegmentedButton/SegmentedButton.d.ts +27 -27
  90. package/dist/components/atoms/SegmentedButton/SegmentedButton.js +49 -49
  91. package/dist/components/atoms/SelectedValue/SelectedValue.d.ts +11 -11
  92. package/dist/components/atoms/SelectedValue/SelectedValue.js +29 -29
  93. package/dist/components/atoms/Slider/Slider.d.ts +52 -52
  94. package/dist/components/atoms/Slider/Slider.js +30 -30
  95. package/dist/components/atoms/Spinner/Spinner.d.ts +9 -9
  96. package/dist/components/atoms/Spinner/Spinner.js +38 -38
  97. package/dist/components/atoms/Spinner/variants/ButtonSpinner.d.ts +8 -8
  98. package/dist/components/atoms/Spinner/variants/ButtonSpinner.js +19 -19
  99. package/dist/components/atoms/Spinner/variants/LoadingSpinner.d.ts +7 -7
  100. package/dist/components/atoms/Spinner/variants/LoadingSpinner.js +7 -7
  101. package/dist/components/atoms/Tab/Tab.css +266 -266
  102. package/dist/components/atoms/Tab/Tab.d.ts +22 -22
  103. package/dist/components/atoms/Tab/Tab.js +54 -54
  104. package/dist/components/atoms/Typography/Typography.d.ts +24 -24
  105. package/dist/components/atoms/Typography/Typography.js +100 -100
  106. package/dist/components/molecules/Calendar/CalendarInput.d.ts +34 -34
  107. package/dist/components/molecules/Calendar/CalendarInput.js +49 -49
  108. package/dist/components/molecules/Calendar/DateTime.d.ts +25 -25
  109. package/dist/components/molecules/Calendar/DateTime.js +106 -106
  110. package/dist/components/molecules/Calendar/TimePicker.d.ts +16 -16
  111. package/dist/components/molecules/Calendar/TimePicker.js +91 -91
  112. package/dist/components/molecules/LocationDropdown/LocationDropdown.d.ts +34 -34
  113. package/dist/components/molecules/LocationDropdown/LocationDropdown.js +120 -120
  114. package/dist/components/molecules/LocationDropdown/index.d.ts +2 -2
  115. package/dist/components/molecules/LocationDropdown/index.js +1 -1
  116. package/dist/components/molecules/MultiSelectDropdown/MultiSelectDropdown.d.ts +29 -29
  117. package/dist/components/molecules/MultiSelectDropdown/MultiSelectDropdown.js +106 -106
  118. package/dist/components/molecules/RatingTab/RatingTab.d.ts +39 -39
  119. package/dist/components/molecules/RatingTab/RatingTab.js +41 -41
  120. package/dist/components/molecules/TabGroup/TabGroup.d.ts +17 -17
  121. package/dist/components/molecules/TabGroup/TabGroup.js +30 -30
  122. package/dist/components/molecules/UserCard/UserCard.d.ts +20 -20
  123. package/dist/components/molecules/UserCard/UserCard.js +57 -57
  124. package/dist/components/organisms/CardContainer/CardContainer.d.ts +37 -37
  125. package/dist/components/organisms/CardContainer/CardContainer.js +27 -27
  126. package/dist/components/organisms/DateTimePicker/DateTimePicker.d.ts +15 -15
  127. package/dist/components/organisms/DateTimePicker/DateTimePicker.js +66 -66
  128. package/dist/components/organisms/Dialog/Dialog.d.ts +103 -103
  129. package/dist/components/organisms/Dialog/Dialog.js +162 -162
  130. package/dist/components/organisms/Footer/Footer.d.ts +38 -38
  131. package/dist/components/organisms/Footer/Footer.js +74 -74
  132. package/dist/components/organisms/PaxSelector/PaxSelector.d.ts +63 -63
  133. package/dist/components/organisms/PaxSelector/PaxSelector.js +402 -402
  134. package/dist/components/organisms/RoundTrip/RoundTrip.d.ts +54 -54
  135. package/dist/components/organisms/RoundTrip/RoundTrip.js +179 -179
  136. package/dist/components/organisms/RoundTrip/index.d.ts +2 -2
  137. package/dist/components/organisms/RoundTrip/index.js +1 -1
  138. package/dist/components/organisms/SearchBarTransfer/SearchBarTransfer.d.ts +35 -35
  139. package/dist/components/organisms/SearchBarTransfer/SearchBarTransfer.js +192 -192
  140. package/dist/components/organisms/SearchBarTransfer/index.d.ts +2 -2
  141. package/dist/components/organisms/SearchBarTransfer/index.js +1 -1
  142. package/dist/components/organisms/TopNavigation/DesktopNav.d.ts +33 -33
  143. package/dist/components/organisms/TopNavigation/DesktopNav.js +32 -32
  144. package/dist/components/organisms/TopNavigation/MobileNav.d.ts +32 -32
  145. package/dist/components/organisms/TopNavigation/MobileNav.js +45 -45
  146. package/dist/components/organisms/TopNavigation/TopNavigation.d.ts +33 -33
  147. package/dist/components/organisms/TopNavigation/TopNavigation.js +20 -20
  148. package/dist/components/organisms/TransferLine/TransferLine.d.ts +53 -53
  149. package/dist/components/organisms/TransferLine/TransferLine.js +179 -179
  150. package/dist/components/ui/button.d.ts +10 -10
  151. package/dist/components/ui/button.js +56 -56
  152. package/dist/components/ui/calendar.d.ts +8 -8
  153. package/dist/components/ui/calendar.js +87 -87
  154. package/dist/components/ui/popover.d.ts +7 -7
  155. package/dist/components/ui/popover.js +42 -42
  156. package/dist/hooks/useMobile.d.ts +5 -5
  157. package/dist/hooks/useMobile.js +26 -26
  158. package/dist/index.d.ts +49 -49
  159. package/dist/index.js +46 -46
  160. package/dist/lib/utils.d.ts +7 -7
  161. package/dist/lib/utils.js +13 -13
  162. package/dist/styles/components/avatar.css +122 -122
  163. package/dist/styles/components/calendar.css +140 -140
  164. package/dist/styles/components/checkbox.css +206 -206
  165. package/dist/styles/components/dropdown.css +269 -269
  166. package/dist/styles/components/forms.css +209 -209
  167. package/dist/styles/components/illustration.css +123 -123
  168. package/dist/styles/components/molecule/calendarInput.css +133 -133
  169. package/dist/styles/components/molecule/dateTime.css +126 -126
  170. package/dist/styles/components/molecule/location-dropdown.css +132 -132
  171. package/dist/styles/components/molecule/timePicker.css +122 -122
  172. package/dist/styles/components/multiselect-dropdown.css +286 -286
  173. package/dist/styles/components/organism/card-container.css +148 -148
  174. package/dist/styles/components/organism/dialog.css +168 -168
  175. package/dist/styles/components/organism/footer.css +119 -119
  176. package/dist/styles/components/organism/pax-selector.css +617 -617
  177. package/dist/styles/components/organism/round-trip.css +139 -139
  178. package/dist/styles/components/organism/search-bar-transfer.css +158 -161
  179. package/dist/styles/components/organism/topnavigation.css +142 -142
  180. package/dist/styles/components/organism/transfer-line.css +138 -138
  181. package/dist/styles/components/rating-star.css +145 -145
  182. package/dist/styles/components/rating-tab.css +179 -179
  183. package/dist/styles/components/scrollbar.css +155 -155
  184. package/dist/styles/components/segmented-button.css +214 -214
  185. package/dist/styles/components/selected-value.css +175 -175
  186. package/dist/styles/components/slider.css +182 -182
  187. package/dist/styles/components/typography.css +245 -245
  188. package/dist/styles/tokens/tokens.css +119 -119
  189. package/dist/styles/tokens/tokens.d.ts +3108 -3108
  190. package/dist/styles/tokens/tokens.js +2652 -2652
  191. package/package.json +103 -103
  192. package/src/components/atoms/Avatar/Avatar.tsx +60 -60
  193. package/src/components/atoms/Button/Button.css +200 -200
  194. package/src/components/atoms/Button/Button.tsx +82 -82
  195. package/src/components/atoms/Checkbox/Checkbox.tsx +83 -83
  196. package/src/components/atoms/Icon/Icon.tsx +163 -163
  197. package/src/components/atoms/Icon/icons/ArrivalIcon.tsx +52 -52
  198. package/src/components/atoms/Icon/icons/BuildingIcon.tsx +50 -50
  199. package/src/components/atoms/Icon/icons/CalendarIcon.tsx +63 -63
  200. package/src/components/atoms/Icon/icons/CalendarOutlineIcon.tsx +50 -50
  201. package/src/components/atoms/Icon/icons/CarIcon.tsx +44 -44
  202. package/src/components/atoms/Icon/icons/Check.tsx +36 -36
  203. package/src/components/atoms/Icon/icons/CheckCircleIcon.tsx +48 -48
  204. package/src/components/atoms/Icon/icons/Chevron.tsx +73 -73
  205. package/src/components/atoms/Icon/icons/ChevronDownIcon.tsx +46 -46
  206. package/src/components/atoms/Icon/icons/Close.tsx +39 -39
  207. package/src/components/atoms/Icon/icons/DeleteIcon.tsx +44 -44
  208. package/src/components/atoms/Icon/icons/DepartureIcon.tsx +50 -50
  209. package/src/components/atoms/Icon/icons/EyeIcon.tsx +44 -44
  210. package/src/components/atoms/Icon/icons/FacebookIcon.tsx +50 -50
  211. package/src/components/atoms/Icon/icons/HomeIcon.tsx +52 -52
  212. package/src/components/atoms/Icon/icons/InfoIcon.tsx +44 -44
  213. package/src/components/atoms/Icon/icons/LinkedInIcon.tsx +50 -50
  214. package/src/components/atoms/Icon/icons/MapPinIcon.tsx +44 -44
  215. package/src/components/atoms/Icon/icons/MautoucoLogo.tsx +93 -93
  216. package/src/components/atoms/Icon/icons/MenuIcon.tsx +49 -49
  217. package/src/components/atoms/Icon/icons/MinusIcon.tsx +45 -45
  218. package/src/components/atoms/Icon/icons/MoreIcon.tsx +44 -44
  219. package/src/components/atoms/Icon/icons/PlaneIcon.tsx +50 -50
  220. package/src/components/atoms/Icon/icons/PlusIcon.tsx +45 -45
  221. package/src/components/atoms/Icon/icons/Search.tsx +37 -37
  222. package/src/components/atoms/Icon/icons/Settings.tsx +38 -38
  223. package/src/components/atoms/Icon/icons/ShipIcon.tsx +50 -50
  224. package/src/components/atoms/Icon/icons/StrollerIcon.tsx +44 -44
  225. package/src/components/atoms/Icon/icons/TwitterIcon.tsx +50 -50
  226. package/src/components/atoms/Icon/icons/User.tsx +37 -37
  227. package/src/components/atoms/Icon/icons/UserIcon.tsx +63 -63
  228. package/src/components/atoms/Icon/icons/Youtube.tsx +50 -50
  229. package/src/components/atoms/Illustration/Illustration.tsx +28 -28
  230. package/src/components/atoms/Illustration/illustrations.ts +116 -116
  231. package/src/components/atoms/Inputs/DropdownInput/DropdownInput.tsx +96 -96
  232. package/src/components/atoms/Inputs/Textarea/Textarea.tsx +51 -51
  233. package/src/components/atoms/Link/Link.tsx +168 -168
  234. package/src/components/atoms/RatingStar/RatingStar.tsx +114 -114
  235. package/src/components/atoms/SegmentedButton/SegmentedButton.tsx +94 -94
  236. package/src/components/atoms/SelectedValue/SelectedValue.tsx +59 -59
  237. package/src/components/atoms/Slider/Slider.tsx +95 -95
  238. package/src/components/atoms/Spinner/Spinner.tsx +56 -56
  239. package/src/components/atoms/Spinner/variants/ButtonSpinner.tsx +37 -37
  240. package/src/components/atoms/Spinner/variants/LoadingSpinner.tsx +22 -22
  241. package/src/components/atoms/Tab/Tab.css +147 -147
  242. package/src/components/atoms/Tab/Tab.tsx +96 -96
  243. package/src/components/atoms/Typography/Typography.tsx +153 -153
  244. package/src/components/molecules/Calendar/CalendarInput.tsx +135 -135
  245. package/src/components/molecules/Calendar/DateTime.tsx +172 -172
  246. package/src/components/molecules/Calendar/TimePicker.tsx +174 -174
  247. package/src/components/molecules/LocationDropdown/LocationDropdown.tsx +234 -234
  248. package/src/components/molecules/LocationDropdown/index.ts +2 -2
  249. package/src/components/molecules/RatingTab/RatingTab.tsx +96 -96
  250. package/src/components/molecules/TabGroup/TabGroup.tsx +60 -60
  251. package/src/components/molecules/UserCard/UserCard.stories.tsx +36 -36
  252. package/src/components/molecules/UserCard/UserCard.tsx +173 -173
  253. package/src/components/organisms/CardContainer/CardContainer.tsx +66 -66
  254. package/src/components/organisms/DateTimePicker/DateTimePicker.tsx +110 -110
  255. package/src/components/organisms/Dialog/Dialog.tsx +352 -352
  256. package/src/components/organisms/Footer/Footer.tsx +290 -290
  257. package/src/components/organisms/PaxSelector/PaxSelector.tsx +979 -979
  258. package/src/components/organisms/RoundTrip/RoundTrip.tsx +335 -335
  259. package/src/components/organisms/RoundTrip/index.ts +2 -2
  260. package/src/components/organisms/SearchBarTransfer/SearchBarTransfer.tsx +388 -388
  261. package/src/components/organisms/SearchBarTransfer/index.ts +2 -2
  262. package/src/components/organisms/TopNavigation/DesktopNav.tsx +133 -133
  263. package/src/components/organisms/TopNavigation/MobileNav.tsx +212 -212
  264. package/src/components/organisms/TopNavigation/TopNavigation.tsx +45 -45
  265. package/src/components/organisms/TransferLine/TransferLine.tsx +369 -369
  266. package/src/components/ui/button.tsx +60 -60
  267. package/src/components/ui/calendar.tsx +246 -246
  268. package/src/components/ui/popover.tsx +46 -46
  269. package/src/styles/components/avatar.css +58 -58
  270. package/src/styles/components/calendar.css +85 -85
  271. package/src/styles/components/checkbox.css +130 -130
  272. package/src/styles/components/dropdown.css +214 -214
  273. package/src/styles/components/forms.css +147 -147
  274. package/src/styles/components/illustration.css +7 -7
  275. package/src/styles/components/molecule/calendarInput.css +156 -156
  276. package/src/styles/components/molecule/dateTime.css +14 -14
  277. package/src/styles/components/molecule/location-dropdown.css +204 -204
  278. package/src/styles/components/molecule/timePicker.css +78 -78
  279. package/src/styles/components/multiselect-dropdown.css +230 -230
  280. package/src/styles/components/organism/card-container.css +49 -49
  281. package/src/styles/components/organism/dialog.css +241 -241
  282. package/src/styles/components/organism/footer.css +113 -113
  283. package/src/styles/components/organism/pax-selector.css +702 -702
  284. package/src/styles/components/organism/round-trip.css +55 -55
  285. package/src/styles/components/organism/search-bar-transfer.css +128 -127
  286. package/src/styles/components/organism/topnavigation.css +161 -161
  287. package/src/styles/components/organism/transfer-line.css +86 -86
  288. package/src/styles/components/rating-star.css +39 -39
  289. package/src/styles/components/rating-tab.css +83 -83
  290. package/src/styles/components/scrollbar.css +63 -63
  291. package/src/styles/components/segmented-button.css +134 -134
  292. package/src/styles/components/selected-value.css +80 -80
  293. package/src/styles/components/slider.css +86 -86
  294. package/src/styles/components/typography.css +251 -251
  295. package/src/styles/fonts.css +50 -0
  296. package/src/styles/tokens/tokens.css +119 -119
  297. package/src/styles/tokens/tokens.js +12 -6
@@ -1,979 +1,979 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import Icon from '../../atoms/Icon/Icon';
3
- import { Text } from '../../atoms/Typography/Typography';
4
-
5
- export type ClientType = 'Standard client' | 'VIP' | 'VVIP' | 'Honeymooners';
6
-
7
- /**
8
- * Individual passenger/pax data for a single room
9
- */
10
- export interface PaxData {
11
- adults: number;
12
- teens: number;
13
- children: number;
14
- infants: number;
15
- /** Ages for teens (1-18 years) */
16
- teenAges?: number[];
17
- /** Ages for children (1-18 years) */
18
- childAges?: number[];
19
- /** Ages for infants (1-18 years) */
20
- infantAges?: number[];
21
- clientType: ClientType;
22
- }
23
-
24
- /**
25
- * Room data for multiple room mode
26
- */
27
- export interface RoomData extends PaxData {
28
- roomId: string;
29
- }
30
-
31
- export interface PaxSelectorProps {
32
- /** Label for the selector */
33
- label?: string;
34
- /** Current pax data (single room mode) */
35
- value?: PaxData;
36
- /** Callback when pax data changes (single room mode) */
37
- onChange?: (data: PaxData) => void;
38
- /** Callback when "Add a room" is clicked */
39
- onAddRoom?: () => void;
40
- /** Callback when Done button is clicked */
41
- onDone?: (data: PaxData | RoomData[]) => void;
42
- /** Placeholder text */
43
- placeholder?: string;
44
- /** Additional CSS classes */
45
- className?: string;
46
- /** Maximum number of adults */
47
- maxAdults?: number;
48
- /** Maximum number of teens */
49
- maxTeens?: number;
50
- /** Maximum number of children */
51
- maxChildren?: number;
52
- /** Maximum number of infants */
53
- maxInfants?: number;
54
- /** Show add room link */
55
- showAddRoom?: boolean;
56
- /** Enable multiple room mode */
57
- multipleRooms?: boolean;
58
- /** Default rooms for multiple room mode */
59
- defaultRooms?: RoomData[];
60
- /** Callback when rooms change in multiple room mode */
61
- onRoomsChange?: (rooms: RoomData[]) => void;
62
- /** Callback when a room is removed */
63
- onRemoveRoom?: (roomId: string) => void;
64
- /** Default pax data for single room mode (will trigger onChange on mount) */
65
- defaultPaxData?: PaxData;
66
- }
67
-
68
- const DEFAULT_PAX_DATA: PaxData = {
69
- adults: 0,
70
- teens: 0,
71
- children: 0,
72
- infants: 0,
73
- teenAges: [],
74
- childAges: [],
75
- infantAges: [],
76
- clientType: 'Standard client',
77
- };
78
-
79
- export const DEFAULT_PAX_DATA_WITH_ADULTS: PaxData = {
80
- adults: 2,
81
- teens: 0,
82
- children: 0,
83
- infants: 0,
84
- teenAges: [],
85
- childAges: [],
86
- infantAges: [],
87
- clientType: 'Standard client',
88
- };
89
-
90
- const CLIENT_TYPES: ClientType[] = ['Standard client', 'VIP', 'VVIP', 'Honeymooners'];
91
-
92
- // Age range for all child categories (teens, children, infants)
93
- const CHILD_CATEGORY_AGES = Array.from({ length: 18 }, (_, i) => i + 1); // 1-18 years
94
-
95
- interface AgeSelectorProps {
96
- label: string;
97
- value: number | undefined;
98
- onChange: (age: number) => void;
99
- ageRange: number[];
100
- required?: boolean;
101
- }
102
-
103
- const AgeSelector: React.FC<AgeSelectorProps> = ({ label, value, onChange, ageRange, required }) => {
104
- const [isOpen, setIsOpen] = useState(false);
105
- const [inputValue, setInputValue] = useState<string>(value !== undefined ? value.toString() : '');
106
- const containerRef = useRef<HTMLDivElement>(null);
107
- const inputRef = useRef<HTMLInputElement>(null);
108
-
109
- // Sync input value when prop value changes
110
- useEffect(() => {
111
- setInputValue(value !== undefined ? value.toString() : '');
112
- }, [value]);
113
-
114
- useEffect(() => {
115
- const handleClickOutside = (event: MouseEvent) => {
116
- if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
117
- setIsOpen(false);
118
- }
119
- };
120
-
121
- if (isOpen) {
122
- document.addEventListener('mousedown', handleClickOutside);
123
- }
124
-
125
- return () => {
126
- document.removeEventListener('mousedown', handleClickOutside);
127
- };
128
- }, [isOpen]);
129
-
130
- const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
131
- const newValue = e.target.value;
132
-
133
- // Only allow numeric input or empty string
134
- if (newValue === '' || /^\d+$/.test(newValue)) {
135
- setInputValue(newValue);
136
-
137
- // Only update if it's a valid number within range
138
- const numValue = parseInt(newValue, 10);
139
- if (newValue === '') {
140
- // Allow empty input - don't update onChange
141
- return;
142
- } else if (!isNaN(numValue) && ageRange.includes(numValue)) {
143
- onChange(numValue);
144
- }
145
- }
146
- };
147
-
148
- const handleInputBlur = () => {
149
- // Validate and set to valid value or clear if invalid
150
- const numValue = parseInt(inputValue, 10);
151
- if (inputValue === '') {
152
- // Keep empty if user cleared it
153
- return;
154
- } else if (isNaN(numValue) || !ageRange.includes(numValue)) {
155
- // Reset to current value if invalid
156
- setInputValue(value !== undefined ? value.toString() : '');
157
- } else {
158
- // Ensure the input value matches the validated value
159
- setInputValue(numValue.toString());
160
- }
161
- };
162
-
163
- const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
164
- if (e.key === 'Enter') {
165
- e.preventDefault();
166
- inputRef.current?.blur();
167
- }
168
- };
169
-
170
- const handleSelect = (age: string) => {
171
- const numAge = parseInt(age, 10);
172
- onChange(numAge);
173
- setIsOpen(false);
174
- setInputValue(age);
175
- };
176
-
177
- const handleDropdownToggle = () => {
178
- setIsOpen(!isOpen);
179
- };
180
-
181
- const ageOptions = ageRange.map(age => age.toString());
182
- const displayValue = inputValue || undefined;
183
-
184
- return (
185
- <div className="pax-selector__age-selector" ref={containerRef}>
186
- <Text size="sm" variant="regular" className="pax-selector__age-label">
187
- {label}
188
- {required && <span className="pax-selector__age-required"> *</span>}
189
- </Text>
190
- <div className="dropdown-container pax-selector__age-dropdown-container">
191
- <div className={`dropdown-input ${isOpen ? 'dropdown-input--open pax-selector__age-dropdown-input--open' : ''} ${value !== undefined ? 'dropdown-input--selected' : 'dropdown-input--default'} pax-selector__age-dropdown-input`}>
192
- <input
193
- ref={inputRef}
194
- type="text"
195
- inputMode="numeric"
196
- className="dropdown-input__text pax-selector__age-input-text"
197
- value={inputValue}
198
- onChange={handleInputChange}
199
- onBlur={handleInputBlur}
200
- onKeyDown={handleInputKeyDown}
201
- onFocus={() => setIsOpen(false)}
202
- placeholder="--"
203
- aria-label={`${label} age`}
204
- style={{
205
- background: 'transparent',
206
- border: 'none',
207
- outline: 'none',
208
- width: '100%',
209
- cursor: 'text'
210
- }}
211
- />
212
- <button
213
- type="button"
214
- className="pax-selector__age-dropdown-btn"
215
- onClick={(e) => {
216
- e.preventDefault();
217
- e.stopPropagation();
218
- handleDropdownToggle();
219
- }}
220
- aria-expanded={isOpen}
221
- aria-haspopup="listbox"
222
- aria-label="Open age dropdown"
223
- style={{
224
- background: 'transparent',
225
- border: 'none',
226
- cursor: 'pointer',
227
- padding: 0,
228
- display: 'flex',
229
- alignItems: 'center'
230
- }}
231
- >
232
- <Icon
233
- name="chevron-down"
234
- size="sm"
235
- className="dropdown-input__icon dropdown-input__icon--chevron"
236
- />
237
- </button>
238
- </div>
239
- {isOpen && (
240
- <div className="dropdown-menu" role="listbox">
241
- {ageOptions.map((age) => (
242
- <div
243
- key={age}
244
- className={`dropdown-option ${value?.toString() === age ? 'dropdown-option--selected' : ''}`}
245
- onClick={() => handleSelect(age)}
246
- role="option"
247
- aria-selected={value?.toString() === age}
248
- >
249
- {age}
250
- </div>
251
- ))}
252
- </div>
253
- )}
254
- </div>
255
- </div>
256
- );
257
- };
258
-
259
- interface StepperRowProps {
260
- label: string;
261
- value: number;
262
- min?: number;
263
- max?: number;
264
- onChange: (value: number) => void;
265
- }
266
-
267
- const StepperRow: React.FC<StepperRowProps> = ({
268
- label,
269
- value,
270
- min = 0,
271
- max = 99,
272
- onChange
273
- }) => {
274
- const handleDecrement = () => {
275
- if (value > min) {
276
- onChange(value - 1);
277
- }
278
- };
279
-
280
- const handleIncrement = () => {
281
- if (value < max) {
282
- onChange(value + 1);
283
- }
284
- };
285
-
286
- return (
287
- <div className="pax-selector__stepper">
288
- <Text size="base" variant="bold" className="pax-selector__stepper-label">
289
- {label}
290
- </Text>
291
- <div className="pax-selector__stepper-controls">
292
- <button
293
- type="button"
294
- className="pax-selector__stepper-btn"
295
- onClick={handleDecrement}
296
- disabled={value <= min}
297
- aria-label={`Decrease ${label}`}
298
- >
299
- <Icon name="minus" size="sm" />
300
- </button>
301
- <span className="pax-selector__stepper-count">{value}</span>
302
- <button
303
- type="button"
304
- className="pax-selector__stepper-btn"
305
- onClick={handleIncrement}
306
- disabled={value >= max}
307
- aria-label={`Increase ${label}`}
308
- >
309
- <Icon name="plus" size="sm" />
310
- </button>
311
- </div>
312
- </div>
313
- );
314
- };
315
-
316
- interface ClientTypeSelectorProps {
317
- value: ClientType;
318
- onChange: (value: ClientType) => void;
319
- }
320
-
321
- const ClientTypeSelector: React.FC<ClientTypeSelectorProps> = ({ value, onChange }) => {
322
- const [isOpen, setIsOpen] = useState(false);
323
- const containerRef = useRef<HTMLDivElement>(null);
324
-
325
- useEffect(() => {
326
- const handleClickOutside = (event: MouseEvent) => {
327
- if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
328
- setIsOpen(false);
329
- }
330
- };
331
-
332
- if (isOpen) {
333
- document.addEventListener('mousedown', handleClickOutside);
334
- }
335
-
336
- return () => {
337
- document.removeEventListener('mousedown', handleClickOutside);
338
- };
339
- }, [isOpen]);
340
-
341
- const handleSelect = (type: ClientType) => {
342
- onChange(type);
343
- setIsOpen(false);
344
- };
345
-
346
- return (
347
- <div className="pax-selector__client-type">
348
- <Text size="sm" variant="regular" className="pax-selector__client-type-label">
349
- Client type
350
- </Text>
351
- <div className="pax-selector__client-type-select" ref={containerRef}>
352
- <button
353
- type="button"
354
- className="pax-selector__client-type-trigger"
355
- onClick={() => setIsOpen(!isOpen)}
356
- aria-expanded={isOpen}
357
- aria-haspopup="listbox"
358
- >
359
- <div className="pax-selector__client-type-content">
360
- <Icon name="user-icon" size="sm" className="pax-selector__client-type-icon" />
361
- <span className="pax-selector__client-type-text">{value}</span>
362
- </div>
363
- <Icon
364
- name="chevron-down"
365
- size="sm"
366
- className={`pax-selector__client-type-chevron ${isOpen ? 'pax-selector__client-type-chevron--open' : ''}`}
367
- />
368
- </button>
369
-
370
- {isOpen && (
371
- <div className="pax-selector__client-type-dropdown" role="listbox">
372
- {CLIENT_TYPES.map((type) => (
373
- <button
374
- key={type}
375
- type="button"
376
- className={`pax-selector__client-type-option ${value === type ? 'pax-selector__client-type-option--selected' : ''}`}
377
- onClick={() => handleSelect(type)}
378
- role="option"
379
- aria-selected={value === type}
380
- >
381
- <Icon name="user-icon" size="sm" />
382
- {type}
383
- </button>
384
- ))}
385
- </div>
386
- )}
387
- </div>
388
- </div>
389
- );
390
- };
391
-
392
- interface RoomEditorProps {
393
- room: RoomData;
394
- roomNumber: number;
395
- showRemove: boolean;
396
- maxAdults: number;
397
- maxTeens: number;
398
- maxChildren: number;
399
- maxInfants: number;
400
- onChange: (room: RoomData) => void;
401
- onRemove: () => void;
402
- }
403
-
404
- const RoomEditor: React.FC<RoomEditorProps> = ({
405
- room,
406
- roomNumber,
407
- showRemove,
408
- maxAdults,
409
- maxTeens,
410
- maxChildren,
411
- maxInfants,
412
- onChange,
413
- onRemove,
414
- }) => {
415
- const handleFieldChange = (field: keyof PaxData, value: number | ClientType | number[]) => {
416
- onChange({ ...room, [field]: value });
417
- };
418
-
419
- const handleAgeChange = (category: 'teenAges' | 'childAges' | 'infantAges', index: number, age: number) => {
420
- const ages = [...(room[category] || [])];
421
- ages[index] = age;
422
- handleFieldChange(category, ages);
423
- };
424
-
425
- // Generate age arrays based on counts
426
- useEffect(() => {
427
- const teenAges = room.teenAges || [];
428
- const childAges = room.childAges || [];
429
- const infantAges = room.infantAges || [];
430
-
431
- // Adjust teen ages array
432
- if (teenAges.length < room.teens) {
433
- handleFieldChange('teenAges', [...teenAges, ...Array(room.teens - teenAges.length).fill(undefined)]);
434
- } else if (teenAges.length > room.teens) {
435
- handleFieldChange('teenAges', teenAges.slice(0, room.teens));
436
- }
437
-
438
- // Adjust child ages array
439
- if (childAges.length < room.children) {
440
- handleFieldChange('childAges', [...childAges, ...Array(room.children - childAges.length).fill(undefined)]);
441
- } else if (childAges.length > room.children) {
442
- handleFieldChange('childAges', childAges.slice(0, room.children));
443
- }
444
-
445
- // Adjust infant ages array
446
- if (infantAges.length < room.infants) {
447
- handleFieldChange('infantAges', [...infantAges, ...Array(room.infants - infantAges.length).fill(undefined)]);
448
- } else if (infantAges.length > room.infants) {
449
- handleFieldChange('infantAges', infantAges.slice(0, room.infants));
450
- }
451
- }, [room.teens, room.children, room.infants]);
452
-
453
- // Chunk ages into groups of 2 for layout
454
- const chunkAges = (ages: (number | undefined)[], category: string) => {
455
- const chunks: (number | undefined)[][] = [];
456
- for (let i = 0; i < ages.length; i += 2) {
457
- chunks.push(ages.slice(i, i + 2));
458
- }
459
- return chunks;
460
- };
461
-
462
- const teenAgeChunks = chunkAges(room.teenAges || [], 'Teen');
463
- const childAgeChunks = chunkAges(room.childAges || [], 'Child');
464
- const infantAgeChunks = chunkAges(room.infantAges || [], 'Infant');
465
-
466
- return (
467
- <div className="pax-selector__room-container">
468
- <div className="pax-selector__room-header">
469
- <div className="pax-selector__room-title">
470
- <Text size="lg" variant="bold" className="pax-selector__room-name">
471
- Room {roomNumber}
472
- </Text>
473
- <Icon name="home" size="md" className="pax-selector__room-icon" />
474
- </div>
475
- {showRemove && (
476
- <button
477
- type="button"
478
- className="pax-selector__room-remove"
479
- onClick={onRemove}
480
- aria-label={`Remove room ${roomNumber}`}
481
- >
482
- <Icon name="close" size="sm" className="pax-selector__room-remove-icon" />
483
- <span className="pax-selector__room-remove-text">Remove</span>
484
- </button>
485
- )}
486
- </div>
487
-
488
- <div className="pax-selector__room-content">
489
- <div className="pax-selector__steppers">
490
- <StepperRow
491
- label="Adult"
492
- value={room.adults}
493
- max={maxAdults}
494
- onChange={(val) => handleFieldChange('adults', val)}
495
- />
496
- <StepperRow
497
- label="Teen"
498
- value={room.teens}
499
- max={maxTeens}
500
- onChange={(val) => handleFieldChange('teens', val)}
501
- />
502
- <StepperRow
503
- label="Child"
504
- value={room.children}
505
- max={maxChildren}
506
- onChange={(val) => handleFieldChange('children', val)}
507
- />
508
- <StepperRow
509
- label="Infant"
510
- value={room.infants}
511
- max={maxInfants}
512
- onChange={(val) => handleFieldChange('infants', val)}
513
- />
514
- </div>
515
-
516
- {/* Age specification section */}
517
- {((room.teens > 0) || (room.children > 0) || (room.infants > 0)) && (
518
- <div className="pax-selector__age-section">
519
- <Text size="base" variant="bold" className="pax-selector__age-section-title">
520
- Please specify the age :
521
- </Text>
522
-
523
- <div className="pax-selector__age-groups">
524
- {/* Teen ages */}
525
- {room.teens > 0 && teenAgeChunks.map((chunk, chunkIndex) => (
526
- <div key={`teen-chunk-${chunkIndex}`} className="pax-selector__age-row">
527
- {chunk.map((age, ageIndex) => {
528
- const actualIndex = chunkIndex * 2 + ageIndex;
529
- return (
530
- <AgeSelector
531
- key={`teen-${actualIndex}`}
532
- label="Teen"
533
- value={age}
534
- onChange={(selectedAge) => handleAgeChange('teenAges', actualIndex, selectedAge)}
535
- ageRange={CHILD_CATEGORY_AGES}
536
- required
537
- />
538
- );
539
- })}
540
- </div>
541
- ))}
542
-
543
- {/* Child ages */}
544
- {room.children > 0 && childAgeChunks.map((chunk, chunkIndex) => (
545
- <div key={`child-chunk-${chunkIndex}`} className="pax-selector__age-row">
546
- {chunk.map((age, ageIndex) => {
547
- const actualIndex = chunkIndex * 2 + ageIndex;
548
- return (
549
- <AgeSelector
550
- key={`child-${actualIndex}`}
551
- label="Child"
552
- value={age}
553
- onChange={(selectedAge) => handleAgeChange('childAges', actualIndex, selectedAge)}
554
- ageRange={CHILD_CATEGORY_AGES}
555
- required
556
- />
557
- );
558
- })}
559
- </div>
560
- ))}
561
-
562
- {/* Infant ages */}
563
- {room.infants > 0 && infantAgeChunks.map((chunk, chunkIndex) => (
564
- <div key={`infant-chunk-${chunkIndex}`} className="pax-selector__age-row">
565
- {chunk.map((age, ageIndex) => {
566
- const actualIndex = chunkIndex * 2 + ageIndex;
567
- return (
568
- <AgeSelector
569
- key={`infant-${actualIndex}`}
570
- label="Infant"
571
- value={age}
572
- onChange={(selectedAge) => handleAgeChange('infantAges', actualIndex, selectedAge)}
573
- ageRange={CHILD_CATEGORY_AGES}
574
- required
575
- />
576
- );
577
- })}
578
- </div>
579
- ))}
580
- </div>
581
- </div>
582
- )}
583
-
584
- <ClientTypeSelector
585
- value={room.clientType}
586
- onChange={(val) => handleFieldChange('clientType', val)}
587
- />
588
- </div>
589
- </div>
590
- );
591
- };
592
-
593
- const PaxSelector: React.FC<PaxSelectorProps> = ({
594
- label = 'Number of pax',
595
- value,
596
- onChange,
597
- onAddRoom,
598
- onDone,
599
- placeholder = 'Select pax',
600
- className = '',
601
- maxAdults = 10,
602
- maxTeens = 10,
603
- maxChildren = 10,
604
- maxInfants = 10,
605
- showAddRoom = true,
606
- multipleRooms = false,
607
- defaultRooms,
608
- onRoomsChange,
609
- onRemoveRoom,
610
- defaultPaxData = DEFAULT_PAX_DATA_WITH_ADULTS,
611
- }) => {
612
- const [isOpen, setIsOpen] = useState(false);
613
- const [internalData, setInternalData] = useState<PaxData>(value || defaultPaxData || DEFAULT_PAX_DATA);
614
- const [rooms, setRooms] = useState<RoomData[]>(
615
- defaultRooms || [{ ...DEFAULT_PAX_DATA, roomId: '1' }]
616
- );
617
- const containerRef = useRef<HTMLDivElement>(null);
618
- const hasInitialized = useRef(false);
619
-
620
- // Sync internal data with external value prop
621
- useEffect(() => {
622
- if (value && !multipleRooms) {
623
- setInternalData(value);
624
- }
625
- }, [value, multipleRooms]);
626
-
627
- // Initialize with default pax data and trigger onChange on mount
628
- useEffect(() => {
629
- if (!hasInitialized.current && !value && defaultPaxData && !multipleRooms && onChange) {
630
- hasInitialized.current = true;
631
- onChange(defaultPaxData);
632
- }
633
- }, [defaultPaxData, value, multipleRooms, onChange]);
634
-
635
- // Manage age arrays in single mode when counts change
636
- useEffect(() => {
637
- if (!multipleRooms) {
638
- const teenAges = internalData.teenAges || [];
639
- const childAges = internalData.childAges || [];
640
- const infantAges = internalData.infantAges || [];
641
-
642
- let needsUpdate = false;
643
- const updatedData = { ...internalData };
644
-
645
- // Adjust teen ages array
646
- if (teenAges.length < internalData.teens) {
647
- updatedData.teenAges = [...teenAges, ...Array(internalData.teens - teenAges.length).fill(undefined)];
648
- needsUpdate = true;
649
- } else if (teenAges.length > internalData.teens) {
650
- updatedData.teenAges = teenAges.slice(0, internalData.teens);
651
- needsUpdate = true;
652
- }
653
-
654
- // Adjust child ages array
655
- if (childAges.length < internalData.children) {
656
- updatedData.childAges = [...childAges, ...Array(internalData.children - childAges.length).fill(undefined)];
657
- needsUpdate = true;
658
- } else if (childAges.length > internalData.children) {
659
- updatedData.childAges = childAges.slice(0, internalData.children);
660
- needsUpdate = true;
661
- }
662
-
663
- // Adjust infant ages array
664
- if (infantAges.length < internalData.infants) {
665
- updatedData.infantAges = [...infantAges, ...Array(internalData.infants - infantAges.length).fill(undefined)];
666
- needsUpdate = true;
667
- } else if (infantAges.length > internalData.infants) {
668
- updatedData.infantAges = infantAges.slice(0, internalData.infants);
669
- needsUpdate = true;
670
- }
671
-
672
- if (needsUpdate) {
673
- setInternalData(updatedData);
674
- onChange?.(updatedData);
675
- }
676
- }
677
- // eslint-disable-next-line react-hooks/exhaustive-deps
678
- }, [internalData.teens, internalData.children, internalData.infants, multipleRooms]);
679
-
680
- // Handle clicks outside the dropdown
681
- useEffect(() => {
682
- const handleClickOutside = (event: MouseEvent) => {
683
- if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
684
- setIsOpen(false);
685
- }
686
- };
687
-
688
- if (isOpen) {
689
- document.addEventListener('mousedown', handleClickOutside);
690
- }
691
-
692
- return () => {
693
- document.removeEventListener('mousedown', handleClickOutside);
694
- };
695
- }, [isOpen]);
696
-
697
- const handleDataChange = (field: keyof PaxData, newValue: number | ClientType | number[]) => {
698
- const newData = { ...internalData, [field]: newValue };
699
- setInternalData(newData);
700
- onChange?.(newData);
701
- };
702
-
703
- const handleClear = () => {
704
- if (multipleRooms) {
705
- const clearedRooms = rooms.map(room => ({ ...DEFAULT_PAX_DATA, roomId: room.roomId }));
706
- setRooms(clearedRooms);
707
- onRoomsChange?.(clearedRooms);
708
- } else {
709
- const clearedData = { ...DEFAULT_PAX_DATA };
710
- setInternalData(clearedData);
711
- onChange?.(clearedData);
712
- }
713
- };
714
-
715
- const handleDone = () => {
716
- setIsOpen(false);
717
- onDone?.(multipleRooms ? rooms : internalData);
718
- };
719
-
720
- const handleAddRoom = () => {
721
- const newRoom: RoomData = {
722
- ...DEFAULT_PAX_DATA,
723
- roomId: `${rooms.length + 1}`
724
- };
725
- const updatedRooms = [...rooms, newRoom];
726
- setRooms(updatedRooms);
727
- onRoomsChange?.(updatedRooms);
728
- onAddRoom?.();
729
- };
730
-
731
- const handleRemoveRoom = (roomId: string) => {
732
- const updatedRooms = rooms.filter(room => room.roomId !== roomId);
733
- setRooms(updatedRooms);
734
- onRoomsChange?.(updatedRooms);
735
- onRemoveRoom?.(roomId);
736
- };
737
-
738
- const handleRoomChange = (roomId: string, updatedRoom: RoomData) => {
739
- const updatedRooms = rooms.map(room =>
740
- room.roomId === roomId ? updatedRoom : room
741
- );
742
- setRooms(updatedRooms);
743
- onRoomsChange?.(updatedRooms);
744
- };
745
-
746
- const getTotalPax = () => {
747
- if (multipleRooms) {
748
- return rooms.reduce((total, room) =>
749
- total + room.adults + room.teens + room.children + room.infants, 0
750
- );
751
- }
752
- const { adults, teens, children, infants } = internalData;
753
- return adults + teens + children + infants;
754
- };
755
-
756
- const getDisplayText = () => {
757
- const total = getTotalPax();
758
- if (total === 0) {
759
- return placeholder;
760
- }
761
- return `${total} pax`;
762
- };
763
-
764
- const hasPax = getTotalPax() > 0;
765
-
766
- return (
767
- <div className={`pax-selector ${className}`} ref={containerRef}>
768
- <button
769
- type="button"
770
- className="pax-selector__trigger"
771
- onClick={() => setIsOpen(!isOpen)}
772
- aria-expanded={isOpen}
773
- aria-haspopup="true"
774
- >
775
- <Text size="sm" variant="regular" className="pax-selector__label">
776
- {label}
777
- </Text>
778
- <div className={`pax-selector__input ${isOpen ? 'pax-selector__input--active' : ''}`}>
779
- <div className="pax-selector__input-content">
780
- <Icon name="user-icon" size="sm" className="pax-selector__input-icon" />
781
- <span className={`pax-selector__input-text ${!hasPax ? 'pax-selector__input-placeholder' : ''}`}>
782
- {getDisplayText()}
783
- </span>
784
- </div>
785
- <Icon
786
- name="chevron-down"
787
- size="sm"
788
- className={`pax-selector__chevron ${isOpen ? 'pax-selector__chevron--open' : ''}`}
789
- />
790
- </div>
791
- </button>
792
-
793
- {isOpen && (
794
- <div className="pax-selector__dropdown">
795
- {/* Multiple Rooms Mode */}
796
- {multipleRooms ? (
797
- <>
798
- {showAddRoom && (
799
- <button
800
- type="button"
801
- className="pax-selector__add-room"
802
- onClick={handleAddRoom}
803
- >
804
- <Icon name="home" size="sm" className="pax-selector__add-room-icon" />
805
- Add a room
806
- <Icon name="plus" size="sm" className="pax-selector__add-room-icon" />
807
- </button>
808
- )}
809
-
810
- <div className="pax-selector__rooms">
811
- {rooms.map((room, index) => (
812
- <RoomEditor
813
- key={room.roomId}
814
- room={room}
815
- roomNumber={index + 1}
816
- showRemove={rooms.length > 1}
817
- maxAdults={maxAdults}
818
- maxTeens={maxTeens}
819
- maxChildren={maxChildren}
820
- maxInfants={maxInfants}
821
- onChange={(updatedRoom) => handleRoomChange(room.roomId, updatedRoom)}
822
- onRemove={() => handleRemoveRoom(room.roomId)}
823
- />
824
- ))}
825
- </div>
826
- </>
827
- ) : (
828
- /* Single Room Mode */
829
- <>
830
- <div className="pax-selector__steppers">
831
- <StepperRow
832
- label="Adult"
833
- value={internalData.adults}
834
- max={maxAdults}
835
- onChange={(val) => handleDataChange('adults', val)}
836
- />
837
- <StepperRow
838
- label="Teen"
839
- value={internalData.teens}
840
- max={maxTeens}
841
- onChange={(val) => handleDataChange('teens', val)}
842
- />
843
- <StepperRow
844
- label="Child"
845
- value={internalData.children}
846
- max={maxChildren}
847
- onChange={(val) => handleDataChange('children', val)}
848
- />
849
- <StepperRow
850
- label="Infant"
851
- value={internalData.infants}
852
- max={maxInfants}
853
- onChange={(val) => handleDataChange('infants', val)}
854
- />
855
- </div>
856
-
857
- {/* Age specification for single room */}
858
- {((internalData.teens > 0) || (internalData.children > 0) || (internalData.infants > 0)) && (
859
- <div className="pax-selector__age-section">
860
- <Text size="base" variant="bold" className="pax-selector__age-section-title">
861
- Please specify the age :
862
- </Text>
863
-
864
- <div className="pax-selector__age-groups">
865
- {/* Helper function to chunk ages into rows of 2 */}
866
- {(() => {
867
- const chunkAges = (ages: (number | undefined)[], category: string) => {
868
- const chunks: (number | undefined)[][] = [];
869
- for (let i = 0; i < ages.length; i += 2) {
870
- chunks.push(ages.slice(i, i + 2));
871
- }
872
- return chunks;
873
- };
874
-
875
- const teenAgeChunks = chunkAges(internalData.teenAges || [], 'Teen');
876
- const childAgeChunks = chunkAges(internalData.childAges || [], 'Child');
877
- const infantAgeChunks = chunkAges(internalData.infantAges || [], 'Infant');
878
-
879
- const handleAgeChange = (category: 'teenAges' | 'childAges' | 'infantAges', index: number, age: number) => {
880
- const ages = [...(internalData[category] || [])];
881
- ages[index] = age;
882
- handleDataChange(category, ages);
883
- };
884
-
885
- return (
886
- <>
887
- {/* Teen ages */}
888
- {internalData.teens > 0 && teenAgeChunks.map((chunk, chunkIndex) => (
889
- <div key={`teen-chunk-${chunkIndex}`} className="pax-selector__age-row">
890
- {chunk.map((age, ageIndex) => {
891
- const actualIndex = chunkIndex * 2 + ageIndex;
892
- return (
893
- <AgeSelector
894
- key={`teen-${actualIndex}`}
895
- label="Teen"
896
- value={age}
897
- onChange={(selectedAge) => handleAgeChange('teenAges', actualIndex, selectedAge)}
898
- ageRange={CHILD_CATEGORY_AGES}
899
- required
900
- />
901
- );
902
- })}
903
- </div>
904
- ))}
905
-
906
- {/* Child ages */}
907
- {internalData.children > 0 && childAgeChunks.map((chunk, chunkIndex) => (
908
- <div key={`child-chunk-${chunkIndex}`} className="pax-selector__age-row">
909
- {chunk.map((age, ageIndex) => {
910
- const actualIndex = chunkIndex * 2 + ageIndex;
911
- return (
912
- <AgeSelector
913
- key={`child-${actualIndex}`}
914
- label="Child"
915
- value={age}
916
- onChange={(selectedAge) => handleAgeChange('childAges', actualIndex, selectedAge)}
917
- ageRange={CHILD_CATEGORY_AGES}
918
- required
919
- />
920
- );
921
- })}
922
- </div>
923
- ))}
924
-
925
- {/* Infant ages */}
926
- {internalData.infants > 0 && infantAgeChunks.map((chunk, chunkIndex) => (
927
- <div key={`infant-chunk-${chunkIndex}`} className="pax-selector__age-row">
928
- {chunk.map((age, ageIndex) => {
929
- const actualIndex = chunkIndex * 2 + ageIndex;
930
- return (
931
- <AgeSelector
932
- key={`infant-${actualIndex}`}
933
- label="Infant"
934
- value={age}
935
- onChange={(selectedAge) => handleAgeChange('infantAges', actualIndex, selectedAge)}
936
- ageRange={CHILD_CATEGORY_AGES}
937
- required
938
- />
939
- );
940
- })}
941
- </div>
942
- ))}
943
- </>
944
- );
945
- })()}
946
- </div>
947
- </div>
948
- )}
949
-
950
- <ClientTypeSelector
951
- value={internalData.clientType}
952
- onChange={(val) => handleDataChange('clientType', val)}
953
- />
954
- </>
955
- )}
956
-
957
- <div className="pax-selector__actions">
958
- <button
959
- type="button"
960
- className="pax-selector__clear-btn"
961
- onClick={handleClear}
962
- >
963
- Clear Pax
964
- </button>
965
- <button
966
- type="button"
967
- className="pax-selector__done-btn"
968
- onClick={handleDone}
969
- >
970
- Done
971
- </button>
972
- </div>
973
- </div>
974
- )}
975
- </div>
976
- );
977
- };
978
-
979
- export default PaxSelector;
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import Icon from '../../atoms/Icon/Icon';
3
+ import { Text } from '../../atoms/Typography/Typography';
4
+
5
+ export type ClientType = 'Standard client' | 'VIP' | 'VVIP' | 'Honeymooners';
6
+
7
+ /**
8
+ * Individual passenger/pax data for a single room
9
+ */
10
+ export interface PaxData {
11
+ adults: number;
12
+ teens: number;
13
+ children: number;
14
+ infants: number;
15
+ /** Ages for teens (1-18 years) */
16
+ teenAges?: number[];
17
+ /** Ages for children (1-18 years) */
18
+ childAges?: number[];
19
+ /** Ages for infants (1-18 years) */
20
+ infantAges?: number[];
21
+ clientType: ClientType;
22
+ }
23
+
24
+ /**
25
+ * Room data for multiple room mode
26
+ */
27
+ export interface RoomData extends PaxData {
28
+ roomId: string;
29
+ }
30
+
31
+ export interface PaxSelectorProps {
32
+ /** Label for the selector */
33
+ label?: string;
34
+ /** Current pax data (single room mode) */
35
+ value?: PaxData;
36
+ /** Callback when pax data changes (single room mode) */
37
+ onChange?: (data: PaxData) => void;
38
+ /** Callback when "Add a room" is clicked */
39
+ onAddRoom?: () => void;
40
+ /** Callback when Done button is clicked */
41
+ onDone?: (data: PaxData | RoomData[]) => void;
42
+ /** Placeholder text */
43
+ placeholder?: string;
44
+ /** Additional CSS classes */
45
+ className?: string;
46
+ /** Maximum number of adults */
47
+ maxAdults?: number;
48
+ /** Maximum number of teens */
49
+ maxTeens?: number;
50
+ /** Maximum number of children */
51
+ maxChildren?: number;
52
+ /** Maximum number of infants */
53
+ maxInfants?: number;
54
+ /** Show add room link */
55
+ showAddRoom?: boolean;
56
+ /** Enable multiple room mode */
57
+ multipleRooms?: boolean;
58
+ /** Default rooms for multiple room mode */
59
+ defaultRooms?: RoomData[];
60
+ /** Callback when rooms change in multiple room mode */
61
+ onRoomsChange?: (rooms: RoomData[]) => void;
62
+ /** Callback when a room is removed */
63
+ onRemoveRoom?: (roomId: string) => void;
64
+ /** Default pax data for single room mode (will trigger onChange on mount) */
65
+ defaultPaxData?: PaxData;
66
+ }
67
+
68
+ const DEFAULT_PAX_DATA: PaxData = {
69
+ adults: 0,
70
+ teens: 0,
71
+ children: 0,
72
+ infants: 0,
73
+ teenAges: [],
74
+ childAges: [],
75
+ infantAges: [],
76
+ clientType: 'Standard client',
77
+ };
78
+
79
+ export const DEFAULT_PAX_DATA_WITH_ADULTS: PaxData = {
80
+ adults: 2,
81
+ teens: 0,
82
+ children: 0,
83
+ infants: 0,
84
+ teenAges: [],
85
+ childAges: [],
86
+ infantAges: [],
87
+ clientType: 'Standard client',
88
+ };
89
+
90
+ const CLIENT_TYPES: ClientType[] = ['Standard client', 'VIP', 'VVIP', 'Honeymooners'];
91
+
92
+ // Age range for all child categories (teens, children, infants)
93
+ const CHILD_CATEGORY_AGES = Array.from({ length: 18 }, (_, i) => i + 1); // 1-18 years
94
+
95
+ interface AgeSelectorProps {
96
+ label: string;
97
+ value: number | undefined;
98
+ onChange: (age: number) => void;
99
+ ageRange: number[];
100
+ required?: boolean;
101
+ }
102
+
103
+ const AgeSelector: React.FC<AgeSelectorProps> = ({ label, value, onChange, ageRange, required }) => {
104
+ const [isOpen, setIsOpen] = useState(false);
105
+ const [inputValue, setInputValue] = useState<string>(value !== undefined ? value.toString() : '');
106
+ const containerRef = useRef<HTMLDivElement>(null);
107
+ const inputRef = useRef<HTMLInputElement>(null);
108
+
109
+ // Sync input value when prop value changes
110
+ useEffect(() => {
111
+ setInputValue(value !== undefined ? value.toString() : '');
112
+ }, [value]);
113
+
114
+ useEffect(() => {
115
+ const handleClickOutside = (event: MouseEvent) => {
116
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
117
+ setIsOpen(false);
118
+ }
119
+ };
120
+
121
+ if (isOpen) {
122
+ document.addEventListener('mousedown', handleClickOutside);
123
+ }
124
+
125
+ return () => {
126
+ document.removeEventListener('mousedown', handleClickOutside);
127
+ };
128
+ }, [isOpen]);
129
+
130
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
131
+ const newValue = e.target.value;
132
+
133
+ // Only allow numeric input or empty string
134
+ if (newValue === '' || /^\d+$/.test(newValue)) {
135
+ setInputValue(newValue);
136
+
137
+ // Only update if it's a valid number within range
138
+ const numValue = parseInt(newValue, 10);
139
+ if (newValue === '') {
140
+ // Allow empty input - don't update onChange
141
+ return;
142
+ } else if (!isNaN(numValue) && ageRange.includes(numValue)) {
143
+ onChange(numValue);
144
+ }
145
+ }
146
+ };
147
+
148
+ const handleInputBlur = () => {
149
+ // Validate and set to valid value or clear if invalid
150
+ const numValue = parseInt(inputValue, 10);
151
+ if (inputValue === '') {
152
+ // Keep empty if user cleared it
153
+ return;
154
+ } else if (isNaN(numValue) || !ageRange.includes(numValue)) {
155
+ // Reset to current value if invalid
156
+ setInputValue(value !== undefined ? value.toString() : '');
157
+ } else {
158
+ // Ensure the input value matches the validated value
159
+ setInputValue(numValue.toString());
160
+ }
161
+ };
162
+
163
+ const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
164
+ if (e.key === 'Enter') {
165
+ e.preventDefault();
166
+ inputRef.current?.blur();
167
+ }
168
+ };
169
+
170
+ const handleSelect = (age: string) => {
171
+ const numAge = parseInt(age, 10);
172
+ onChange(numAge);
173
+ setIsOpen(false);
174
+ setInputValue(age);
175
+ };
176
+
177
+ const handleDropdownToggle = () => {
178
+ setIsOpen(!isOpen);
179
+ };
180
+
181
+ const ageOptions = ageRange.map(age => age.toString());
182
+ const displayValue = inputValue || undefined;
183
+
184
+ return (
185
+ <div className="pax-selector__age-selector" ref={containerRef}>
186
+ <Text size="sm" variant="regular" className="pax-selector__age-label">
187
+ {label}
188
+ {required && <span className="pax-selector__age-required"> *</span>}
189
+ </Text>
190
+ <div className="dropdown-container pax-selector__age-dropdown-container">
191
+ <div className={`dropdown-input ${isOpen ? 'dropdown-input--open pax-selector__age-dropdown-input--open' : ''} ${value !== undefined ? 'dropdown-input--selected' : 'dropdown-input--default'} pax-selector__age-dropdown-input`}>
192
+ <input
193
+ ref={inputRef}
194
+ type="text"
195
+ inputMode="numeric"
196
+ className="dropdown-input__text pax-selector__age-input-text"
197
+ value={inputValue}
198
+ onChange={handleInputChange}
199
+ onBlur={handleInputBlur}
200
+ onKeyDown={handleInputKeyDown}
201
+ onFocus={() => setIsOpen(false)}
202
+ placeholder="--"
203
+ aria-label={`${label} age`}
204
+ style={{
205
+ background: 'transparent',
206
+ border: 'none',
207
+ outline: 'none',
208
+ width: '100%',
209
+ cursor: 'text'
210
+ }}
211
+ />
212
+ <button
213
+ type="button"
214
+ className="pax-selector__age-dropdown-btn"
215
+ onClick={(e) => {
216
+ e.preventDefault();
217
+ e.stopPropagation();
218
+ handleDropdownToggle();
219
+ }}
220
+ aria-expanded={isOpen}
221
+ aria-haspopup="listbox"
222
+ aria-label="Open age dropdown"
223
+ style={{
224
+ background: 'transparent',
225
+ border: 'none',
226
+ cursor: 'pointer',
227
+ padding: 0,
228
+ display: 'flex',
229
+ alignItems: 'center'
230
+ }}
231
+ >
232
+ <Icon
233
+ name="chevron-down"
234
+ size="sm"
235
+ className="dropdown-input__icon dropdown-input__icon--chevron"
236
+ />
237
+ </button>
238
+ </div>
239
+ {isOpen && (
240
+ <div className="dropdown-menu" role="listbox">
241
+ {ageOptions.map((age) => (
242
+ <div
243
+ key={age}
244
+ className={`dropdown-option ${value?.toString() === age ? 'dropdown-option--selected' : ''}`}
245
+ onClick={() => handleSelect(age)}
246
+ role="option"
247
+ aria-selected={value?.toString() === age}
248
+ >
249
+ {age}
250
+ </div>
251
+ ))}
252
+ </div>
253
+ )}
254
+ </div>
255
+ </div>
256
+ );
257
+ };
258
+
259
+ interface StepperRowProps {
260
+ label: string;
261
+ value: number;
262
+ min?: number;
263
+ max?: number;
264
+ onChange: (value: number) => void;
265
+ }
266
+
267
+ const StepperRow: React.FC<StepperRowProps> = ({
268
+ label,
269
+ value,
270
+ min = 0,
271
+ max = 99,
272
+ onChange
273
+ }) => {
274
+ const handleDecrement = () => {
275
+ if (value > min) {
276
+ onChange(value - 1);
277
+ }
278
+ };
279
+
280
+ const handleIncrement = () => {
281
+ if (value < max) {
282
+ onChange(value + 1);
283
+ }
284
+ };
285
+
286
+ return (
287
+ <div className="pax-selector__stepper">
288
+ <Text size="base" variant="bold" className="pax-selector__stepper-label">
289
+ {label}
290
+ </Text>
291
+ <div className="pax-selector__stepper-controls">
292
+ <button
293
+ type="button"
294
+ className="pax-selector__stepper-btn"
295
+ onClick={handleDecrement}
296
+ disabled={value <= min}
297
+ aria-label={`Decrease ${label}`}
298
+ >
299
+ <Icon name="minus" size="sm" />
300
+ </button>
301
+ <span className="pax-selector__stepper-count">{value}</span>
302
+ <button
303
+ type="button"
304
+ className="pax-selector__stepper-btn"
305
+ onClick={handleIncrement}
306
+ disabled={value >= max}
307
+ aria-label={`Increase ${label}`}
308
+ >
309
+ <Icon name="plus" size="sm" />
310
+ </button>
311
+ </div>
312
+ </div>
313
+ );
314
+ };
315
+
316
+ interface ClientTypeSelectorProps {
317
+ value: ClientType;
318
+ onChange: (value: ClientType) => void;
319
+ }
320
+
321
+ const ClientTypeSelector: React.FC<ClientTypeSelectorProps> = ({ value, onChange }) => {
322
+ const [isOpen, setIsOpen] = useState(false);
323
+ const containerRef = useRef<HTMLDivElement>(null);
324
+
325
+ useEffect(() => {
326
+ const handleClickOutside = (event: MouseEvent) => {
327
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
328
+ setIsOpen(false);
329
+ }
330
+ };
331
+
332
+ if (isOpen) {
333
+ document.addEventListener('mousedown', handleClickOutside);
334
+ }
335
+
336
+ return () => {
337
+ document.removeEventListener('mousedown', handleClickOutside);
338
+ };
339
+ }, [isOpen]);
340
+
341
+ const handleSelect = (type: ClientType) => {
342
+ onChange(type);
343
+ setIsOpen(false);
344
+ };
345
+
346
+ return (
347
+ <div className="pax-selector__client-type">
348
+ <Text size="sm" variant="regular" className="pax-selector__client-type-label">
349
+ Client type
350
+ </Text>
351
+ <div className="pax-selector__client-type-select" ref={containerRef}>
352
+ <button
353
+ type="button"
354
+ className="pax-selector__client-type-trigger"
355
+ onClick={() => setIsOpen(!isOpen)}
356
+ aria-expanded={isOpen}
357
+ aria-haspopup="listbox"
358
+ >
359
+ <div className="pax-selector__client-type-content">
360
+ <Icon name="user-icon" size="sm" className="pax-selector__client-type-icon" />
361
+ <span className="pax-selector__client-type-text">{value}</span>
362
+ </div>
363
+ <Icon
364
+ name="chevron-down"
365
+ size="sm"
366
+ className={`pax-selector__client-type-chevron ${isOpen ? 'pax-selector__client-type-chevron--open' : ''}`}
367
+ />
368
+ </button>
369
+
370
+ {isOpen && (
371
+ <div className="pax-selector__client-type-dropdown" role="listbox">
372
+ {CLIENT_TYPES.map((type) => (
373
+ <button
374
+ key={type}
375
+ type="button"
376
+ className={`pax-selector__client-type-option ${value === type ? 'pax-selector__client-type-option--selected' : ''}`}
377
+ onClick={() => handleSelect(type)}
378
+ role="option"
379
+ aria-selected={value === type}
380
+ >
381
+ <Icon name="user-icon" size="sm" />
382
+ {type}
383
+ </button>
384
+ ))}
385
+ </div>
386
+ )}
387
+ </div>
388
+ </div>
389
+ );
390
+ };
391
+
392
+ interface RoomEditorProps {
393
+ room: RoomData;
394
+ roomNumber: number;
395
+ showRemove: boolean;
396
+ maxAdults: number;
397
+ maxTeens: number;
398
+ maxChildren: number;
399
+ maxInfants: number;
400
+ onChange: (room: RoomData) => void;
401
+ onRemove: () => void;
402
+ }
403
+
404
+ const RoomEditor: React.FC<RoomEditorProps> = ({
405
+ room,
406
+ roomNumber,
407
+ showRemove,
408
+ maxAdults,
409
+ maxTeens,
410
+ maxChildren,
411
+ maxInfants,
412
+ onChange,
413
+ onRemove,
414
+ }) => {
415
+ const handleFieldChange = (field: keyof PaxData, value: number | ClientType | number[]) => {
416
+ onChange({ ...room, [field]: value });
417
+ };
418
+
419
+ const handleAgeChange = (category: 'teenAges' | 'childAges' | 'infantAges', index: number, age: number) => {
420
+ const ages = [...(room[category] || [])];
421
+ ages[index] = age;
422
+ handleFieldChange(category, ages);
423
+ };
424
+
425
+ // Generate age arrays based on counts
426
+ useEffect(() => {
427
+ const teenAges = room.teenAges || [];
428
+ const childAges = room.childAges || [];
429
+ const infantAges = room.infantAges || [];
430
+
431
+ // Adjust teen ages array
432
+ if (teenAges.length < room.teens) {
433
+ handleFieldChange('teenAges', [...teenAges, ...Array(room.teens - teenAges.length).fill(undefined)]);
434
+ } else if (teenAges.length > room.teens) {
435
+ handleFieldChange('teenAges', teenAges.slice(0, room.teens));
436
+ }
437
+
438
+ // Adjust child ages array
439
+ if (childAges.length < room.children) {
440
+ handleFieldChange('childAges', [...childAges, ...Array(room.children - childAges.length).fill(undefined)]);
441
+ } else if (childAges.length > room.children) {
442
+ handleFieldChange('childAges', childAges.slice(0, room.children));
443
+ }
444
+
445
+ // Adjust infant ages array
446
+ if (infantAges.length < room.infants) {
447
+ handleFieldChange('infantAges', [...infantAges, ...Array(room.infants - infantAges.length).fill(undefined)]);
448
+ } else if (infantAges.length > room.infants) {
449
+ handleFieldChange('infantAges', infantAges.slice(0, room.infants));
450
+ }
451
+ }, [room.teens, room.children, room.infants]);
452
+
453
+ // Chunk ages into groups of 2 for layout
454
+ const chunkAges = (ages: (number | undefined)[], category: string) => {
455
+ const chunks: (number | undefined)[][] = [];
456
+ for (let i = 0; i < ages.length; i += 2) {
457
+ chunks.push(ages.slice(i, i + 2));
458
+ }
459
+ return chunks;
460
+ };
461
+
462
+ const teenAgeChunks = chunkAges(room.teenAges || [], 'Teen');
463
+ const childAgeChunks = chunkAges(room.childAges || [], 'Child');
464
+ const infantAgeChunks = chunkAges(room.infantAges || [], 'Infant');
465
+
466
+ return (
467
+ <div className="pax-selector__room-container">
468
+ <div className="pax-selector__room-header">
469
+ <div className="pax-selector__room-title">
470
+ <Text size="lg" variant="bold" className="pax-selector__room-name">
471
+ Room {roomNumber}
472
+ </Text>
473
+ <Icon name="home" size="md" className="pax-selector__room-icon" />
474
+ </div>
475
+ {showRemove && (
476
+ <button
477
+ type="button"
478
+ className="pax-selector__room-remove"
479
+ onClick={onRemove}
480
+ aria-label={`Remove room ${roomNumber}`}
481
+ >
482
+ <Icon name="close" size="sm" className="pax-selector__room-remove-icon" />
483
+ <span className="pax-selector__room-remove-text">Remove</span>
484
+ </button>
485
+ )}
486
+ </div>
487
+
488
+ <div className="pax-selector__room-content">
489
+ <div className="pax-selector__steppers">
490
+ <StepperRow
491
+ label="Adult"
492
+ value={room.adults}
493
+ max={maxAdults}
494
+ onChange={(val) => handleFieldChange('adults', val)}
495
+ />
496
+ <StepperRow
497
+ label="Teen"
498
+ value={room.teens}
499
+ max={maxTeens}
500
+ onChange={(val) => handleFieldChange('teens', val)}
501
+ />
502
+ <StepperRow
503
+ label="Child"
504
+ value={room.children}
505
+ max={maxChildren}
506
+ onChange={(val) => handleFieldChange('children', val)}
507
+ />
508
+ <StepperRow
509
+ label="Infant"
510
+ value={room.infants}
511
+ max={maxInfants}
512
+ onChange={(val) => handleFieldChange('infants', val)}
513
+ />
514
+ </div>
515
+
516
+ {/* Age specification section */}
517
+ {((room.teens > 0) || (room.children > 0) || (room.infants > 0)) && (
518
+ <div className="pax-selector__age-section">
519
+ <Text size="base" variant="bold" className="pax-selector__age-section-title">
520
+ Please specify the age :
521
+ </Text>
522
+
523
+ <div className="pax-selector__age-groups">
524
+ {/* Teen ages */}
525
+ {room.teens > 0 && teenAgeChunks.map((chunk, chunkIndex) => (
526
+ <div key={`teen-chunk-${chunkIndex}`} className="pax-selector__age-row">
527
+ {chunk.map((age, ageIndex) => {
528
+ const actualIndex = chunkIndex * 2 + ageIndex;
529
+ return (
530
+ <AgeSelector
531
+ key={`teen-${actualIndex}`}
532
+ label="Teen"
533
+ value={age}
534
+ onChange={(selectedAge) => handleAgeChange('teenAges', actualIndex, selectedAge)}
535
+ ageRange={CHILD_CATEGORY_AGES}
536
+ required
537
+ />
538
+ );
539
+ })}
540
+ </div>
541
+ ))}
542
+
543
+ {/* Child ages */}
544
+ {room.children > 0 && childAgeChunks.map((chunk, chunkIndex) => (
545
+ <div key={`child-chunk-${chunkIndex}`} className="pax-selector__age-row">
546
+ {chunk.map((age, ageIndex) => {
547
+ const actualIndex = chunkIndex * 2 + ageIndex;
548
+ return (
549
+ <AgeSelector
550
+ key={`child-${actualIndex}`}
551
+ label="Child"
552
+ value={age}
553
+ onChange={(selectedAge) => handleAgeChange('childAges', actualIndex, selectedAge)}
554
+ ageRange={CHILD_CATEGORY_AGES}
555
+ required
556
+ />
557
+ );
558
+ })}
559
+ </div>
560
+ ))}
561
+
562
+ {/* Infant ages */}
563
+ {room.infants > 0 && infantAgeChunks.map((chunk, chunkIndex) => (
564
+ <div key={`infant-chunk-${chunkIndex}`} className="pax-selector__age-row">
565
+ {chunk.map((age, ageIndex) => {
566
+ const actualIndex = chunkIndex * 2 + ageIndex;
567
+ return (
568
+ <AgeSelector
569
+ key={`infant-${actualIndex}`}
570
+ label="Infant"
571
+ value={age}
572
+ onChange={(selectedAge) => handleAgeChange('infantAges', actualIndex, selectedAge)}
573
+ ageRange={CHILD_CATEGORY_AGES}
574
+ required
575
+ />
576
+ );
577
+ })}
578
+ </div>
579
+ ))}
580
+ </div>
581
+ </div>
582
+ )}
583
+
584
+ <ClientTypeSelector
585
+ value={room.clientType}
586
+ onChange={(val) => handleFieldChange('clientType', val)}
587
+ />
588
+ </div>
589
+ </div>
590
+ );
591
+ };
592
+
593
+ const PaxSelector: React.FC<PaxSelectorProps> = ({
594
+ label = 'Number of pax',
595
+ value,
596
+ onChange,
597
+ onAddRoom,
598
+ onDone,
599
+ placeholder = 'Select pax',
600
+ className = '',
601
+ maxAdults = 10,
602
+ maxTeens = 10,
603
+ maxChildren = 10,
604
+ maxInfants = 10,
605
+ showAddRoom = true,
606
+ multipleRooms = false,
607
+ defaultRooms,
608
+ onRoomsChange,
609
+ onRemoveRoom,
610
+ defaultPaxData = DEFAULT_PAX_DATA_WITH_ADULTS,
611
+ }) => {
612
+ const [isOpen, setIsOpen] = useState(false);
613
+ const [internalData, setInternalData] = useState<PaxData>(value || defaultPaxData || DEFAULT_PAX_DATA);
614
+ const [rooms, setRooms] = useState<RoomData[]>(
615
+ defaultRooms || [{ ...DEFAULT_PAX_DATA, roomId: '1' }]
616
+ );
617
+ const containerRef = useRef<HTMLDivElement>(null);
618
+ const hasInitialized = useRef(false);
619
+
620
+ // Sync internal data with external value prop
621
+ useEffect(() => {
622
+ if (value && !multipleRooms) {
623
+ setInternalData(value);
624
+ }
625
+ }, [value, multipleRooms]);
626
+
627
+ // Initialize with default pax data and trigger onChange on mount
628
+ useEffect(() => {
629
+ if (!hasInitialized.current && !value && defaultPaxData && !multipleRooms && onChange) {
630
+ hasInitialized.current = true;
631
+ onChange(defaultPaxData);
632
+ }
633
+ }, [defaultPaxData, value, multipleRooms, onChange]);
634
+
635
+ // Manage age arrays in single mode when counts change
636
+ useEffect(() => {
637
+ if (!multipleRooms) {
638
+ const teenAges = internalData.teenAges || [];
639
+ const childAges = internalData.childAges || [];
640
+ const infantAges = internalData.infantAges || [];
641
+
642
+ let needsUpdate = false;
643
+ const updatedData = { ...internalData };
644
+
645
+ // Adjust teen ages array
646
+ if (teenAges.length < internalData.teens) {
647
+ updatedData.teenAges = [...teenAges, ...Array(internalData.teens - teenAges.length).fill(undefined)];
648
+ needsUpdate = true;
649
+ } else if (teenAges.length > internalData.teens) {
650
+ updatedData.teenAges = teenAges.slice(0, internalData.teens);
651
+ needsUpdate = true;
652
+ }
653
+
654
+ // Adjust child ages array
655
+ if (childAges.length < internalData.children) {
656
+ updatedData.childAges = [...childAges, ...Array(internalData.children - childAges.length).fill(undefined)];
657
+ needsUpdate = true;
658
+ } else if (childAges.length > internalData.children) {
659
+ updatedData.childAges = childAges.slice(0, internalData.children);
660
+ needsUpdate = true;
661
+ }
662
+
663
+ // Adjust infant ages array
664
+ if (infantAges.length < internalData.infants) {
665
+ updatedData.infantAges = [...infantAges, ...Array(internalData.infants - infantAges.length).fill(undefined)];
666
+ needsUpdate = true;
667
+ } else if (infantAges.length > internalData.infants) {
668
+ updatedData.infantAges = infantAges.slice(0, internalData.infants);
669
+ needsUpdate = true;
670
+ }
671
+
672
+ if (needsUpdate) {
673
+ setInternalData(updatedData);
674
+ onChange?.(updatedData);
675
+ }
676
+ }
677
+ // eslint-disable-next-line react-hooks/exhaustive-deps
678
+ }, [internalData.teens, internalData.children, internalData.infants, multipleRooms]);
679
+
680
+ // Handle clicks outside the dropdown
681
+ useEffect(() => {
682
+ const handleClickOutside = (event: MouseEvent) => {
683
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
684
+ setIsOpen(false);
685
+ }
686
+ };
687
+
688
+ if (isOpen) {
689
+ document.addEventListener('mousedown', handleClickOutside);
690
+ }
691
+
692
+ return () => {
693
+ document.removeEventListener('mousedown', handleClickOutside);
694
+ };
695
+ }, [isOpen]);
696
+
697
+ const handleDataChange = (field: keyof PaxData, newValue: number | ClientType | number[]) => {
698
+ const newData = { ...internalData, [field]: newValue };
699
+ setInternalData(newData);
700
+ onChange?.(newData);
701
+ };
702
+
703
+ const handleClear = () => {
704
+ if (multipleRooms) {
705
+ const clearedRooms = rooms.map(room => ({ ...DEFAULT_PAX_DATA, roomId: room.roomId }));
706
+ setRooms(clearedRooms);
707
+ onRoomsChange?.(clearedRooms);
708
+ } else {
709
+ const clearedData = { ...DEFAULT_PAX_DATA };
710
+ setInternalData(clearedData);
711
+ onChange?.(clearedData);
712
+ }
713
+ };
714
+
715
+ const handleDone = () => {
716
+ setIsOpen(false);
717
+ onDone?.(multipleRooms ? rooms : internalData);
718
+ };
719
+
720
+ const handleAddRoom = () => {
721
+ const newRoom: RoomData = {
722
+ ...DEFAULT_PAX_DATA,
723
+ roomId: `${rooms.length + 1}`
724
+ };
725
+ const updatedRooms = [...rooms, newRoom];
726
+ setRooms(updatedRooms);
727
+ onRoomsChange?.(updatedRooms);
728
+ onAddRoom?.();
729
+ };
730
+
731
+ const handleRemoveRoom = (roomId: string) => {
732
+ const updatedRooms = rooms.filter(room => room.roomId !== roomId);
733
+ setRooms(updatedRooms);
734
+ onRoomsChange?.(updatedRooms);
735
+ onRemoveRoom?.(roomId);
736
+ };
737
+
738
+ const handleRoomChange = (roomId: string, updatedRoom: RoomData) => {
739
+ const updatedRooms = rooms.map(room =>
740
+ room.roomId === roomId ? updatedRoom : room
741
+ );
742
+ setRooms(updatedRooms);
743
+ onRoomsChange?.(updatedRooms);
744
+ };
745
+
746
+ const getTotalPax = () => {
747
+ if (multipleRooms) {
748
+ return rooms.reduce((total, room) =>
749
+ total + room.adults + room.teens + room.children + room.infants, 0
750
+ );
751
+ }
752
+ const { adults, teens, children, infants } = internalData;
753
+ return adults + teens + children + infants;
754
+ };
755
+
756
+ const getDisplayText = () => {
757
+ const total = getTotalPax();
758
+ if (total === 0) {
759
+ return placeholder;
760
+ }
761
+ return `${total} pax`;
762
+ };
763
+
764
+ const hasPax = getTotalPax() > 0;
765
+
766
+ return (
767
+ <div className={`pax-selector ${className}`} ref={containerRef}>
768
+ <button
769
+ type="button"
770
+ className="pax-selector__trigger"
771
+ onClick={() => setIsOpen(!isOpen)}
772
+ aria-expanded={isOpen}
773
+ aria-haspopup="true"
774
+ >
775
+ <Text size="sm" variant="regular" className="pax-selector__label">
776
+ {label}
777
+ </Text>
778
+ <div className={`pax-selector__input ${isOpen ? 'pax-selector__input--active' : ''}`}>
779
+ <div className="pax-selector__input-content">
780
+ <Icon name="user-icon" size="sm" className="pax-selector__input-icon" />
781
+ <span className={`pax-selector__input-text ${!hasPax ? 'pax-selector__input-placeholder' : ''}`}>
782
+ {getDisplayText()}
783
+ </span>
784
+ </div>
785
+ <Icon
786
+ name="chevron-down"
787
+ size="sm"
788
+ className={`pax-selector__chevron ${isOpen ? 'pax-selector__chevron--open' : ''}`}
789
+ />
790
+ </div>
791
+ </button>
792
+
793
+ {isOpen && (
794
+ <div className="pax-selector__dropdown">
795
+ {/* Multiple Rooms Mode */}
796
+ {multipleRooms ? (
797
+ <>
798
+ {showAddRoom && (
799
+ <button
800
+ type="button"
801
+ className="pax-selector__add-room"
802
+ onClick={handleAddRoom}
803
+ >
804
+ <Icon name="home" size="sm" className="pax-selector__add-room-icon" />
805
+ Add a room
806
+ <Icon name="plus" size="sm" className="pax-selector__add-room-icon" />
807
+ </button>
808
+ )}
809
+
810
+ <div className="pax-selector__rooms">
811
+ {rooms.map((room, index) => (
812
+ <RoomEditor
813
+ key={room.roomId}
814
+ room={room}
815
+ roomNumber={index + 1}
816
+ showRemove={rooms.length > 1}
817
+ maxAdults={maxAdults}
818
+ maxTeens={maxTeens}
819
+ maxChildren={maxChildren}
820
+ maxInfants={maxInfants}
821
+ onChange={(updatedRoom) => handleRoomChange(room.roomId, updatedRoom)}
822
+ onRemove={() => handleRemoveRoom(room.roomId)}
823
+ />
824
+ ))}
825
+ </div>
826
+ </>
827
+ ) : (
828
+ /* Single Room Mode */
829
+ <>
830
+ <div className="pax-selector__steppers">
831
+ <StepperRow
832
+ label="Adult"
833
+ value={internalData.adults}
834
+ max={maxAdults}
835
+ onChange={(val) => handleDataChange('adults', val)}
836
+ />
837
+ <StepperRow
838
+ label="Teen"
839
+ value={internalData.teens}
840
+ max={maxTeens}
841
+ onChange={(val) => handleDataChange('teens', val)}
842
+ />
843
+ <StepperRow
844
+ label="Child"
845
+ value={internalData.children}
846
+ max={maxChildren}
847
+ onChange={(val) => handleDataChange('children', val)}
848
+ />
849
+ <StepperRow
850
+ label="Infant"
851
+ value={internalData.infants}
852
+ max={maxInfants}
853
+ onChange={(val) => handleDataChange('infants', val)}
854
+ />
855
+ </div>
856
+
857
+ {/* Age specification for single room */}
858
+ {((internalData.teens > 0) || (internalData.children > 0) || (internalData.infants > 0)) && (
859
+ <div className="pax-selector__age-section">
860
+ <Text size="base" variant="bold" className="pax-selector__age-section-title">
861
+ Please specify the age :
862
+ </Text>
863
+
864
+ <div className="pax-selector__age-groups">
865
+ {/* Helper function to chunk ages into rows of 2 */}
866
+ {(() => {
867
+ const chunkAges = (ages: (number | undefined)[], category: string) => {
868
+ const chunks: (number | undefined)[][] = [];
869
+ for (let i = 0; i < ages.length; i += 2) {
870
+ chunks.push(ages.slice(i, i + 2));
871
+ }
872
+ return chunks;
873
+ };
874
+
875
+ const teenAgeChunks = chunkAges(internalData.teenAges || [], 'Teen');
876
+ const childAgeChunks = chunkAges(internalData.childAges || [], 'Child');
877
+ const infantAgeChunks = chunkAges(internalData.infantAges || [], 'Infant');
878
+
879
+ const handleAgeChange = (category: 'teenAges' | 'childAges' | 'infantAges', index: number, age: number) => {
880
+ const ages = [...(internalData[category] || [])];
881
+ ages[index] = age;
882
+ handleDataChange(category, ages);
883
+ };
884
+
885
+ return (
886
+ <>
887
+ {/* Teen ages */}
888
+ {internalData.teens > 0 && teenAgeChunks.map((chunk, chunkIndex) => (
889
+ <div key={`teen-chunk-${chunkIndex}`} className="pax-selector__age-row">
890
+ {chunk.map((age, ageIndex) => {
891
+ const actualIndex = chunkIndex * 2 + ageIndex;
892
+ return (
893
+ <AgeSelector
894
+ key={`teen-${actualIndex}`}
895
+ label="Teen"
896
+ value={age}
897
+ onChange={(selectedAge) => handleAgeChange('teenAges', actualIndex, selectedAge)}
898
+ ageRange={CHILD_CATEGORY_AGES}
899
+ required
900
+ />
901
+ );
902
+ })}
903
+ </div>
904
+ ))}
905
+
906
+ {/* Child ages */}
907
+ {internalData.children > 0 && childAgeChunks.map((chunk, chunkIndex) => (
908
+ <div key={`child-chunk-${chunkIndex}`} className="pax-selector__age-row">
909
+ {chunk.map((age, ageIndex) => {
910
+ const actualIndex = chunkIndex * 2 + ageIndex;
911
+ return (
912
+ <AgeSelector
913
+ key={`child-${actualIndex}`}
914
+ label="Child"
915
+ value={age}
916
+ onChange={(selectedAge) => handleAgeChange('childAges', actualIndex, selectedAge)}
917
+ ageRange={CHILD_CATEGORY_AGES}
918
+ required
919
+ />
920
+ );
921
+ })}
922
+ </div>
923
+ ))}
924
+
925
+ {/* Infant ages */}
926
+ {internalData.infants > 0 && infantAgeChunks.map((chunk, chunkIndex) => (
927
+ <div key={`infant-chunk-${chunkIndex}`} className="pax-selector__age-row">
928
+ {chunk.map((age, ageIndex) => {
929
+ const actualIndex = chunkIndex * 2 + ageIndex;
930
+ return (
931
+ <AgeSelector
932
+ key={`infant-${actualIndex}`}
933
+ label="Infant"
934
+ value={age}
935
+ onChange={(selectedAge) => handleAgeChange('infantAges', actualIndex, selectedAge)}
936
+ ageRange={CHILD_CATEGORY_AGES}
937
+ required
938
+ />
939
+ );
940
+ })}
941
+ </div>
942
+ ))}
943
+ </>
944
+ );
945
+ })()}
946
+ </div>
947
+ </div>
948
+ )}
949
+
950
+ <ClientTypeSelector
951
+ value={internalData.clientType}
952
+ onChange={(val) => handleDataChange('clientType', val)}
953
+ />
954
+ </>
955
+ )}
956
+
957
+ <div className="pax-selector__actions">
958
+ <button
959
+ type="button"
960
+ className="pax-selector__clear-btn"
961
+ onClick={handleClear}
962
+ >
963
+ Clear Pax
964
+ </button>
965
+ <button
966
+ type="button"
967
+ className="pax-selector__done-btn"
968
+ onClick={handleDone}
969
+ >
970
+ Done
971
+ </button>
972
+ </div>
973
+ </div>
974
+ )}
975
+ </div>
976
+ );
977
+ };
978
+
979
+ export default PaxSelector;