mautourco-components 0.2.4 → 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.
- package/README.md +190 -190
- package/dist/components/atoms/Avatar/Avatar.d.ts +14 -14
- package/dist/components/atoms/Avatar/Avatar.js +31 -31
- package/dist/components/atoms/Button/Button.css +320 -320
- package/dist/components/atoms/Button/Button.d.ts +27 -27
- package/dist/components/atoms/Button/Button.js +35 -35
- package/dist/components/atoms/Checkbox/Checkbox.d.ts +13 -13
- package/dist/components/atoms/Checkbox/Checkbox.js +39 -39
- package/dist/components/atoms/Icon/Icon.d.ts +10 -10
- package/dist/components/atoms/Icon/Icon.js +123 -123
- package/dist/components/atoms/Icon/icons/ArrivalIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/ArrivalIcon.js +31 -31
- package/dist/components/atoms/Icon/icons/BuildingIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/BuildingIcon.js +36 -36
- package/dist/components/atoms/Icon/icons/CalendarIcon.d.ts +12 -12
- package/dist/components/atoms/Icon/icons/CalendarIcon.js +41 -41
- package/dist/components/atoms/Icon/icons/CalendarOutlineIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/CalendarOutlineIcon.js +36 -36
- package/dist/components/atoms/Icon/icons/CarIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/CarIcon.js +30 -30
- package/dist/components/atoms/Icon/icons/Check.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/Check.js +30 -30
- package/dist/components/atoms/Icon/icons/CheckCircleIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/CheckCircleIcon.js +30 -30
- package/dist/components/atoms/Icon/icons/Chevron.d.ts +9 -9
- package/dist/components/atoms/Icon/icons/Chevron.js +54 -54
- package/dist/components/atoms/Icon/icons/ChevronDownIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/ChevronDownIcon.js +30 -30
- package/dist/components/atoms/Icon/icons/Close.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/Close.js +30 -30
- package/dist/components/atoms/Icon/icons/DeleteIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/DeleteIcon.js +30 -30
- package/dist/components/atoms/Icon/icons/DepartureIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/DepartureIcon.js +30 -30
- package/dist/components/atoms/Icon/icons/EyeIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/EyeIcon.js +30 -30
- package/dist/components/atoms/Icon/icons/FacebookIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/FacebookIcon.js +36 -36
- package/dist/components/atoms/Icon/icons/HomeIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/HomeIcon.js +25 -25
- package/dist/components/atoms/Icon/icons/InfoIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/InfoIcon.js +30 -30
- package/dist/components/atoms/Icon/icons/LinkedInIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/LinkedInIcon.js +36 -36
- package/dist/components/atoms/Icon/icons/MapPinIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/MapPinIcon.js +30 -30
- package/dist/components/atoms/Icon/icons/MautoucoLogo.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/MautoucoLogo.js +37 -37
- package/dist/components/atoms/Icon/icons/MenuIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/MenuIcon.js +37 -37
- package/dist/components/atoms/Icon/icons/MinusIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/MinusIcon.js +25 -25
- package/dist/components/atoms/Icon/icons/MoreIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/MoreIcon.js +30 -30
- package/dist/components/atoms/Icon/icons/PlaneIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/PlaneIcon.js +36 -36
- package/dist/components/atoms/Icon/icons/PlusIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/PlusIcon.js +25 -25
- package/dist/components/atoms/Icon/icons/Search.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/Search.js +30 -30
- package/dist/components/atoms/Icon/icons/Settings.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/Settings.js +30 -30
- package/dist/components/atoms/Icon/icons/ShipIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/ShipIcon.js +36 -36
- package/dist/components/atoms/Icon/icons/StrollerIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/StrollerIcon.js +30 -30
- package/dist/components/atoms/Icon/icons/TwitterIcon.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/TwitterIcon.js +36 -36
- package/dist/components/atoms/Icon/icons/User.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/User.js +30 -30
- package/dist/components/atoms/Icon/icons/UserIcon.d.ts +12 -12
- package/dist/components/atoms/Icon/icons/UserIcon.js +41 -41
- package/dist/components/atoms/Icon/icons/Youtube.d.ts +8 -8
- package/dist/components/atoms/Icon/icons/Youtube.js +36 -36
- package/dist/components/atoms/Illustration/Illustration.d.ts +14 -14
- package/dist/components/atoms/Illustration/Illustration.js +33 -33
- package/dist/components/atoms/Illustration/illustrations.d.ts +51 -51
- package/dist/components/atoms/Illustration/illustrations.js +97 -97
- package/dist/components/atoms/Inputs/DropdownInput/DropdownInput.d.ts +12 -12
- package/dist/components/atoms/Inputs/DropdownInput/DropdownInput.js +53 -53
- package/dist/components/atoms/Inputs/Input/Input.d.ts +15 -15
- package/dist/components/atoms/Inputs/Input/Input.js +27 -27
- package/dist/components/atoms/Inputs/Textarea/Textarea.d.ts +14 -14
- package/dist/components/atoms/Inputs/Textarea/Textarea.js +15 -15
- package/dist/components/atoms/Link/Link.d.ts +44 -44
- package/dist/components/atoms/Link/Link.js +76 -76
- package/dist/components/atoms/RatingStar/RatingStar.d.ts +40 -40
- package/dist/components/atoms/RatingStar/RatingStar.js +54 -54
- package/dist/components/atoms/SegmentedButton/SegmentedButton.d.ts +27 -27
- package/dist/components/atoms/SegmentedButton/SegmentedButton.js +49 -49
- package/dist/components/atoms/SelectedValue/SelectedValue.d.ts +11 -11
- package/dist/components/atoms/SelectedValue/SelectedValue.js +29 -29
- package/dist/components/atoms/Slider/Slider.d.ts +52 -52
- package/dist/components/atoms/Slider/Slider.js +30 -30
- package/dist/components/atoms/Spinner/Spinner.d.ts +9 -9
- package/dist/components/atoms/Spinner/Spinner.js +38 -38
- package/dist/components/atoms/Spinner/variants/ButtonSpinner.d.ts +8 -8
- package/dist/components/atoms/Spinner/variants/ButtonSpinner.js +19 -19
- package/dist/components/atoms/Spinner/variants/LoadingSpinner.d.ts +7 -7
- package/dist/components/atoms/Spinner/variants/LoadingSpinner.js +7 -7
- package/dist/components/atoms/Tab/Tab.css +266 -266
- package/dist/components/atoms/Tab/Tab.d.ts +22 -22
- package/dist/components/atoms/Tab/Tab.js +54 -54
- package/dist/components/atoms/Typography/Typography.d.ts +24 -24
- package/dist/components/atoms/Typography/Typography.js +100 -100
- package/dist/components/molecules/Calendar/CalendarInput.d.ts +34 -34
- package/dist/components/molecules/Calendar/CalendarInput.js +49 -49
- package/dist/components/molecules/Calendar/DateTime.d.ts +25 -25
- package/dist/components/molecules/Calendar/DateTime.js +106 -106
- package/dist/components/molecules/Calendar/TimePicker.d.ts +16 -16
- package/dist/components/molecules/Calendar/TimePicker.js +91 -91
- package/dist/components/molecules/LocationDropdown/LocationDropdown.d.ts +34 -34
- package/dist/components/molecules/LocationDropdown/LocationDropdown.js +120 -120
- package/dist/components/molecules/LocationDropdown/index.d.ts +2 -2
- package/dist/components/molecules/LocationDropdown/index.js +1 -1
- package/dist/components/molecules/MultiSelectDropdown/MultiSelectDropdown.d.ts +29 -29
- package/dist/components/molecules/MultiSelectDropdown/MultiSelectDropdown.js +106 -106
- package/dist/components/molecules/RatingTab/RatingTab.d.ts +39 -39
- package/dist/components/molecules/RatingTab/RatingTab.js +41 -41
- package/dist/components/molecules/TabGroup/TabGroup.d.ts +17 -17
- package/dist/components/molecules/TabGroup/TabGroup.js +30 -30
- package/dist/components/molecules/UserCard/UserCard.d.ts +20 -20
- package/dist/components/molecules/UserCard/UserCard.js +57 -57
- package/dist/components/organisms/CardContainer/CardContainer.d.ts +37 -37
- package/dist/components/organisms/CardContainer/CardContainer.js +27 -27
- package/dist/components/organisms/DateTimePicker/DateTimePicker.d.ts +15 -15
- package/dist/components/organisms/DateTimePicker/DateTimePicker.js +66 -66
- package/dist/components/organisms/Dialog/Dialog.d.ts +103 -103
- package/dist/components/organisms/Dialog/Dialog.js +162 -162
- package/dist/components/organisms/Footer/Footer.d.ts +38 -38
- package/dist/components/organisms/Footer/Footer.js +74 -74
- package/dist/components/organisms/PaxSelector/PaxSelector.d.ts +63 -63
- package/dist/components/organisms/PaxSelector/PaxSelector.js +402 -402
- package/dist/components/organisms/RoundTrip/RoundTrip.d.ts +54 -54
- package/dist/components/organisms/RoundTrip/RoundTrip.js +179 -179
- package/dist/components/organisms/RoundTrip/index.d.ts +2 -2
- package/dist/components/organisms/RoundTrip/index.js +1 -1
- package/dist/components/organisms/SearchBarTransfer/SearchBarTransfer.d.ts +35 -35
- package/dist/components/organisms/SearchBarTransfer/SearchBarTransfer.js +192 -192
- package/dist/components/organisms/SearchBarTransfer/index.d.ts +2 -2
- package/dist/components/organisms/SearchBarTransfer/index.js +1 -1
- package/dist/components/organisms/TopNavigation/DesktopNav.d.ts +33 -33
- package/dist/components/organisms/TopNavigation/DesktopNav.js +32 -26
- package/dist/components/organisms/TopNavigation/MobileNav.d.ts +32 -32
- package/dist/components/organisms/TopNavigation/MobileNav.js +45 -45
- package/dist/components/organisms/TopNavigation/TopNavigation.d.ts +33 -33
- package/dist/components/organisms/TopNavigation/TopNavigation.js +20 -20
- package/dist/components/organisms/TransferLine/TransferLine.d.ts +53 -53
- package/dist/components/organisms/TransferLine/TransferLine.js +179 -179
- package/dist/components/ui/button.d.ts +10 -10
- package/dist/components/ui/button.js +56 -56
- package/dist/components/ui/calendar.d.ts +8 -8
- package/dist/components/ui/calendar.js +87 -87
- package/dist/components/ui/popover.d.ts +7 -7
- package/dist/components/ui/popover.js +42 -42
- package/dist/hooks/useMobile.d.ts +5 -5
- package/dist/hooks/useMobile.js +26 -26
- package/dist/index.d.ts +49 -49
- package/dist/index.js +46 -46
- package/dist/lib/utils.d.ts +7 -7
- package/dist/lib/utils.js +13 -13
- package/dist/styles/components/avatar.css +122 -122
- package/dist/styles/components/calendar.css +140 -140
- package/dist/styles/components/checkbox.css +206 -206
- package/dist/styles/components/dropdown.css +269 -269
- package/dist/styles/components/forms.css +209 -209
- package/dist/styles/components/illustration.css +123 -123
- package/dist/styles/components/molecule/calendarInput.css +133 -133
- package/dist/styles/components/molecule/dateTime.css +126 -126
- package/dist/styles/components/molecule/location-dropdown.css +132 -132
- package/dist/styles/components/molecule/timePicker.css +122 -122
- package/dist/styles/components/multiselect-dropdown.css +286 -286
- package/dist/styles/components/organism/card-container.css +148 -148
- package/dist/styles/components/organism/dialog.css +168 -168
- package/dist/styles/components/organism/footer.css +119 -119
- package/dist/styles/components/organism/pax-selector.css +617 -617
- package/dist/styles/components/organism/round-trip.css +139 -139
- package/dist/styles/components/organism/search-bar-transfer.css +158 -161
- package/dist/styles/components/organism/topnavigation.css +143 -143
- package/dist/styles/components/organism/transfer-line.css +138 -138
- package/dist/styles/components/rating-star.css +145 -145
- package/dist/styles/components/rating-tab.css +179 -179
- package/dist/styles/components/scrollbar.css +155 -155
- package/dist/styles/components/segmented-button.css +214 -214
- package/dist/styles/components/selected-value.css +175 -175
- package/dist/styles/components/slider.css +182 -182
- package/dist/styles/components/typography.css +245 -245
- package/dist/styles/tokens/tokens.css +119 -119
- package/dist/styles/tokens/tokens.d.ts +3108 -3108
- package/dist/styles/tokens/tokens.js +2652 -2652
- package/package.json +103 -103
- package/src/components/atoms/Avatar/Avatar.tsx +60 -60
- package/src/components/atoms/Button/Button.css +200 -200
- package/src/components/atoms/Button/Button.tsx +82 -82
- package/src/components/atoms/Checkbox/Checkbox.tsx +83 -83
- package/src/components/atoms/Icon/Icon.tsx +163 -163
- package/src/components/atoms/Icon/icons/ArrivalIcon.tsx +52 -52
- package/src/components/atoms/Icon/icons/BuildingIcon.tsx +50 -50
- package/src/components/atoms/Icon/icons/CalendarIcon.tsx +63 -63
- package/src/components/atoms/Icon/icons/CalendarOutlineIcon.tsx +50 -50
- package/src/components/atoms/Icon/icons/CarIcon.tsx +44 -44
- package/src/components/atoms/Icon/icons/Check.tsx +36 -36
- package/src/components/atoms/Icon/icons/CheckCircleIcon.tsx +48 -48
- package/src/components/atoms/Icon/icons/Chevron.tsx +73 -73
- package/src/components/atoms/Icon/icons/ChevronDownIcon.tsx +46 -46
- package/src/components/atoms/Icon/icons/Close.tsx +39 -39
- package/src/components/atoms/Icon/icons/DeleteIcon.tsx +44 -44
- package/src/components/atoms/Icon/icons/DepartureIcon.tsx +50 -50
- package/src/components/atoms/Icon/icons/EyeIcon.tsx +44 -44
- package/src/components/atoms/Icon/icons/FacebookIcon.tsx +50 -50
- package/src/components/atoms/Icon/icons/HomeIcon.tsx +52 -52
- package/src/components/atoms/Icon/icons/InfoIcon.tsx +44 -44
- package/src/components/atoms/Icon/icons/LinkedInIcon.tsx +50 -50
- package/src/components/atoms/Icon/icons/MapPinIcon.tsx +44 -44
- package/src/components/atoms/Icon/icons/MautoucoLogo.tsx +93 -93
- package/src/components/atoms/Icon/icons/MenuIcon.tsx +49 -49
- package/src/components/atoms/Icon/icons/MinusIcon.tsx +45 -45
- package/src/components/atoms/Icon/icons/MoreIcon.tsx +44 -44
- package/src/components/atoms/Icon/icons/PlaneIcon.tsx +50 -50
- package/src/components/atoms/Icon/icons/PlusIcon.tsx +45 -45
- package/src/components/atoms/Icon/icons/Search.tsx +37 -37
- package/src/components/atoms/Icon/icons/Settings.tsx +38 -38
- package/src/components/atoms/Icon/icons/ShipIcon.tsx +50 -50
- package/src/components/atoms/Icon/icons/StrollerIcon.tsx +44 -44
- package/src/components/atoms/Icon/icons/TwitterIcon.tsx +50 -50
- package/src/components/atoms/Icon/icons/User.tsx +37 -37
- package/src/components/atoms/Icon/icons/UserIcon.tsx +63 -63
- package/src/components/atoms/Icon/icons/Youtube.tsx +50 -50
- package/src/components/atoms/Illustration/Illustration.tsx +28 -28
- package/src/components/atoms/Illustration/illustrations.ts +116 -116
- package/src/components/atoms/Inputs/DropdownInput/DropdownInput.tsx +96 -96
- package/src/components/atoms/Inputs/Textarea/Textarea.tsx +51 -51
- package/src/components/atoms/Link/Link.tsx +168 -168
- package/src/components/atoms/RatingStar/RatingStar.tsx +114 -114
- package/src/components/atoms/SegmentedButton/SegmentedButton.tsx +94 -94
- package/src/components/atoms/SelectedValue/SelectedValue.tsx +59 -59
- package/src/components/atoms/Slider/Slider.tsx +95 -95
- package/src/components/atoms/Spinner/Spinner.tsx +56 -56
- package/src/components/atoms/Spinner/variants/ButtonSpinner.tsx +37 -37
- package/src/components/atoms/Spinner/variants/LoadingSpinner.tsx +22 -22
- package/src/components/atoms/Tab/Tab.css +147 -147
- package/src/components/atoms/Tab/Tab.tsx +96 -96
- package/src/components/atoms/Typography/Typography.tsx +153 -153
- package/src/components/molecules/Calendar/CalendarInput.tsx +135 -135
- package/src/components/molecules/Calendar/DateTime.tsx +172 -172
- package/src/components/molecules/Calendar/TimePicker.tsx +174 -174
- package/src/components/molecules/LocationDropdown/LocationDropdown.tsx +234 -234
- package/src/components/molecules/LocationDropdown/index.ts +2 -2
- package/src/components/molecules/RatingTab/RatingTab.tsx +96 -96
- package/src/components/molecules/TabGroup/TabGroup.tsx +60 -60
- package/src/components/molecules/UserCard/UserCard.stories.tsx +36 -36
- package/src/components/molecules/UserCard/UserCard.tsx +173 -173
- package/src/components/organisms/CardContainer/CardContainer.tsx +66 -66
- package/src/components/organisms/DateTimePicker/DateTimePicker.tsx +110 -110
- package/src/components/organisms/Dialog/Dialog.tsx +352 -352
- package/src/components/organisms/Footer/Footer.tsx +290 -290
- package/src/components/organisms/PaxSelector/PaxSelector.tsx +979 -979
- package/src/components/organisms/RoundTrip/RoundTrip.tsx +335 -335
- package/src/components/organisms/RoundTrip/index.ts +2 -2
- package/src/components/organisms/SearchBarTransfer/SearchBarTransfer.tsx +388 -388
- package/src/components/organisms/SearchBarTransfer/index.ts +2 -2
- package/src/components/organisms/TopNavigation/DesktopNav.tsx +133 -122
- package/src/components/organisms/TopNavigation/MobileNav.tsx +212 -212
- package/src/components/organisms/TopNavigation/TopNavigation.tsx +45 -45
- package/src/components/organisms/TransferLine/TransferLine.tsx +369 -369
- package/src/components/ui/button.tsx +60 -60
- package/src/components/ui/calendar.tsx +246 -246
- package/src/components/ui/popover.tsx +46 -46
- package/src/styles/components/avatar.css +58 -58
- package/src/styles/components/calendar.css +85 -85
- package/src/styles/components/checkbox.css +130 -130
- package/src/styles/components/dropdown.css +214 -214
- package/src/styles/components/forms.css +147 -147
- package/src/styles/components/illustration.css +7 -7
- package/src/styles/components/molecule/calendarInput.css +156 -156
- package/src/styles/components/molecule/dateTime.css +14 -14
- package/src/styles/components/molecule/location-dropdown.css +204 -204
- package/src/styles/components/molecule/timePicker.css +78 -78
- package/src/styles/components/multiselect-dropdown.css +230 -230
- package/src/styles/components/organism/card-container.css +49 -49
- package/src/styles/components/organism/dialog.css +241 -241
- package/src/styles/components/organism/footer.css +113 -113
- package/src/styles/components/organism/pax-selector.css +702 -702
- package/src/styles/components/organism/round-trip.css +55 -55
- package/src/styles/components/organism/search-bar-transfer.css +128 -127
- package/src/styles/components/organism/topnavigation.css +161 -161
- package/src/styles/components/organism/transfer-line.css +86 -86
- package/src/styles/components/rating-star.css +39 -39
- package/src/styles/components/rating-tab.css +83 -83
- package/src/styles/components/scrollbar.css +63 -63
- package/src/styles/components/segmented-button.css +134 -134
- package/src/styles/components/selected-value.css +80 -80
- package/src/styles/components/slider.css +86 -86
- package/src/styles/components/typography.css +251 -251
- package/src/styles/fonts.css +50 -0
- package/src/styles/tokens/tokens.css +119 -119
- 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;
|