@qite/tide-booking-component 1.4.104 → 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 +1079 -116
- 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/store/search-results-selectors.d.ts +122 -0
- package/build/build-cjs/src/search-results/store/search-results-slice.d.ts +30 -2
- package/build/build-cjs/src/search-results/types.d.ts +27 -1
- package/build/build-cjs/src/search-results/utils/query-utils.d.ts +1 -0
- package/build/build-cjs/src/shared/components/flyin/flyin.d.ts +2 -1
- 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 +1071 -116
- 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/store/search-results-selectors.d.ts +122 -0
- package/build/build-esm/src/search-results/store/search-results-slice.d.ts +30 -2
- package/build/build-esm/src/search-results/types.d.ts +27 -1
- package/build/build-esm/src/search-results/utils/query-utils.d.ts +1 -0
- package/build/build-esm/src/shared/components/flyin/flyin.d.ts +2 -1
- 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/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/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 +90 -28
- package/src/search-results/store/search-results-selectors.ts +11 -0
- package/src/search-results/store/search-results-slice.ts +46 -3
- package/src/search-results/types.ts +42 -1
- package/src/search-results/utils/query-utils.ts +1 -0
- 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 +18 -6
- package/src/shared/components/flyin/group-tour-flyin.tsx +3 -1
- 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/_search.scss +11 -1
|
@@ -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;
|
|
@@ -34,9 +34,6 @@ const HotelCard: React.FC<HotelCardProps> = ({ result, translations }) => {
|
|
|
34
34
|
onClick={() => handleChange(result.code)}>
|
|
35
35
|
{selectedPackagingAccoResultCode === result.code ? translations?.SHARED.SELECTED : translations?.SHARED.SELECT}
|
|
36
36
|
</button>
|
|
37
|
-
{/* <button type="button" className="cta cta--select" onClick={() => console.log('Clicked on customCard with id:', result.id)}>
|
|
38
|
-
{translations?.SRP.VIEW_DETAILS}
|
|
39
|
-
</button> */}
|
|
40
37
|
</div>
|
|
41
38
|
</div>
|
|
42
39
|
);
|
|
@@ -315,10 +315,7 @@ const Icon: React.FC<IconProps> = ({ name, className, title, width, height, fill
|
|
|
315
315
|
fill={fill ?? 'currentColor'}>
|
|
316
316
|
<HTMLComment text="!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc." />
|
|
317
317
|
{title && <title>{title}</title>}
|
|
318
|
-
<path
|
|
319
|
-
d="M64 64C28.7 64 0 92.7 0 128l0 64C0 200.8 7.4 207.7 15.7 210.6 34.5 217.1 48 235 48 256s-13.5 38.9-32.3 45.4C7.4 304.3 0 311.2 0 320l0 64c0 35.3 28.7 64 64 64l448 0c35.3 0 64-28.7 64-64l0-64c0-8.8-7.4-15.7-15.7-18.6-18.8-6.5-32.3-24.4-32.3-45.4s13.5-38.9 32.3-45.4c8.3-2.9 15.7-9.8 15.7-18.6l0-64c0-35.3-28.7-64-64-64L64 64zM416 336l0-160-256 0 0 160 256 0zM112 160c0-17.7 14.3-32 32-32l288 0c17.7 0 32 14.3 32 32l0 192c0 17.7-14.3 32-32 32l-288 0c-17.7 0-32-14.3-32-32l0-192z"
|
|
320
|
-
fill="currentColor"
|
|
321
|
-
/>
|
|
318
|
+
<path d="M64 64C28.7 64 0 92.7 0 128l0 64C0 200.8 7.4 207.7 15.7 210.6 34.5 217.1 48 235 48 256s-13.5 38.9-32.3 45.4C7.4 304.3 0 311.2 0 320l0 64c0 35.3 28.7 64 64 64l448 0c35.3 0 64-28.7 64-64l0-64c0-8.8-7.4-15.7-15.7-18.6-18.8-6.5-32.3-24.4-32.3-45.4s13.5-38.9 32.3-45.4c8.3-2.9 15.7-9.8 15.7-18.6l0-64c0-35.3-28.7-64-64-64L64 64zM416 336l0-160-256 0 0 160 256 0zM112 160c0-17.7 14.3-32 32-32l288 0c17.7 0 32 14.3 32 32l0 192c0 17.7-14.3 32-32 32l-288 0c-17.7 0-32-14.3-32-32l0-192z" />
|
|
322
319
|
</svg>
|
|
323
320
|
);
|
|
324
321
|
|