@qite/tide-booking-component 1.4.103 → 1.4.105
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 +2782 -1052
- package/build/build-cjs/src/search-results/components/excursions/day-by-day-excursions.d.ts +4 -0
- package/build/build-cjs/src/search-results/components/excursions/excursion-details.d.ts +3 -0
- package/build/build-cjs/src/search-results/components/excursions/excursion-results.d.ts +8 -0
- package/build/build-cjs/src/search-results/components/filters/filters.d.ts +2 -0
- package/build/build-cjs/src/search-results/components/hotel/hotel-accommodation-results.d.ts +1 -0
- package/build/build-cjs/src/search-results/store/search-results-selectors.d.ts +546 -0
- package/build/build-cjs/src/search-results/store/search-results-slice.d.ts +55 -8
- package/build/build-cjs/src/search-results/types.d.ts +40 -2
- package/build/build-cjs/src/search-results/utils/query-utils.d.ts +1 -0
- package/build/build-cjs/src/search-results/utils/search-results-utils.d.ts +8 -6
- package/build/build-cjs/src/shared/components/flyin/flyin.d.ts +4 -3
- package/build/build-cjs/src/shared/components/flyin/packaging-flights-flyin.d.ts +7 -0
- package/build/build-cjs/src/shared/utils/localization-util.d.ts +3 -0
- package/build/build-cjs/src/shared/utils/tide-api-utils.d.ts +6 -0
- package/build/build-esm/index.js +2735 -1023
- package/build/build-esm/src/search-results/components/excursions/day-by-day-excursions.d.ts +4 -0
- package/build/build-esm/src/search-results/components/excursions/excursion-details.d.ts +3 -0
- package/build/build-esm/src/search-results/components/excursions/excursion-results.d.ts +8 -0
- package/build/build-esm/src/search-results/components/filters/filters.d.ts +2 -0
- package/build/build-esm/src/search-results/components/hotel/hotel-accommodation-results.d.ts +1 -0
- package/build/build-esm/src/search-results/store/search-results-selectors.d.ts +546 -0
- package/build/build-esm/src/search-results/store/search-results-slice.d.ts +55 -8
- package/build/build-esm/src/search-results/types.d.ts +40 -2
- package/build/build-esm/src/search-results/utils/query-utils.d.ts +1 -0
- package/build/build-esm/src/search-results/utils/search-results-utils.d.ts +8 -6
- package/build/build-esm/src/shared/components/flyin/flyin.d.ts +4 -3
- package/build/build-esm/src/shared/components/flyin/packaging-flights-flyin.d.ts +7 -0
- package/build/build-esm/src/shared/utils/localization-util.d.ts +3 -0
- package/build/build-esm/src/shared/utils/tide-api-utils.d.ts +6 -0
- package/package.json +2 -2
- package/src/booking-wizard/features/flight-options/index.tsx +6 -2
- package/src/search-results/components/excursions/day-by-day-excursions.tsx +169 -0
- package/src/search-results/components/excursions/excursion-details.tsx +340 -0
- package/src/search-results/components/excursions/excursion-results.tsx +186 -0
- package/src/search-results/components/filters/filters.tsx +8 -9
- package/src/search-results/components/hotel/hotel-accommodation-results.tsx +81 -24
- package/src/search-results/components/hotel/hotel-card.tsx +0 -3
- package/src/search-results/components/icon.tsx +1 -4
- package/src/search-results/components/search-results-container/search-results-container.tsx +208 -130
- package/src/search-results/store/search-results-selectors.ts +84 -0
- package/src/search-results/store/search-results-slice.ts +138 -15
- package/src/search-results/types.ts +55 -2
- package/src/search-results/utils/query-utils.ts +1 -0
- package/src/search-results/utils/search-results-utils.ts +310 -58
- package/src/shared/components/flyin/accommodation-flyin.tsx +4 -2
- package/src/shared/components/flyin/flights-flyin.tsx +3 -1
- package/src/shared/components/flyin/flyin.tsx +116 -21
- package/src/shared/components/flyin/group-tour-flyin.tsx +3 -1
- package/src/shared/components/flyin/packaging-flights-flyin.tsx +164 -0
- package/src/shared/translations/ar-SA.json +4 -2
- package/src/shared/translations/da-DK.json +4 -2
- package/src/shared/translations/de-DE.json +4 -2
- package/src/shared/translations/en-GB.json +4 -2
- package/src/shared/translations/es-ES.json +4 -2
- package/src/shared/translations/fr-BE.json +4 -2
- package/src/shared/translations/fr-FR.json +4 -2
- package/src/shared/translations/is-IS.json +4 -2
- package/src/shared/translations/it-IT.json +4 -2
- package/src/shared/translations/ja-JP.json +4 -2
- package/src/shared/translations/nl-BE.json +4 -2
- package/src/shared/translations/nl-NL.json +4 -2
- package/src/shared/translations/no-NO.json +4 -2
- package/src/shared/translations/pl-PL.json +4 -2
- package/src/shared/translations/pt-PT.json +4 -2
- package/src/shared/translations/sv-SE.json +4 -2
- package/src/shared/utils/localization-util.ts +14 -0
- package/src/shared/utils/tide-api-utils.ts +8 -0
- package/styles/components/_flyin.scss +16 -0
- package/styles/components/_search.scss +15 -2
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import React, { useContext, useMemo } from 'react';
|
|
2
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
3
|
+
import { first } from 'lodash';
|
|
4
|
+
import { SearchResultsRootState } from '../../store/search-results-store';
|
|
5
|
+
import { getTranslations } from '../../../shared/utils/localization-util';
|
|
6
|
+
import SearchResultsConfigurationContext from '../../search-results-configuration-context';
|
|
7
|
+
import { PackagingAccommodationResponse } from '@qite/tide-client';
|
|
8
|
+
import { confirmExcursionForDay, setFlyInIsOpen, setSelectedExcursionSearchResult } from '../../store/search-results-slice';
|
|
9
|
+
import { format, parseISO } from 'date-fns';
|
|
10
|
+
|
|
11
|
+
type ExcursionOption = PackagingAccommodationResponse['rooms'][number]['options'][number];
|
|
12
|
+
|
|
13
|
+
type GroupedExcursion = {
|
|
14
|
+
accommodationCode: string;
|
|
15
|
+
accommodationName: string;
|
|
16
|
+
options: ExcursionOption[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const formatPrice = (price?: number, currencyCode?: string | null) => {
|
|
20
|
+
if (typeof price !== 'number') return '';
|
|
21
|
+
|
|
22
|
+
return new Intl.NumberFormat('nl-BE', {
|
|
23
|
+
style: 'currency',
|
|
24
|
+
currency: currencyCode ?? 'EUR'
|
|
25
|
+
}).format(price);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const getExcursionDayKey = (date: string | Date) => {
|
|
29
|
+
const parsed = typeof date === 'string' ? parseISO(date) : date;
|
|
30
|
+
return format(parsed, 'yyyy-MM-dd');
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getOptionPaxIds = (option: ExcursionOption): number[] => {
|
|
34
|
+
return Array.isArray(option.paxIds) ? Array.from(new Set(option.paxIds)).sort((a, b) => a - b) : [];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const optionAppliesToPax = (option: ExcursionOption, paxId: number) => {
|
|
38
|
+
return getOptionPaxIds(option).includes(paxId);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const optionAppliesToAllTravellers = (option: ExcursionOption, travellerCount: number) => {
|
|
42
|
+
const paxIds = getOptionPaxIds(option);
|
|
43
|
+
const expected = Array.from({ length: travellerCount }, (_, i) => i);
|
|
44
|
+
|
|
45
|
+
return paxIds.length === expected.length && paxIds.every((id, index) => id === expected[index]);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const groupOptionsByExcursion = (options: ExcursionOption[]): GroupedExcursion[] => {
|
|
49
|
+
const groupedMap = new Map<string, GroupedExcursion>();
|
|
50
|
+
|
|
51
|
+
options.forEach((option) => {
|
|
52
|
+
const key = option.accommodationCode;
|
|
53
|
+
|
|
54
|
+
if (!groupedMap.has(key)) {
|
|
55
|
+
groupedMap.set(key, {
|
|
56
|
+
accommodationCode: option.accommodationCode,
|
|
57
|
+
accommodationName: option.accommodationName,
|
|
58
|
+
options: []
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
groupedMap.get(key)!.options.push(option);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return Array.from(groupedMap.values());
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const ExcursionDetails: React.FC = () => {
|
|
69
|
+
const context = useContext(SearchResultsConfigurationContext);
|
|
70
|
+
const dispatch = useDispatch();
|
|
71
|
+
|
|
72
|
+
const { selectedExcursionSearchResult, editablePackagingEntry, excursionSearchParams } = useSelector((state: SearchResultsRootState) => state.searchResults);
|
|
73
|
+
|
|
74
|
+
if (!context || !selectedExcursionSearchResult || !editablePackagingEntry || !excursionSearchParams?.date) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const translations = getTranslations(context.languageCode ?? 'en-GB');
|
|
79
|
+
const travellerCount = editablePackagingEntry.pax.length;
|
|
80
|
+
|
|
81
|
+
const allOptions = useMemo(() => {
|
|
82
|
+
return selectedExcursionSearchResult.rooms.flatMap((room) => room.options ?? []);
|
|
83
|
+
}, [selectedExcursionSearchResult]);
|
|
84
|
+
|
|
85
|
+
const sharedOptions = useMemo(() => {
|
|
86
|
+
return allOptions.filter((option) => optionAppliesToAllTravellers(option, travellerCount));
|
|
87
|
+
}, [allOptions, travellerCount]);
|
|
88
|
+
|
|
89
|
+
const sharedExcursions = useMemo(() => {
|
|
90
|
+
return groupOptionsByExcursion(sharedOptions);
|
|
91
|
+
}, [sharedOptions]);
|
|
92
|
+
|
|
93
|
+
const paxGroups = useMemo(() => {
|
|
94
|
+
return editablePackagingEntry.pax.map((pax) => {
|
|
95
|
+
const paxOptions = allOptions.filter((option) => optionAppliesToPax(option, pax.id) && !optionAppliesToAllTravellers(option, travellerCount));
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
pax,
|
|
99
|
+
paxId: pax.id,
|
|
100
|
+
excursions: groupOptionsByExcursion(paxOptions)
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}, [editablePackagingEntry.pax, allOptions, travellerCount]);
|
|
104
|
+
|
|
105
|
+
const getSelectedSharedOption = () => {
|
|
106
|
+
return sharedOptions.find((option) => option.isSelected);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const getSelectedSharedOptionForExcursion = (accommodationCode: string) => {
|
|
110
|
+
return sharedOptions.find((option) => option.accommodationCode === accommodationCode && option.isSelected);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const getSelectedOptionForPax = (paxId: number) => {
|
|
114
|
+
return allOptions.find((option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerCount) && option.isSelected);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const getSelectedOptionForExcursion = (paxId: number, accommodationCode: string) => {
|
|
118
|
+
return allOptions.find(
|
|
119
|
+
(option) =>
|
|
120
|
+
optionAppliesToPax(option, paxId) &&
|
|
121
|
+
!optionAppliesToAllTravellers(option, travellerCount) &&
|
|
122
|
+
option.accommodationCode === accommodationCode &&
|
|
123
|
+
option.isSelected
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const handlePick = (selectedGuid?: string, paxId?: number) => {
|
|
128
|
+
const updatedExcursionSearchResult: PackagingAccommodationResponse = {
|
|
129
|
+
...selectedExcursionSearchResult,
|
|
130
|
+
rooms: selectedExcursionSearchResult.rooms.map((room) => ({
|
|
131
|
+
...room,
|
|
132
|
+
options: room.options.map((option) => {
|
|
133
|
+
const isSharedOption = optionAppliesToAllTravellers(option, travellerCount);
|
|
134
|
+
|
|
135
|
+
if (paxId === undefined) {
|
|
136
|
+
if (!isSharedOption) {
|
|
137
|
+
return option;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
...option,
|
|
142
|
+
isSelected: option.guid === selectedGuid
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (isSharedOption || !optionAppliesToPax(option, paxId)) {
|
|
147
|
+
return option;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
...option,
|
|
152
|
+
isSelected: option.guid === selectedGuid
|
|
153
|
+
};
|
|
154
|
+
})
|
|
155
|
+
}))
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
dispatch(setSelectedExcursionSearchResult(updatedExcursionSearchResult));
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const calculateTotalPrice = () => {
|
|
162
|
+
const selectedOptions = allOptions.filter((option) => option.isSelected);
|
|
163
|
+
const totalPrice = selectedOptions.reduce((total, option) => total + (option.price || 0), 0);
|
|
164
|
+
|
|
165
|
+
return formatPrice(totalPrice, selectedExcursionSearchResult.currencyCode);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const getSharedPriceDifference = (accommodationCode: string) => {
|
|
169
|
+
const currentSelectedShared = getSelectedSharedOption();
|
|
170
|
+
|
|
171
|
+
let targetPrice = 0;
|
|
172
|
+
|
|
173
|
+
const selectedOption = getSelectedSharedOptionForExcursion(accommodationCode);
|
|
174
|
+
|
|
175
|
+
if (selectedOption?.price) {
|
|
176
|
+
targetPrice = selectedOption.price;
|
|
177
|
+
} else {
|
|
178
|
+
const firstOption = sharedOptions.find((option) => option.accommodationCode === accommodationCode);
|
|
179
|
+
targetPrice = firstOption?.price || 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return targetPrice - (currentSelectedShared?.price || 0);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const getPriceDifference = (currentSelectedPrice: number | undefined, paxId: number, accommodationCode: string) => {
|
|
186
|
+
let targetPrice = 0;
|
|
187
|
+
|
|
188
|
+
const selectedOption = getSelectedOptionForExcursion(paxId, accommodationCode);
|
|
189
|
+
|
|
190
|
+
if (selectedOption?.price) {
|
|
191
|
+
targetPrice = selectedOption.price;
|
|
192
|
+
} else {
|
|
193
|
+
const firstOption = allOptions.find(
|
|
194
|
+
(option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerCount) && option.accommodationCode === accommodationCode
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
targetPrice = firstOption?.price || 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return targetPrice - (currentSelectedPrice || 0);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const formatPriceDifference = (difference: number, currencyCode: string) => {
|
|
204
|
+
if (difference === 0) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const formattedAbsoluteValue = formatPrice(Math.abs(difference), currencyCode);
|
|
209
|
+
return `${difference > 0 ? '+' : '-'} ${formattedAbsoluteValue}`;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const getPriceDifferenceClassName = (difference: number) => {
|
|
213
|
+
if (difference < 0) {
|
|
214
|
+
return 'flyin__acco__price flyin__acco__price--decrease';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (difference > 0) {
|
|
218
|
+
return 'flyin__acco__price flyin__acco__price--increase';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return 'flyin__acco__price';
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleConfirm = () => {
|
|
225
|
+
const dayKey = getExcursionDayKey(excursionSearchParams.date);
|
|
226
|
+
|
|
227
|
+
dispatch(
|
|
228
|
+
confirmExcursionForDay({
|
|
229
|
+
dayKey,
|
|
230
|
+
excursion: selectedExcursionSearchResult
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
dispatch(setFlyInIsOpen(false));
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<>
|
|
239
|
+
<div className="flyin__content">
|
|
240
|
+
{sharedExcursions.length > 0 && (
|
|
241
|
+
<div className="flyin__acco">
|
|
242
|
+
<h3 className="flyin__acco__room-title">{translations.QSM.ALL_TRAVELERS}</h3>
|
|
243
|
+
|
|
244
|
+
<div className="flyin__acco__cards">
|
|
245
|
+
{sharedExcursions.map((excursion) => {
|
|
246
|
+
const selectedOption = getSelectedSharedOptionForExcursion(excursion.accommodationCode);
|
|
247
|
+
const priceDifference = getSharedPriceDifference(excursion.accommodationCode);
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<div className="flyin__acco__card" key={`all-${excursion.accommodationCode}`}>
|
|
251
|
+
<div className="flyin__acco__content">
|
|
252
|
+
<h4 className="flyin__acco__title">{excursion.accommodationName}</h4>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<div className="flyin__acco__footer">
|
|
256
|
+
<div className="flyin__acco__footer__actions">
|
|
257
|
+
<button
|
|
258
|
+
className={selectedOption ? 'cta cta--select cta--selected' : 'cta cta--select'}
|
|
259
|
+
onClick={() => handlePick(selectedOption ? selectedOption.guid : first(excursion.options)?.guid)}>
|
|
260
|
+
{selectedOption ? translations?.SHARED.SELECTED : translations?.SHARED.SELECT}
|
|
261
|
+
</button>
|
|
262
|
+
|
|
263
|
+
<div className="flyin__acco__price__wrapper">
|
|
264
|
+
<span className={getPriceDifferenceClassName(priceDifference)}>
|
|
265
|
+
{formatPriceDifference(priceDifference, selectedExcursionSearchResult.currencyCode)}
|
|
266
|
+
</span>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
})}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{paxGroups.map(({ pax, paxId, excursions }) => {
|
|
278
|
+
if (excursions.length === 0) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const selectedPaxOption = getSelectedOptionForPax(paxId);
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div className="flyin__acco" key={`pax-${pax.id}`}>
|
|
286
|
+
<h3 className="flyin__acco__room-title">
|
|
287
|
+
{translations.SUMMARY.TRAVELER} {pax.id + 1}
|
|
288
|
+
</h3>
|
|
289
|
+
|
|
290
|
+
<div className="flyin__acco__cards">
|
|
291
|
+
{excursions.map((excursion) => {
|
|
292
|
+
const selectedOption = getSelectedOptionForExcursion(paxId, excursion.accommodationCode);
|
|
293
|
+
const priceDifference = getPriceDifference(selectedPaxOption?.price, paxId, excursion.accommodationCode);
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<div className="flyin__acco__card" key={`${pax.id}-${excursion.accommodationCode}`}>
|
|
297
|
+
<div className="flyin__acco__content">
|
|
298
|
+
<h4 className="flyin__acco__title">{excursion.accommodationName}</h4>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div className="flyin__acco__footer">
|
|
302
|
+
<div className="flyin__acco__footer__actions">
|
|
303
|
+
<button
|
|
304
|
+
className={
|
|
305
|
+
selectedPaxOption?.accommodationCode === excursion.accommodationCode ? 'cta cta--select cta--selected' : 'cta cta--select'
|
|
306
|
+
}
|
|
307
|
+
onClick={() => handlePick(selectedOption ? selectedOption.guid : first(excursion.options)?.guid, paxId)}>
|
|
308
|
+
{selectedPaxOption?.accommodationCode === excursion.accommodationCode ? translations?.SHARED.SELECTED : translations?.SHARED.SELECT}
|
|
309
|
+
</button>
|
|
310
|
+
|
|
311
|
+
<div className="flyin__acco__price__wrapper">
|
|
312
|
+
<span className={getPriceDifferenceClassName(priceDifference)}>
|
|
313
|
+
{formatPriceDifference(priceDifference, selectedExcursionSearchResult.currencyCode)}
|
|
314
|
+
</span>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
})}
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
})}
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div className="flyin__footer">
|
|
328
|
+
<div className="flyin__footer__price">
|
|
329
|
+
{translations.SHARED.TOTAL_PRICE}: {calculateTotalPrice()}
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<button type="button" className="cta cta--primary" onClick={handleConfirm}>
|
|
333
|
+
{translations?.QSM.CONFIRM}
|
|
334
|
+
</button>
|
|
335
|
+
</div>
|
|
336
|
+
</>
|
|
337
|
+
);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
export default ExcursionDetails;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import Spinner from '../spinner/spinner';
|
|
3
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
4
|
+
import { SearchResultsRootState } from '../../store/search-results-store';
|
|
5
|
+
import { getTranslations } from '../../../shared/utils/localization-util';
|
|
6
|
+
import SearchResultsConfigurationContext from '../../search-results-configuration-context';
|
|
7
|
+
import {
|
|
8
|
+
PackagingAccommodationRequest,
|
|
9
|
+
PackagingAccommodationResponse,
|
|
10
|
+
PackagingDestination,
|
|
11
|
+
searchPackagingExcursions,
|
|
12
|
+
TideClientConfig
|
|
13
|
+
} from '@qite/tide-client';
|
|
14
|
+
import { EXCURSION_SERVICE_TYPE } from '../../utils/query-utils';
|
|
15
|
+
import { SearchSeed } from '../../types';
|
|
16
|
+
import he from 'he';
|
|
17
|
+
import { setFlyInType, setSelectedExcursionSearchResult } from '../../store/search-results-slice';
|
|
18
|
+
|
|
19
|
+
interface ExcursionResultsProps {
|
|
20
|
+
isFlyIn?: boolean;
|
|
21
|
+
activeSearchSeed?: SearchSeed | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ExcursionResults: React.FC<ExcursionResultsProps> = ({ isFlyIn, activeSearchSeed }) => {
|
|
25
|
+
const context = useContext(SearchResultsConfigurationContext);
|
|
26
|
+
const dispatch = useDispatch();
|
|
27
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
28
|
+
const [excursions, setExcursions] = useState<PackagingAccommodationResponse[] | null>(null);
|
|
29
|
+
|
|
30
|
+
const { flyInIsOpen, flyInType, excursionSearchParams, transactionId } = useSelector((state: SearchResultsRootState) => state.searchResults);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!context || !activeSearchSeed || !excursionSearchParams) return;
|
|
34
|
+
|
|
35
|
+
(async () => {
|
|
36
|
+
try {
|
|
37
|
+
setIsLoading(true);
|
|
38
|
+
console.log('Excursion search params changed, fetching excursions...', excursionSearchParams);
|
|
39
|
+
|
|
40
|
+
const config: TideClientConfig = {
|
|
41
|
+
host: context.tideConnection.host,
|
|
42
|
+
apiKey: context.tideConnection.apiKey
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const destination = excursionSearchParams.locationId
|
|
46
|
+
? { id: Number(excursionSearchParams.locationId), type: 'location' }
|
|
47
|
+
: excursionSearchParams.oordId
|
|
48
|
+
? { id: Number(excursionSearchParams.oordId), type: 'oord' }
|
|
49
|
+
: excursionSearchParams.regionId
|
|
50
|
+
? { id: Number(excursionSearchParams.regionId), type: 'region' }
|
|
51
|
+
: excursionSearchParams.countryId
|
|
52
|
+
? { id: Number(excursionSearchParams.countryId), type: 'country' }
|
|
53
|
+
: { id: 0, type: null };
|
|
54
|
+
|
|
55
|
+
const allPax = activeSearchSeed.rooms.flatMap((room) => room.pax);
|
|
56
|
+
|
|
57
|
+
const searchRequest: PackagingAccommodationRequest = {
|
|
58
|
+
transactionId: transactionId ?? '',
|
|
59
|
+
officeId: context.tideConnection.officeId ?? 1,
|
|
60
|
+
agentId: context.agentId ?? null,
|
|
61
|
+
portalId: context.portalId ?? null,
|
|
62
|
+
catalogueId: context.searchConfiguration.defaultCatalogueId ?? 0,
|
|
63
|
+
searchConfigurationId: context.searchConfiguration.id,
|
|
64
|
+
language: context.languageCode ?? 'en-GB',
|
|
65
|
+
serviceType: EXCURSION_SERVICE_TYPE,
|
|
66
|
+
fromDate: excursionSearchParams.fromDate,
|
|
67
|
+
toDate: excursionSearchParams.toDate,
|
|
68
|
+
destination: {
|
|
69
|
+
id: destination.id,
|
|
70
|
+
isCountry: destination.type === 'country',
|
|
71
|
+
isRegion: destination.type === 'region',
|
|
72
|
+
isOord: destination.type === 'oord',
|
|
73
|
+
isLocation: destination.type === 'location',
|
|
74
|
+
isAirport: false,
|
|
75
|
+
code: ''
|
|
76
|
+
} as PackagingDestination,
|
|
77
|
+
productCode: '',
|
|
78
|
+
// rooms: activeSearchSeed.rooms.map((room) => ({
|
|
79
|
+
// travellers: room.pax.map((pax) => ({
|
|
80
|
+
// id: pax.id,
|
|
81
|
+
// age: pax.age,
|
|
82
|
+
// dateOfBirth: pax.dateOfBirth
|
|
83
|
+
// }))
|
|
84
|
+
// })),
|
|
85
|
+
rooms: [
|
|
86
|
+
{
|
|
87
|
+
travellers: allPax.map((pax) => ({
|
|
88
|
+
id: pax.id,
|
|
89
|
+
age: pax.age,
|
|
90
|
+
dateOfBirth: pax.dateOfBirth
|
|
91
|
+
}))
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
tagIds: []
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const packageExcursionSearchResults = await searchPackagingExcursions(config, searchRequest);
|
|
98
|
+
console.log('Excursion search results', packageExcursionSearchResults);
|
|
99
|
+
setExcursions(packageExcursionSearchResults);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error('Excursion search failed', err);
|
|
102
|
+
} finally {
|
|
103
|
+
setIsLoading(false);
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
}, [context, activeSearchSeed, excursionSearchParams, transactionId]);
|
|
107
|
+
|
|
108
|
+
if (!context || !activeSearchSeed) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!flyInIsOpen || flyInType !== 'excursion-results') {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const translations = getTranslations(context.languageCode ?? 'en-GB');
|
|
117
|
+
|
|
118
|
+
const handleChange = (excursion: PackagingAccommodationResponse): void => {
|
|
119
|
+
console.log('Selected excursion', excursion);
|
|
120
|
+
dispatch(setFlyInType('excursion-details'));
|
|
121
|
+
dispatch(setSelectedExcursionSearchResult(excursion));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return isLoading ? (
|
|
125
|
+
<Spinner />
|
|
126
|
+
) : (
|
|
127
|
+
<div className="flyin__content flyin__content--columns">
|
|
128
|
+
{/* <Filters
|
|
129
|
+
initialFilters={initialFilters}
|
|
130
|
+
filters={filters}
|
|
131
|
+
isOpen={false}
|
|
132
|
+
handleSetIsOpen={() => { }}
|
|
133
|
+
// handleApplyFilters={() => setSearchTrigger((prev) => prev + 1)}
|
|
134
|
+
isLoading={isLoading}
|
|
135
|
+
setFilters={(filters) => dispatch(setFilters(filters))}
|
|
136
|
+
resetFilters={(filters) => dispatch(resetFilters(filters))}
|
|
137
|
+
/> */}
|
|
138
|
+
<div className="search__results__wrapper">
|
|
139
|
+
<div className="search__result-row">
|
|
140
|
+
<span className="search__result-row-text">
|
|
141
|
+
{!isLoading && (
|
|
142
|
+
<>
|
|
143
|
+
{excursions?.length && excursions.length}
|
|
144
|
+
{translations.SRP.TOTAL_RESULTS_LABEL}
|
|
145
|
+
</>
|
|
146
|
+
)}
|
|
147
|
+
</span>
|
|
148
|
+
{/* {sortByTypes && sortByTypes.length > 0 && (
|
|
149
|
+
<div className="search__result-row-filter">
|
|
150
|
+
<ItemPicker
|
|
151
|
+
items={sortByTypes}
|
|
152
|
+
selection={selectedSortType?.label || undefined}
|
|
153
|
+
selectedSortByType={selectedSortType}
|
|
154
|
+
label={translations.SRP.SORTBY}
|
|
155
|
+
placeholder={translations.SRP.SORTBY}
|
|
156
|
+
classModifier="travel-class-picker__items"
|
|
157
|
+
valueFormatter={(value, direction) => getSortingName(translations, findSortByType(sortByTypes, value, direction ?? 'asc'))}
|
|
158
|
+
onPick={(newSortKey, direction) => handleSortChange(newSortKey, direction)}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
)} */}
|
|
162
|
+
</div>
|
|
163
|
+
<div className="search__results__cards search__results__cards--compact">
|
|
164
|
+
{excursions &&
|
|
165
|
+
excursions.length > 0 &&
|
|
166
|
+
excursions.map((excursion) => (
|
|
167
|
+
<div
|
|
168
|
+
key={excursion.code}
|
|
169
|
+
className="search__result-card__wrapper search__result-card__wrapper--custom"
|
|
170
|
+
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.02)')}
|
|
171
|
+
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}>
|
|
172
|
+
{excursion.contents ? <div dangerouslySetInnerHTML={{ __html: he.decode(excursion.contents) }}></div> : 'no contents'}
|
|
173
|
+
<div className="search__result-card__footer">
|
|
174
|
+
<button type="button" className="cta cta--select" onClick={() => handleChange(excursion)}>
|
|
175
|
+
{translations?.SHARED.SELECT}
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export default ExcursionResults;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import React, { useContext,
|
|
1
|
+
import React, { useContext, useState } from 'react';
|
|
2
2
|
import { Filter, FilterOption } from '../../types';
|
|
3
3
|
import MultiRangeFilter from '../multi-range-filter';
|
|
4
4
|
import SearchResultsConfigurationContext from '../../search-results-configuration-context';
|
|
5
|
-
import { resetFilters, setFilters } from '../../store/search-results-slice';
|
|
6
|
-
import { useDispatch } from 'react-redux';
|
|
7
5
|
import Spinner from '../spinner/spinner';
|
|
8
6
|
import Icon from '../icon';
|
|
9
7
|
import { getTranslations } from '../../../shared/utils/localization-util';
|
|
@@ -14,10 +12,13 @@ interface FiltersProps {
|
|
|
14
12
|
isOpen: boolean;
|
|
15
13
|
handleSetIsOpen: () => void;
|
|
16
14
|
isLoading?: boolean;
|
|
15
|
+
setFilters: (filters: Filter[]) => void;
|
|
16
|
+
resetFilters: (filters: Filter[]) => void;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const Filters: React.FC<FiltersProps> = ({ initialFilters, filters, isOpen, handleSetIsOpen, isLoading }) => {
|
|
19
|
+
const Filters: React.FC<FiltersProps> = ({ initialFilters, filters, isOpen, handleSetIsOpen, isLoading, setFilters, resetFilters }) => {
|
|
20
20
|
const context = useContext(SearchResultsConfigurationContext);
|
|
21
|
+
|
|
21
22
|
if (!context || !context.showFilters) {
|
|
22
23
|
return null;
|
|
23
24
|
}
|
|
@@ -25,8 +26,6 @@ const Filters: React.FC<FiltersProps> = ({ initialFilters, filters, isOpen, hand
|
|
|
25
26
|
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
26
27
|
const [visibleFilters, setVisibleFilters] = useState<Record<string, boolean>>({});
|
|
27
28
|
|
|
28
|
-
const dispatch = useDispatch();
|
|
29
|
-
|
|
30
29
|
const toggleFilterVisibility = (filterId: string) => {
|
|
31
30
|
setVisibleFilters((prev) => ({
|
|
32
31
|
...prev,
|
|
@@ -44,7 +43,7 @@ const Filters: React.FC<FiltersProps> = ({ initialFilters, filters, isOpen, hand
|
|
|
44
43
|
};
|
|
45
44
|
});
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
setFilters(updated);
|
|
48
47
|
};
|
|
49
48
|
|
|
50
49
|
const handleSliderChange = (filter: Filter, newMin: number, newMax: number) => {
|
|
@@ -58,12 +57,12 @@ const Filters: React.FC<FiltersProps> = ({ initialFilters, filters, isOpen, hand
|
|
|
58
57
|
};
|
|
59
58
|
});
|
|
60
59
|
|
|
61
|
-
|
|
60
|
+
setFilters(updated);
|
|
62
61
|
};
|
|
63
62
|
|
|
64
63
|
const handleFullReset = () => {
|
|
65
64
|
if (!isLoading) {
|
|
66
|
-
|
|
65
|
+
resetFilters(initialFilters);
|
|
67
66
|
}
|
|
68
67
|
};
|
|
69
68
|
|