@qite/tide-booking-component 1.4.29 → 1.4.31
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/build/build-cjs/index.js +621 -2822
- package/build/build-cjs/search-results/store/search-results-slice.d.ts +6 -3
- package/build/build-cjs/search-results/types.d.ts +9 -12
- package/build/build-esm/index.js +617 -2625
- package/build/build-esm/search-results/store/search-results-slice.d.ts +6 -3
- package/build/build-esm/search-results/types.d.ts +9 -12
- package/package.json +2 -3
- package/src/booking-product/components/dates.tsx +9 -4
- package/src/booking-wizard/components/step-indicator.tsx +11 -2
- package/src/booking-wizard/features/booking/booking-slice.ts +27 -2
- package/src/booking-wizard/features/booking/booking.tsx +32 -15
- package/src/booking-wizard/features/booking/selectors.ts +6 -0
- package/src/booking-wizard/features/flight-options/index.tsx +27 -3
- package/src/booking-wizard/features/product-options/option-room.tsx +1 -1
- package/src/booking-wizard/features/product-options/options-form.tsx +14 -4
- package/src/booking-wizard/features/room-options/room.tsx +1 -1
- package/src/booking-wizard/features/sidebar/index.tsx +4 -1
- package/src/booking-wizard/features/sidebar/sidebar-util.ts +1 -3
- package/src/booking-wizard/features/sidebar/sidebar.tsx +112 -104
- package/src/booking-wizard/features/travelers-form/travelers-form-slice.ts +1 -0
- package/src/booking-wizard/features/travelers-form/travelers-form.tsx +146 -10
- package/src/booking-wizard/settings-context.ts +2 -1
- package/src/booking-wizard/types.ts +1 -0
- package/src/qsm/components/search-input-group/index.tsx +17 -8
- package/src/search-results/components/hotel/hotel-accommodation-results.tsx +78 -83
- package/src/search-results/components/hotel/hotel-card.tsx +54 -24
- package/src/search-results/components/icon.tsx +13 -0
- package/src/search-results/components/item-picker/index.tsx +5 -7
- package/src/search-results/components/search-results-container/search-results-container.tsx +72 -117
- package/src/search-results/components/tab-views/index.tsx +22 -3
- package/src/search-results/features/flights/flight-search-results-self-contained.tsx +0 -13
- package/src/search-results/features/hotels/hotel-flight-search-results-self-contained.tsx +1 -8
- package/src/search-results/features/hotels/hotel-search-results-self-contained.tsx +1 -14
- package/src/search-results/features/roundtrips/roundtrip-search-results-self-contained.tsx +1 -9
- package/src/search-results/store/search-results-slice.ts +11 -4
- package/src/search-results/types.ts +11 -16
- package/src/shared/translations/en-GB.json +5 -1
- package/src/shared/translations/fr-BE.json +5 -1
- package/src/shared/translations/nl-BE.json +5 -1
- package/styles/components/_form.scss +51 -2
- package/styles/components/_passenger-picker.scss +3 -2
- package/styles/components/_qsm.scss +1 -1
- package/styles/qsm/_qsm.scss +67 -6
|
@@ -31,6 +31,7 @@ interface SidebarProps {
|
|
|
31
31
|
headerComponent?: JSX.Element;
|
|
32
32
|
footerComponent?: JSX.Element;
|
|
33
33
|
loaderComponent?: JSX.Element;
|
|
34
|
+
isUnavailable?: boolean;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const Sidebar: React.FC<SidebarProps> = ({
|
|
@@ -52,7 +53,8 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|
|
52
53
|
isOnRequest,
|
|
53
54
|
headerComponent,
|
|
54
55
|
footerComponent,
|
|
55
|
-
loaderComponent
|
|
56
|
+
loaderComponent,
|
|
57
|
+
isUnavailable
|
|
56
58
|
}) => {
|
|
57
59
|
const [active, setActive] = useState<boolean>(false);
|
|
58
60
|
const translations = useSelector(selectTranslations);
|
|
@@ -75,127 +77,133 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|
|
75
77
|
<div className="booking__sidebar-frame">
|
|
76
78
|
<ProductCard productName={productName} thumbnailUrl={thumbnailUrl} handleToggleClick={handleToggleClick} />
|
|
77
79
|
<div className="pricing-summary">
|
|
78
|
-
|
|
79
|
-
<div className=
|
|
80
|
-
<div className=
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
travelerRooms
|
|
84
|
-
|
|
85
|
-
<div className="pricing-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
{!isUnavailable ? (
|
|
81
|
+
<div className="pricing-summary__wrapper">
|
|
82
|
+
<div className={`pricing-summary__region ${!isLoading ? 'pricing-summary__region--fade-in' : ''}`}>
|
|
83
|
+
<div className="pricing-summary__group">
|
|
84
|
+
<h6 className="pricing-summary__title">{translations.SIDEBAR.TRAVEL_INFO}</h6>
|
|
85
|
+
{!isEmpty(travelerRooms) &&
|
|
86
|
+
travelerRooms?.map((room, rIndex) => (
|
|
87
|
+
<div className="pricing-summary__row" key={rIndex}>
|
|
88
|
+
<div className="pricing-summary__property">
|
|
89
|
+
{travelerRooms.length > 1 && `${translations.SHARED.ROOM} ${rIndex + 1}`}
|
|
90
|
+
{travelerRooms.length === 1 && translations.ROOM_OPTIONS_FORM.TRAVELER_GROUP}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="pricing-summary__value">{room}</div>
|
|
88
94
|
</div>
|
|
95
|
+
))}
|
|
89
96
|
|
|
90
|
-
|
|
97
|
+
{startDateText && (
|
|
98
|
+
<div className="pricing-summary__row">
|
|
99
|
+
<div className="pricing-summary__property">
|
|
100
|
+
{startDateText && endDateText ? translations.SIDEBAR.DEPARTURE : translations.SIDEBAR.DEPARTURE_SINGLE}
|
|
101
|
+
</div>
|
|
102
|
+
<div className="pricing-summary__value">{startDateText}</div>
|
|
91
103
|
</div>
|
|
92
|
-
)
|
|
104
|
+
)}
|
|
93
105
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
{
|
|
106
|
+
{endDateText && (
|
|
107
|
+
<div className="pricing-summary__row">
|
|
108
|
+
<div className="pricing-summary__property">{translations.SIDEBAR.ARRIVAL}</div>
|
|
109
|
+
<div className="pricing-summary__value">{endDateText}</div>
|
|
98
110
|
</div>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)}
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
102
113
|
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<div className="pricing-summary__value">{endDateText}</div>
|
|
107
|
-
</div>
|
|
114
|
+
{isLoading && loaderComponent}
|
|
115
|
+
{!isLoading && departureFlightMetaData && (
|
|
116
|
+
<SidebarFlight title={translations.SIDEBAR.DEPARTURE_FLIGHT} flightMetaData={departureFlightMetaData} />
|
|
108
117
|
)}
|
|
109
|
-
</div>
|
|
110
|
-
|
|
111
|
-
{isLoading && loaderComponent}
|
|
112
|
-
{!isLoading && departureFlightMetaData && (
|
|
113
|
-
<SidebarFlight title={translations.SIDEBAR.DEPARTURE_FLIGHT} flightMetaData={departureFlightMetaData} />
|
|
114
|
-
)}
|
|
115
118
|
|
|
116
|
-
|
|
119
|
+
{!isLoading && returnFlightMetaData && <SidebarFlight title={translations.SIDEBAR.ARRIVAL_FLIGHT} flightMetaData={returnFlightMetaData} />}
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
121
|
+
{accommodations && (
|
|
122
|
+
<div className="pricing-summary__group">
|
|
123
|
+
<h6 className="pricing-summary__title">{translations.SIDEBAR.ACCOMMODATION}</h6>
|
|
124
|
+
{accommodations.map((accommodation) => {
|
|
125
|
+
let option = accommodation.options.find((x) => x.isSelected);
|
|
126
|
+
return (
|
|
127
|
+
<div key={accommodation.index}>
|
|
128
|
+
<div className="pricing-summary__row">
|
|
129
|
+
<div className="pricing-summary__property">
|
|
130
|
+
{option?.accommodationName}
|
|
131
|
+
{isOnRequest ? ` (${translations.SIDEBAR.ON_REQUEST})` : ''}
|
|
132
|
+
{option?.isOnRequest ? ` (${translations.SIDEBAR.ON_REQUEST})` : ''}
|
|
133
|
+
</div>
|
|
134
|
+
{/*<div className="pricing-summary__value">
|
|
132
135
|
{option && option.price > 0 && formatPrice(option?.price)}
|
|
133
136
|
</div>*/}
|
|
137
|
+
</div>
|
|
138
|
+
<div className="pricing-summary__row">
|
|
139
|
+
<div className="price-summarty__property">{option?.regimeName}</div>
|
|
140
|
+
<div className="price-summary__value">{!isFlightOnly && getDatePeriodText(translations, option?.from, option?.to, true)}</div>
|
|
141
|
+
</div>
|
|
134
142
|
</div>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
);
|
|
144
|
+
})}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{!isLoading && canShowPriceBreakdownSection && (
|
|
150
|
+
<div className={`pricing-summary__region ${!isLoading ? 'pricing-summary__region--fade-in' : ''}`}>
|
|
151
|
+
{basePrice !== undefined && basePrice > 0 && (
|
|
152
|
+
<div className="pricing-summary__group">
|
|
153
|
+
<div className="pricing-summary__row">
|
|
154
|
+
<div className="pricing-summary__property">
|
|
155
|
+
<h6 className="pricing-summary__title">{translations.SIDEBAR.BASE_PRICE}</h6>
|
|
138
156
|
</div>
|
|
157
|
+
<div className="pricing-summary__value">{formatPrice(basePrice, currencyCode)}</div>
|
|
139
158
|
</div>
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
{!isEmpty(includedCosts) && (
|
|
162
|
+
<div className="pricing-summary__group">
|
|
163
|
+
<h6 className="pricing-summary__title">{translations.SIDEBAR.INCLUDED_COSTS}</h6>
|
|
164
|
+
{includedCosts?.map((priceDetail, index) => (
|
|
165
|
+
<React.Fragment key={compact([priceDetail.productCode, priceDetail.accommodationCode, index]).join('_')}>
|
|
166
|
+
<div className="pricing-summary__row">
|
|
167
|
+
<div className="pricing-summary__property">{priceDetail.productName}</div>
|
|
168
|
+
{priceDetail.showPrice && (
|
|
169
|
+
<div className="pricing-summary__value">{formatPrice(priceDetail.price * priceDetail.amount, currencyCode)}</div>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
<div className="pricing-summary__row">
|
|
173
|
+
<div className="price-summary__property">{priceDetail.accommodationName ?? priceDetail.accommodationCode}</div>
|
|
174
|
+
</div>
|
|
175
|
+
</React.Fragment>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
{!isEmpty(extraCosts) && (
|
|
180
|
+
<div className="pricing-summary__group">
|
|
181
|
+
<h6 className="pricing-summary__title">{translations.SIDEBAR.EXTRA_COSTS}</h6>
|
|
182
|
+
{extraCosts?.map((priceDetail, index) => (
|
|
183
|
+
<React.Fragment key={compact([priceDetail.productCode, priceDetail.accommodationCode, index]).join('_')}>
|
|
184
|
+
<div className="pricing-summary__row">
|
|
185
|
+
<div className="pricing-summary__property">{priceDetail.productName}</div>
|
|
186
|
+
{priceDetail.showPrice && (
|
|
187
|
+
<div className="pricing-summary__value">{formatPrice(priceDetail.price * priceDetail.amount, currencyCode)}</div>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
<div className="pricing-summary__row">
|
|
191
|
+
<div className="price-summary__property">{priceDetail.accommodationName ?? priceDetail.accommodationCode}</div>
|
|
192
|
+
</div>
|
|
193
|
+
</React.Fragment>
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
142
197
|
</div>
|
|
143
198
|
)}
|
|
144
199
|
</div>
|
|
200
|
+
) : (
|
|
201
|
+
<div className="pricing-summary__region">
|
|
202
|
+
<h6 className="pricing-summary__title">{translations.SIDEBAR.PACKAGE_NOT_AVAILABLE}</h6>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
145
205
|
|
|
146
|
-
|
|
147
|
-
<div className={`pricing-summary__region ${!isLoading ? 'pricing-summary__region--fade-in' : ''}`}>
|
|
148
|
-
{basePrice !== undefined && basePrice > 0 && (
|
|
149
|
-
<div className="pricing-summary__group">
|
|
150
|
-
<div className="pricing-summary__row">
|
|
151
|
-
<div className="pricing-summary__property">
|
|
152
|
-
<h6 className="pricing-summary__title">{translations.SIDEBAR.BASE_PRICE}</h6>
|
|
153
|
-
</div>
|
|
154
|
-
<div className="pricing-summary__value">{formatPrice(basePrice, currencyCode)}</div>
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
158
|
-
{!isEmpty(includedCosts) && (
|
|
159
|
-
<div className="pricing-summary__group">
|
|
160
|
-
<h6 className="pricing-summary__title">{translations.SIDEBAR.INCLUDED_COSTS}</h6>
|
|
161
|
-
{includedCosts?.map((priceDetail, index) => (
|
|
162
|
-
<React.Fragment key={compact([priceDetail.productCode, priceDetail.accommodationCode, index]).join('_')}>
|
|
163
|
-
<div className="pricing-summary__row">
|
|
164
|
-
<div className="pricing-summary__property">{priceDetail.productName}</div>
|
|
165
|
-
{priceDetail.showPrice && (
|
|
166
|
-
<div className="pricing-summary__value">{formatPrice(priceDetail.price * priceDetail.amount, currencyCode)}</div>
|
|
167
|
-
)}
|
|
168
|
-
</div>
|
|
169
|
-
<div className="pricing-summary__row">
|
|
170
|
-
<div className="price-summary__property">{priceDetail.accommodationName ?? priceDetail.accommodationCode}</div>
|
|
171
|
-
</div>
|
|
172
|
-
</React.Fragment>
|
|
173
|
-
))}
|
|
174
|
-
</div>
|
|
175
|
-
)}
|
|
176
|
-
{!isEmpty(extraCosts) && (
|
|
177
|
-
<div className="pricing-summary__group">
|
|
178
|
-
<h6 className="pricing-summary__title">{translations.SIDEBAR.EXTRA_COSTS}</h6>
|
|
179
|
-
{extraCosts?.map((priceDetail, index) => (
|
|
180
|
-
<React.Fragment key={compact([priceDetail.productCode, priceDetail.accommodationCode, index]).join('_')}>
|
|
181
|
-
<div className="pricing-summary__row">
|
|
182
|
-
<div className="pricing-summary__property">{priceDetail.productName}</div>
|
|
183
|
-
{priceDetail.showPrice && (
|
|
184
|
-
<div className="pricing-summary__value">{formatPrice(priceDetail.price * priceDetail.amount, currencyCode)}</div>
|
|
185
|
-
)}
|
|
186
|
-
</div>
|
|
187
|
-
<div className="pricing-summary__row">
|
|
188
|
-
<div className="price-summary__property">{priceDetail.accommodationName ?? priceDetail.accommodationCode}</div>
|
|
189
|
-
</div>
|
|
190
|
-
</React.Fragment>
|
|
191
|
-
))}
|
|
192
|
-
</div>
|
|
193
|
-
)}
|
|
194
|
-
</div>
|
|
195
|
-
)}
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
{!isLoading && canShowTotalPriceSection && (
|
|
206
|
+
{!isLoading && canShowTotalPriceSection && !isUnavailable && (
|
|
199
207
|
<div className={`pricing-summary__region pricing-summary__region--pricing ${!isLoading ? 'pricing-summary__region--fade-in' : ''}`}>
|
|
200
208
|
{deposit && remainingAmount > 0 ? (
|
|
201
209
|
<div className="pricing-summary__group">
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { compact, get, sortBy } from 'lodash';
|
|
2
2
|
import React, { useContext, useEffect, useState } from 'react';
|
|
3
3
|
import { useSelector } from 'react-redux';
|
|
4
|
-
import { setCurrentStep } from '../booking/booking-slice';
|
|
4
|
+
import { fetchPackage, setCurrentStep, setHasMounted, setIsFetching } from '../booking/booking-slice';
|
|
5
|
+
import { selectBookingAttributes, selectHasMounted, selectIsFetching, selectIsUnavailable, selectTravelersFirstStep } from '../booking/selectors';
|
|
5
6
|
import { selectFormRooms, selectTravelersFormValues, setFormValues } from './travelers-form-slice';
|
|
6
7
|
|
|
7
8
|
import { Link, navigate } from '@reach/router';
|
|
@@ -16,12 +17,12 @@ import SettingsContext from '../../settings-context';
|
|
|
16
17
|
import { useAppDispatch } from '../../store';
|
|
17
18
|
import { RoomTraveler, Traveler, TravelersFormValues } from '../../types';
|
|
18
19
|
import { setBookingType } from '../booking/booking-slice';
|
|
19
|
-
import { OPTIONS_FORM_STEP, SUMMARY_STEP } from '../booking/constants';
|
|
20
|
+
import { FLIGHT_OPTIONS_FORM_STEP, OPTIONS_FORM_STEP, SUMMARY_STEP } from '../booking/constants';
|
|
20
21
|
import { selectAgentAdressId, selectAgents, selectBookingQueryString, selectBookingType, selectStartDate, selectTranslations } from '../booking/selectors';
|
|
21
22
|
import { fetchPriceDetails } from '../price-details/price-details-slice';
|
|
23
|
+
import GenderControl from './controls/gender-control';
|
|
22
24
|
import TypeAheadInput from './type-ahead-input';
|
|
23
25
|
import validateForm from './validate-form';
|
|
24
|
-
import GenderControl from './controls/gender-control';
|
|
25
26
|
import PhoneInput from '../../components/phone-input';
|
|
26
27
|
|
|
27
28
|
interface TravelersFormProps {}
|
|
@@ -94,6 +95,12 @@ const TravelersForm: React.FC<TravelersFormProps> = () => {
|
|
|
94
95
|
const agents = useSelector(selectAgents);
|
|
95
96
|
const agentAdressId = useSelector(selectAgentAdressId);
|
|
96
97
|
const translations = useSelector(selectTranslations);
|
|
98
|
+
const travelersFirstStep = useSelector(selectTravelersFirstStep);
|
|
99
|
+
const isUnavailable = useSelector(selectIsUnavailable);
|
|
100
|
+
const bookingAttributes = useSelector(selectBookingAttributes);
|
|
101
|
+
const isFetching = useSelector(selectIsFetching);
|
|
102
|
+
const hasMounted = useSelector(selectHasMounted);
|
|
103
|
+
|
|
97
104
|
const useCompactForm = !!settings.travellers.compactForm && !!settings.agentAdressId;
|
|
98
105
|
|
|
99
106
|
const initialValues =
|
|
@@ -135,12 +142,20 @@ const TravelersForm: React.FC<TravelersFormProps> = () => {
|
|
|
135
142
|
validateForm(values, settings.agentRequired, bookingType, translations, settings.travellers.formFields, settings.travellers.mainBookerFormFields),
|
|
136
143
|
onSubmit: (values) => {
|
|
137
144
|
dispatch(setFormValues(values));
|
|
138
|
-
dispatch(
|
|
145
|
+
dispatch(fetchPackage());
|
|
139
146
|
|
|
140
147
|
if (settings.skipRouter) {
|
|
141
|
-
|
|
148
|
+
if (travelersFirstStep) {
|
|
149
|
+
dispatch(setCurrentStep(FLIGHT_OPTIONS_FORM_STEP));
|
|
150
|
+
} else {
|
|
151
|
+
dispatch(setCurrentStep(SUMMARY_STEP));
|
|
152
|
+
}
|
|
142
153
|
} else {
|
|
143
|
-
|
|
154
|
+
if (travelersFirstStep) {
|
|
155
|
+
navigate(`${settings.basePath}${settings.flightOptions.pathSuffix}?${bookingQueryString}`);
|
|
156
|
+
} else {
|
|
157
|
+
navigate(`${settings.basePath}${settings.summary.pathSuffix}?${bookingQueryString}`);
|
|
158
|
+
}
|
|
144
159
|
}
|
|
145
160
|
}
|
|
146
161
|
});
|
|
@@ -149,6 +164,51 @@ const TravelersForm: React.FC<TravelersFormProps> = () => {
|
|
|
149
164
|
dispatch(fetchPriceDetails());
|
|
150
165
|
}, []);
|
|
151
166
|
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!bookingAttributes?.rooms?.length || isFetching) return;
|
|
169
|
+
|
|
170
|
+
if (!hasMounted) {
|
|
171
|
+
dispatch(setHasMounted(true));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const fetchAll = async () => {
|
|
176
|
+
dispatch(setIsFetching(true));
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await dispatch(fetchPackage());
|
|
180
|
+
await dispatch(fetchPriceDetails());
|
|
181
|
+
} finally {
|
|
182
|
+
dispatch(setIsFetching(false));
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
fetchAll();
|
|
187
|
+
}, [bookingAttributes?.rooms]);
|
|
188
|
+
|
|
189
|
+
// Update URL querystring when form data changes
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (settings.skipRouter || !travelersFirstStep) return;
|
|
192
|
+
const params = new URLSearchParams(bookingQueryString);
|
|
193
|
+
params.delete('rooms');
|
|
194
|
+
const roomsString = formik.values.rooms
|
|
195
|
+
.map((room) => {
|
|
196
|
+
const adults = room.adults ? room.adults.length : 0;
|
|
197
|
+
const childAges = room.children && room.children.length ? room.children.map((c) => c.age).join(',') : '';
|
|
198
|
+
return `adults:${adults},childAges:(${childAges})`;
|
|
199
|
+
})
|
|
200
|
+
.map((s) => `(${s})`)
|
|
201
|
+
.join(',');
|
|
202
|
+
let query = params.toString();
|
|
203
|
+
if (query) {
|
|
204
|
+
query += `&rooms=(${roomsString})`;
|
|
205
|
+
} else {
|
|
206
|
+
query = `rooms=(${roomsString})`;
|
|
207
|
+
}
|
|
208
|
+
const newUrl = `${window.location.pathname}?${query}`;
|
|
209
|
+
navigate(newUrl, { replace: true });
|
|
210
|
+
}, [formik.values]);
|
|
211
|
+
|
|
152
212
|
useEffect(() => {
|
|
153
213
|
if (agents && settings.affiliateSlug) {
|
|
154
214
|
const agent = agents.find((x) => x.affiliateSlug && x.affiliateSlug === settings.affiliateSlug);
|
|
@@ -206,6 +266,55 @@ const TravelersForm: React.FC<TravelersFormProps> = () => {
|
|
|
206
266
|
dispatch(setBookingType('b2c'));
|
|
207
267
|
};
|
|
208
268
|
|
|
269
|
+
const handleAddTraveler = (roomIndex: number) => {
|
|
270
|
+
const rooms = [...formik.values.rooms];
|
|
271
|
+
const newAdult = {
|
|
272
|
+
id: Date.now(),
|
|
273
|
+
firstName: '',
|
|
274
|
+
lastName: '',
|
|
275
|
+
birthDate: '',
|
|
276
|
+
gender: ''
|
|
277
|
+
};
|
|
278
|
+
rooms[roomIndex] = {
|
|
279
|
+
...rooms[roomIndex],
|
|
280
|
+
adults: [...rooms[roomIndex].adults, newAdult]
|
|
281
|
+
};
|
|
282
|
+
formik.setFieldValue('rooms', rooms);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const handleRemoveTraveler = (roomIndex: number, travelerIndex: number) => {
|
|
286
|
+
const rooms = [...formik.values.rooms];
|
|
287
|
+
const adults = [...rooms[roomIndex].adults];
|
|
288
|
+
if (adults.length <= 1) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
adults.splice(travelerIndex, 1);
|
|
292
|
+
rooms[roomIndex] = {
|
|
293
|
+
...rooms[roomIndex],
|
|
294
|
+
adults
|
|
295
|
+
};
|
|
296
|
+
formik.setFieldValue('rooms', rooms);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const handleAddRoom = () => {
|
|
300
|
+
const rooms = [...formik.values.rooms];
|
|
301
|
+
const newAdult = {
|
|
302
|
+
id: Date.now(),
|
|
303
|
+
firstName: '',
|
|
304
|
+
lastName: '',
|
|
305
|
+
birthDate: '',
|
|
306
|
+
gender: ''
|
|
307
|
+
};
|
|
308
|
+
rooms.push({ adults: [newAdult], children: [] });
|
|
309
|
+
formik.setFieldValue('rooms', rooms);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const handleRemoveRoom = (roomIndex: number) => {
|
|
313
|
+
const rooms = [...formik.values.rooms];
|
|
314
|
+
rooms.splice(roomIndex, 1);
|
|
315
|
+
formik.setFieldValue('rooms', rooms);
|
|
316
|
+
};
|
|
317
|
+
|
|
209
318
|
const toggleAgent = (value: boolean) => {
|
|
210
319
|
setShowAgents(value);
|
|
211
320
|
|
|
@@ -440,6 +549,7 @@ const TravelersForm: React.FC<TravelersFormProps> = () => {
|
|
|
440
549
|
) : (
|
|
441
550
|
<>
|
|
442
551
|
<div className="form__travelers__wrapper">
|
|
552
|
+
{/* map each room */}
|
|
443
553
|
{formik.values.rooms.map((room, rIndex) => (
|
|
444
554
|
<div key={rIndex}>
|
|
445
555
|
{formik.values.rooms.length > 1 && (
|
|
@@ -460,8 +570,14 @@ const TravelersForm: React.FC<TravelersFormProps> = () => {
|
|
|
460
570
|
]).join('')}
|
|
461
571
|
</p>
|
|
462
572
|
</div>
|
|
573
|
+
{travelersFirstStep && formik.values.rooms.length > 1 && (
|
|
574
|
+
<button type="button" className="cta cta--secondary" onClick={() => handleRemoveRoom(rIndex)}>
|
|
575
|
+
Verwijder reisgezelschap
|
|
576
|
+
</button>
|
|
577
|
+
)}
|
|
463
578
|
</div>
|
|
464
579
|
)}
|
|
580
|
+
{/* map adults here for the room */}
|
|
465
581
|
{room.adults.map((travelerValues, index) => (
|
|
466
582
|
<div className="form__region" key={travelerValues.id}>
|
|
467
583
|
<div className="form__region-header">
|
|
@@ -581,6 +697,11 @@ const TravelersForm: React.FC<TravelersFormProps> = () => {
|
|
|
581
697
|
value={travelerValues.birthDate}
|
|
582
698
|
/>
|
|
583
699
|
</div>
|
|
700
|
+
{travelersFirstStep && room.adults.length > 1 && (
|
|
701
|
+
<button type="button" className="cta cta--secondary" onClick={() => handleRemoveTraveler(rIndex, index)}>
|
|
702
|
+
{translations.TRAVELERS_FORM.REMOVE_TRAVELER}
|
|
703
|
+
</button>
|
|
704
|
+
)}
|
|
584
705
|
</>
|
|
585
706
|
)}
|
|
586
707
|
</div>
|
|
@@ -694,6 +815,13 @@ const TravelersForm: React.FC<TravelersFormProps> = () => {
|
|
|
694
815
|
)}
|
|
695
816
|
</div>
|
|
696
817
|
))}
|
|
818
|
+
{travelersFirstStep && (
|
|
819
|
+
<div className="form__region">
|
|
820
|
+
<button type="button" className="cta cta--select" onClick={() => handleAddTraveler(rIndex)}>
|
|
821
|
+
{translations.TRAVELERS_FORM.ADD_TRAVELER}
|
|
822
|
+
</button>
|
|
823
|
+
</div>
|
|
824
|
+
)}
|
|
697
825
|
</div>
|
|
698
826
|
))}
|
|
699
827
|
|
|
@@ -918,20 +1046,28 @@ const TravelersForm: React.FC<TravelersFormProps> = () => {
|
|
|
918
1046
|
)}
|
|
919
1047
|
</>
|
|
920
1048
|
)}
|
|
1049
|
+
{travelersFirstStep && (
|
|
1050
|
+
<div className="booking__navigator">
|
|
1051
|
+
<button type="button" className="cta cta--select" onClick={handleAddRoom}>
|
|
1052
|
+
{translations.TRAVELERS_FORM.ADD_ROOM}
|
|
1053
|
+
</button>
|
|
1054
|
+
</div>
|
|
1055
|
+
)}
|
|
1056
|
+
|
|
921
1057
|
<div className="booking__navigator">
|
|
922
|
-
{settings.skipRouter ? (
|
|
1058
|
+
{!travelersFirstStep && settings.skipRouter ? (
|
|
923
1059
|
<button type="button" title={translations.STEPS.PREVIOUS} onClick={() => goPrevious()} className="cta cta--secondary">
|
|
924
1060
|
{translations.STEPS.PREVIOUS}
|
|
925
1061
|
</button>
|
|
926
|
-
) : (
|
|
1062
|
+
) : !travelersFirstStep ? (
|
|
927
1063
|
<Link
|
|
928
1064
|
to={`${settings.basePath}${settings.options.pathSuffix}?${bookingQueryString}`}
|
|
929
1065
|
title={translations.STEPS.PREVIOUS}
|
|
930
1066
|
className="cta cta--secondary">
|
|
931
1067
|
{translations.STEPS.PREVIOUS}
|
|
932
1068
|
</Link>
|
|
933
|
-
)}
|
|
934
|
-
<button type="submit" title={translations.STEPS.NEXT} className=
|
|
1069
|
+
) : null}
|
|
1070
|
+
<button type="submit" title={translations.STEPS.NEXT} className={'cta' + (isUnavailable ? ' cta--disabled' : '')}>
|
|
935
1071
|
{translations.STEPS.NEXT}
|
|
936
1072
|
</button>
|
|
937
1073
|
</div>
|
|
@@ -25,7 +25,8 @@ const SettingsContext = React.createContext<WizardSettingsContextProps>({
|
|
|
25
25
|
reportPrintActionId: null
|
|
26
26
|
},
|
|
27
27
|
travellers: {
|
|
28
|
-
pathSuffix: '/reizigers'
|
|
28
|
+
pathSuffix: '/reizigers',
|
|
29
|
+
travelersFirstStep: false
|
|
29
30
|
},
|
|
30
31
|
summary: {
|
|
31
32
|
pathSuffix: '/samenvatting',
|
|
@@ -74,22 +74,30 @@ const SearchInputGroup: React.FC<Props> = ({
|
|
|
74
74
|
|
|
75
75
|
if (small) return;
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
dispatch(setActiveSearchField(fieldKey));
|
|
78
|
+
dispatch(setSearchResults(match(input)));
|
|
79
|
+
},
|
|
80
|
+
[dispatch, fieldKey, small, match, options]
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const handleKeyDown = useCallback(
|
|
84
|
+
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
85
|
+
if (!['Tab', 'Enter'].includes(e.key)) return;
|
|
86
|
+
|
|
87
|
+
if (value.length === 3 && autoComplete) {
|
|
88
|
+
const exactIataMatch = findExactIataMatch(options, value);
|
|
79
89
|
|
|
80
90
|
if (exactIataMatch) {
|
|
91
|
+
if (e.key === 'Enter') {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
}
|
|
81
94
|
dispatch(setFieldValue({ fieldKey, value: exactIataMatch.value }));
|
|
82
95
|
dispatch(setSearchResults([]));
|
|
83
96
|
dispatch(setActiveSearchField(null));
|
|
84
|
-
return;
|
|
85
97
|
}
|
|
86
98
|
}
|
|
87
|
-
|
|
88
|
-
// Normal typeahead behavior
|
|
89
|
-
dispatch(setActiveSearchField(fieldKey));
|
|
90
|
-
dispatch(setSearchResults(match(input)));
|
|
91
99
|
},
|
|
92
|
-
[
|
|
100
|
+
[value, autoComplete, options, dispatch, fieldKey]
|
|
93
101
|
);
|
|
94
102
|
|
|
95
103
|
const handleOptionSelect = useCallback(
|
|
@@ -155,6 +163,7 @@ const SearchInputGroup: React.FC<Props> = ({
|
|
|
155
163
|
readOnly={small}
|
|
156
164
|
onFocus={click}
|
|
157
165
|
onClick={(e) => e.stopPropagation()}
|
|
166
|
+
onKeyDown={handleKeyDown}
|
|
158
167
|
onChange={(e) => !small && !readOnlyForced && handleInputChange(e.target.value)}
|
|
159
168
|
className={`qsm__input${isSecondInput ? ' qsm__input--splittable' : ' u-ps-2'}`}
|
|
160
169
|
placeholder={placeholder}
|