@qite/tide-booking-component 1.4.69 → 1.4.71
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 +1299 -1058
- package/build/build-cjs/src/qsm/store/qsm-slice.d.ts +4 -4
- package/build/build-cjs/src/qsm/types.d.ts +2 -14
- package/build/build-cjs/src/search-results/components/filters/filters.d.ts +1 -1
- package/build/build-cjs/src/search-results/components/filters/utility.d.ts +2 -2
- package/build/build-cjs/src/search-results/components/group-tour/group-tour-card.d.ts +8 -0
- package/build/build-cjs/src/search-results/components/group-tour/group-tour-results.d.ts +6 -0
- package/build/build-cjs/src/search-results/components/hotel/hotel-accommodation-results.d.ts +0 -2
- package/build/build-cjs/src/search-results/store/search-results-slice.d.ts +5 -8
- package/build/build-cjs/src/search-results/types.d.ts +7 -2
- package/build/build-cjs/src/search-results/utils/search-results-utils.d.ts +3 -0
- package/build/build-cjs/src/shared/components/flyin/accommodation-flyin.d.ts +8 -0
- package/build/build-cjs/src/shared/components/flyin/flights-flyin.d.ts +7 -0
- package/build/build-cjs/src/shared/components/{flyin.d.ts → flyin/flyin.d.ts} +3 -2
- package/build/build-cjs/src/shared/types.d.ts +12 -0
- package/build/build-cjs/src/shared/utils/localization-util.d.ts +5 -0
- package/build/build-esm/index.js +1285 -1053
- package/build/build-esm/src/qsm/store/qsm-slice.d.ts +4 -4
- package/build/build-esm/src/qsm/types.d.ts +2 -14
- package/build/build-esm/src/search-results/components/filters/filters.d.ts +1 -1
- package/build/build-esm/src/search-results/components/filters/utility.d.ts +2 -2
- package/build/build-esm/src/search-results/components/group-tour/group-tour-card.d.ts +8 -0
- package/build/build-esm/src/search-results/components/group-tour/group-tour-results.d.ts +6 -0
- package/build/build-esm/src/search-results/components/hotel/hotel-accommodation-results.d.ts +0 -2
- package/build/build-esm/src/search-results/store/search-results-slice.d.ts +5 -8
- package/build/build-esm/src/search-results/types.d.ts +7 -2
- package/build/build-esm/src/search-results/utils/search-results-utils.d.ts +3 -0
- package/build/build-esm/src/shared/components/flyin/accommodation-flyin.d.ts +8 -0
- package/build/build-esm/src/shared/components/flyin/flights-flyin.d.ts +7 -0
- package/build/build-esm/src/shared/components/{flyin.d.ts → flyin/flyin.d.ts} +3 -2
- package/build/build-esm/src/shared/types.d.ts +12 -0
- package/build/build-esm/src/shared/utils/localization-util.d.ts +5 -0
- package/package.json +2 -2
- package/src/qsm/components/QSMContainer/qsm-container.tsx +16 -3
- package/src/qsm/store/qsm-slice.ts +4 -4
- package/src/qsm/types.ts +2 -15
- package/src/search-results/components/filters/filters.tsx +136 -293
- package/src/search-results/components/filters/utility.tsx +61 -2
- package/src/search-results/components/group-tour/group-tour-card.tsx +100 -0
- package/src/search-results/components/group-tour/group-tour-results.tsx +40 -0
- package/src/search-results/components/hotel/hotel-accommodation-results.tsx +13 -16
- package/src/search-results/components/hotel/hotel-card.tsx +11 -8
- package/src/search-results/components/icon.tsx +18 -0
- package/src/search-results/components/search-results-container/search-results-container.tsx +62 -30
- package/src/search-results/store/search-results-slice.ts +13 -7
- package/src/search-results/types.ts +9 -2
- package/src/search-results/utils/search-results-utils.ts +42 -0
- package/src/shared/components/flyin/accommodation-flyin.tsx +40 -0
- package/src/shared/components/flyin/flights-flyin.tsx +499 -0
- package/src/shared/components/flyin/flyin.tsx +79 -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/types.ts +13 -0
- package/src/shared/utils/localization-util.ts +16 -0
- package/styles/components/_flyin.scss +10 -0
- package/src/shared/components/flyin.tsx +0 -546
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import Icon from '../icon';
|
|
3
|
+
import { ExtendedFlightSearchResponseItem } from '../../../search-results/types';
|
|
4
|
+
import { useFlightSearch } from '../../../search-results/components/flight/flight-search-context';
|
|
5
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
6
|
+
import { setSelectedFlightDetails } from '../../../search-results/store/search-results-slice';
|
|
7
|
+
import { SearchResultsRootState } from '../../../search-results/store/search-results-store';
|
|
8
|
+
import Spinner from '../../../search-results/components/spinner/spinner';
|
|
9
|
+
import SearchResultsConfigurationContext from '../../../search-results/search-results-configuration-context';
|
|
10
|
+
import { durationTicksInHoursString, getTranslations, timeFromDateTime } from '../../utils/localization-util';
|
|
11
|
+
import { getArrivalSegment, getDepartureSegment, getNumberOfStopsLabel } from '../../../search-results/utils/flight-utils';
|
|
12
|
+
import { first, isEmpty } from 'lodash';
|
|
13
|
+
|
|
14
|
+
type FlightsFlyInProps = {
|
|
15
|
+
isOpen: boolean;
|
|
16
|
+
setIsOpen: (open: boolean) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const FlightsFlyIn: React.FC<FlightsFlyInProps> = ({ isOpen, setIsOpen }) => {
|
|
20
|
+
const context = useContext(SearchResultsConfigurationContext);
|
|
21
|
+
const language = context?.languageCode ?? 'en-GB';
|
|
22
|
+
const translations = getTranslations(language);
|
|
23
|
+
|
|
24
|
+
const dispatch = useDispatch();
|
|
25
|
+
const { flightSearchDetailsLoading, flightDetailsSearchResults, onCancelSearch, numberOfTravellers } = useFlightSearch();
|
|
26
|
+
const { selectedFlight } = useSelector((state: SearchResultsRootState) => state.searchResults);
|
|
27
|
+
|
|
28
|
+
const [flights, setFlights] = useState<ExtendedFlightSearchResponseItem[]>([]);
|
|
29
|
+
const [flight, setFlight] = useState<ExtendedFlightSearchResponseItem | undefined>(undefined);
|
|
30
|
+
|
|
31
|
+
const [uniqueOutwardFlights, setUniqueOutwardFlights] = useState<ExtendedFlightSearchResponseItem[]>([]);
|
|
32
|
+
const [selectedOutwardFareCode, setSelectedOutwardFareCode] = useState<string | null>(null);
|
|
33
|
+
const [uniqueReturnFlights, setUniqueReturnFlights] = useState<ExtendedFlightSearchResponseItem[]>([]);
|
|
34
|
+
const [selectedReturnFareCode, setSelectedReturnFareCode] = useState<string | null>(null);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (flightDetailsSearchResults.length > 0 && selectedFlight) {
|
|
38
|
+
const routeFlights = flightDetailsSearchResults.filter((r) => r.flightRouteId === selectedFlight.flightRouteId);
|
|
39
|
+
|
|
40
|
+
setFlights(routeFlights);
|
|
41
|
+
setFlight(first(routeFlights));
|
|
42
|
+
|
|
43
|
+
// Deduplicate by outward fareCode
|
|
44
|
+
const uniqueMap = new Map<string, ExtendedFlightSearchResponseItem>();
|
|
45
|
+
|
|
46
|
+
routeFlights.forEach((flight) => {
|
|
47
|
+
const fareCode = flight.outward.segments?.[0]?.metaData?.fareCode;
|
|
48
|
+
|
|
49
|
+
if (fareCode && !uniqueMap.has(fareCode)) {
|
|
50
|
+
uniqueMap.set(fareCode, flight);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const values = Array.from(uniqueMap.values());
|
|
55
|
+
setUniqueOutwardFlights(values);
|
|
56
|
+
const fareCode = first(values)?.outward.segments?.[0]?.metaData?.fareCode;
|
|
57
|
+
setSelectedOutwardFareCode(fareCode ?? null);
|
|
58
|
+
}
|
|
59
|
+
}, [flightDetailsSearchResults, flightSearchDetailsLoading]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!selectedOutwardFareCode) {
|
|
63
|
+
setUniqueReturnFlights([]);
|
|
64
|
+
setSelectedReturnFareCode(null);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Filter combinations that match selected outward fare
|
|
69
|
+
const matchingCombinations = flights.filter((flight) => flight.outward.segments?.[0]?.metaData?.fareCode === selectedOutwardFareCode);
|
|
70
|
+
const returnMap = new Map<string, ExtendedFlightSearchResponseItem>();
|
|
71
|
+
|
|
72
|
+
// Deduplicate return flights by return fareCode
|
|
73
|
+
matchingCombinations.forEach((flight) => {
|
|
74
|
+
const returnFareCode = flight.return.segments?.[0]?.metaData?.fareCode;
|
|
75
|
+
|
|
76
|
+
if (returnFareCode && !returnMap.has(returnFareCode)) {
|
|
77
|
+
returnMap.set(returnFareCode, flight);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const returns = Array.from(returnMap.values());
|
|
82
|
+
|
|
83
|
+
setUniqueReturnFlights(returns);
|
|
84
|
+
|
|
85
|
+
// keep current return if still valid based on the selected outward, otherwise select the first available
|
|
86
|
+
const stillValid = returns.some((r) => r.return.segments?.[0]?.metaData?.fareCode === selectedReturnFareCode);
|
|
87
|
+
|
|
88
|
+
if (!stillValid) {
|
|
89
|
+
const defaultFareCode = returns[0]?.return.segments?.[0]?.metaData?.fareCode ?? null;
|
|
90
|
+
setSelectedReturnFareCode(defaultFareCode);
|
|
91
|
+
}
|
|
92
|
+
}, [selectedOutwardFareCode, flights]);
|
|
93
|
+
|
|
94
|
+
const selectedCombinationFlight = React.useMemo(() => {
|
|
95
|
+
if (!selectedOutwardFareCode || !selectedReturnFareCode) return undefined;
|
|
96
|
+
|
|
97
|
+
return flights.find(
|
|
98
|
+
(flight) =>
|
|
99
|
+
flight.outward.segments?.[0]?.metaData?.fareCode === selectedOutwardFareCode &&
|
|
100
|
+
flight.return.segments?.[0]?.metaData?.fareCode === selectedReturnFareCode
|
|
101
|
+
);
|
|
102
|
+
}, [flights, selectedOutwardFareCode, selectedReturnFareCode]);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!selectedCombinationFlight) return;
|
|
106
|
+
|
|
107
|
+
dispatch(setSelectedFlightDetails(selectedCombinationFlight));
|
|
108
|
+
}, [selectedCombinationFlight, dispatch]);
|
|
109
|
+
|
|
110
|
+
const getOutwardPriceDiff = (outwardFareCode: string) => {
|
|
111
|
+
if (!selectedReturnFareCode || !selectedCombinationFlight) return 0;
|
|
112
|
+
|
|
113
|
+
const combo =
|
|
114
|
+
flights.find(
|
|
115
|
+
(flight) =>
|
|
116
|
+
flight.outward.segments?.[0]?.metaData?.fareCode === outwardFareCode && flight.return.segments?.[0]?.metaData?.fareCode === selectedReturnFareCode
|
|
117
|
+
) ??
|
|
118
|
+
// it means that outward flight cannot be paired with the selected return fallback to first combo with that outward fare
|
|
119
|
+
flights.find((flight) => flight.outward.segments?.[0]?.metaData?.fareCode === outwardFareCode);
|
|
120
|
+
|
|
121
|
+
if (!combo) return 0;
|
|
122
|
+
|
|
123
|
+
return Math.round(combo.price - selectedCombinationFlight.price);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const getReturnPriceDiff = (returnFareCode: string) => {
|
|
127
|
+
if (!selectedOutwardFareCode || !selectedCombinationFlight) return 0;
|
|
128
|
+
|
|
129
|
+
const combo =
|
|
130
|
+
flights.find(
|
|
131
|
+
(flight) =>
|
|
132
|
+
flight.outward.segments?.[0]?.metaData?.fareCode === selectedOutwardFareCode && flight.return.segments?.[0]?.metaData?.fareCode === returnFareCode
|
|
133
|
+
) ??
|
|
134
|
+
// it means that return flight cannot be paired with the selected outward fallback to first combo with that return fare
|
|
135
|
+
flights.find((flight) => flight.return.segments?.[0]?.metaData?.fareCode === returnFareCode);
|
|
136
|
+
|
|
137
|
+
if (!combo) return 0;
|
|
138
|
+
|
|
139
|
+
return Math.round(combo.price - selectedCombinationFlight.price);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// TODO: go to booking page?
|
|
143
|
+
const handleConfirm = () => {
|
|
144
|
+
if (isOpen) {
|
|
145
|
+
onCancelSearch();
|
|
146
|
+
setIsOpen(false);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
<div className="flyin__content">
|
|
153
|
+
{flightSearchDetailsLoading || isEmpty(flights) ? (
|
|
154
|
+
<Spinner />
|
|
155
|
+
) : (
|
|
156
|
+
flight && (
|
|
157
|
+
<div className="flyin__content-text-row">
|
|
158
|
+
<div className="flyin__content-text-icon-row">
|
|
159
|
+
<img
|
|
160
|
+
src={`https://media.tidesoftware.be/media/shared/Airlines/${getDepartureSegment(flight.outward)?.marketingAirlineCode}.png?height=256`}
|
|
161
|
+
alt="airline-logo"
|
|
162
|
+
className="logo"
|
|
163
|
+
aria-hidden="true"
|
|
164
|
+
/>
|
|
165
|
+
<div className="flyin__content-text-col">
|
|
166
|
+
<span className="flyin__content-text-title-row">
|
|
167
|
+
<strong>{translations.SRP.DEPARTURE}</strong> {getDepartureSegment(flight?.outward)?.departureAirportCode} -{' '}
|
|
168
|
+
{getArrivalSegment(flight?.outward)?.arrivalAirportCode}
|
|
169
|
+
</span>
|
|
170
|
+
<span className="flyin__content-text">
|
|
171
|
+
{timeFromDateTime(getDepartureSegment(flight?.outward)?.departureDateTime)} -{' '}
|
|
172
|
+
{timeFromDateTime(getArrivalSegment(flight?.outward)?.arrivalDateTime)} ({durationTicksInHoursString(flight.outward.durationInTicks)},{' '}
|
|
173
|
+
{getNumberOfStopsLabel(flight.outward, translations.SRP.DIRECT, translations.SRP.STOPS, translations.SRP.STOP)}), {numberOfTravellers}{' '}
|
|
174
|
+
travellers
|
|
175
|
+
</span>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div className="flyin__content-arrow-row">
|
|
180
|
+
<div className="flyin__content-arrow is-disabled" aria-disabled="true">
|
|
181
|
+
<Icon name="ui-arrow" className="flyin__content-arrow-icon" width={16} height={16} aria-hidden="true" />
|
|
182
|
+
</div>
|
|
183
|
+
<div className="flyin__content-arrow">
|
|
184
|
+
<Icon name="ui-arrow" className="flyin__content-arrow-icon flyin__content-arrow-icon--forward" width={16} height={16} aria-hidden="true" />
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
{!flightSearchDetailsLoading && flight && (
|
|
192
|
+
<>
|
|
193
|
+
<div className="flyin__content-cards-wrapper">
|
|
194
|
+
<div className="flyin__content-cards">
|
|
195
|
+
{uniqueOutwardFlights.map((flightOption, index) => {
|
|
196
|
+
const firstSegment = first(flightOption.outward.segments);
|
|
197
|
+
if (!firstSegment) return null;
|
|
198
|
+
const diff = getOutwardPriceDiff(firstSegment.metaData.fareCode);
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div
|
|
202
|
+
key={`outward-flight-option-${index}`}
|
|
203
|
+
className={`flyin__content-card ${selectedOutwardFareCode === firstSegment.metaData.fareCode ? 'flyin__content-card--selected' : ''}`}>
|
|
204
|
+
<div className="flyin__content-card-top">
|
|
205
|
+
<span className="flyin__content-card-top-tag">{firstSegment.metaData.fareMarketingName}</span>
|
|
206
|
+
{diff !== null && diff != 0 && (
|
|
207
|
+
<span
|
|
208
|
+
className={`flyin__content-card-top-price ${
|
|
209
|
+
diff > 0 ? 'flyin__content-card-top-price--increase' : diff < 0 ? 'flyin__content-card-top-price--decrease' : ''
|
|
210
|
+
}`}>
|
|
211
|
+
{diff > 0 ? `+€${diff}` : `-€${Math.abs(diff)}`}
|
|
212
|
+
</span>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div className="flyin__content-card-middle">
|
|
217
|
+
<div className="flyin__content-card-middle-rows">
|
|
218
|
+
<div className="flyin__content-card-middle-row">
|
|
219
|
+
<span className="flyin__content-card-middle-row-left">Number of travellers</span>
|
|
220
|
+
<span className="flyin__content-card-middle-row-right">{numberOfTravellers}</span>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<div className="flyin__content-card-middle-row">
|
|
224
|
+
<span className="flyin__content-card-middle-row-left">Travel class</span>
|
|
225
|
+
<span className="flyin__content-card-middle-row-right">{firstSegment.metaData.fareMarketingName}</span>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div className="flyin__content-card-middle-row">
|
|
229
|
+
<span className="flyin__content-card-middle-row-left">Booking class</span>
|
|
230
|
+
<span className="flyin__content-card-middle-row-right">{firstSegment.bookingClassCode}</span>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div className="flyin__content-card-middle-row">
|
|
234
|
+
<span className="flyin__content-card-middle-row-left">Fare basis</span>
|
|
235
|
+
<span className="flyin__content-card-middle-row-right">{firstSegment.metaData.fareCode}</span>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div className="flyin__content-data">
|
|
239
|
+
{firstSegment.metaData.luggageCarryOn && (
|
|
240
|
+
<div className="flyin__content-data__item">
|
|
241
|
+
<div className="flyin__content-data__item-icon">
|
|
242
|
+
<Icon name="ui-bag" width={20} aria-hidden="true" />
|
|
243
|
+
</div>
|
|
244
|
+
<div className="flyin__content-data__item-content">
|
|
245
|
+
<div className="flyin__content-data__item-content-title">Carry-on luggage</div>
|
|
246
|
+
<div className="flyin__content-data__item-content-description">{firstSegment.metaData.luggageCarryOn.text}</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{firstSegment.metaData.luggageChecked && (
|
|
252
|
+
<div className="flyin__content-data__item">
|
|
253
|
+
<div className="flyin__content-data__item-icon">
|
|
254
|
+
<Icon name="ui-suitcase" width={20} />
|
|
255
|
+
</div>
|
|
256
|
+
<div className="flyin__content-data__item-content">
|
|
257
|
+
<div className="flyin__content-data__item-content-title">Checked luggage</div>
|
|
258
|
+
<div className="flyin__content-data__item-content-description">{firstSegment.metaData.luggageChecked.text}</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{firstSegment.metaData.seatSelection && (
|
|
264
|
+
<div className="flyin__content-data__item">
|
|
265
|
+
<div className="flyin__content-data__item-icon">
|
|
266
|
+
<Icon name="ui-seat" width={20} />
|
|
267
|
+
</div>
|
|
268
|
+
<div className="flyin__content-data__item-content">
|
|
269
|
+
<div className="flyin__content-data__item-content-title">Seat selection</div>
|
|
270
|
+
<div className="flyin__content-data__item-content-description">{firstSegment.metaData.seatSelection.text}</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{firstSegment.metaData.cancel && (
|
|
276
|
+
<div className="flyin__content-data__item">
|
|
277
|
+
<div className="flyin__content-data__item-icon">
|
|
278
|
+
<Icon name="ui-refund" width={20} />
|
|
279
|
+
</div>
|
|
280
|
+
<div className="flyin__content-data__item-content">
|
|
281
|
+
<div className="flyin__content-data__item-content-title">Refund</div>
|
|
282
|
+
<div className="flyin__content-data__item-content-description">{firstSegment.metaData.cancel.text}</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{firstSegment.metaData.other && (
|
|
288
|
+
<div className="flyin__content-data__item">
|
|
289
|
+
<div className="flyin__content-data__item-icon flyin__content-data__item-icon--other">
|
|
290
|
+
<Icon name="ui-else" width={20} />
|
|
291
|
+
</div>
|
|
292
|
+
<div className="flyin__content-data__item-content">
|
|
293
|
+
<div className="flyin__content-data__item-content-title">Other</div>
|
|
294
|
+
<ul className="flyin__content-data__item-content-description flyin__content-data__item-content-description--list">
|
|
295
|
+
{firstSegment.metaData.other.map((other, index) => (
|
|
296
|
+
<li key={`other-${index}`}>{other.text}</li>
|
|
297
|
+
))}
|
|
298
|
+
</ul>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
<div
|
|
306
|
+
className="flyin__content-card-button"
|
|
307
|
+
onClick={() => {
|
|
308
|
+
const fareCode = flightOption.outward.segments?.[0]?.metaData?.fareCode;
|
|
309
|
+
setSelectedOutwardFareCode(fareCode ?? null);
|
|
310
|
+
}}>
|
|
311
|
+
<div className={`cta ${selectedOutwardFareCode === firstSegment.metaData.fareCode ? 'cta--selected' : ''}`}>
|
|
312
|
+
{' '}
|
|
313
|
+
{selectedOutwardFareCode === firstSegment.metaData.fareCode ? 'Selected' : 'Select'}
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
})}
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div className="flyin__content">
|
|
323
|
+
<div className="flyin__content-text-row">
|
|
324
|
+
<div className="flyin__content-text-icon-row">
|
|
325
|
+
<img
|
|
326
|
+
src={`https://media.tidesoftware.be/media/shared/Airlines/${getDepartureSegment(flight.return)?.marketingAirlineCode}.png?height=256`}
|
|
327
|
+
alt="airline-logo"
|
|
328
|
+
className="logo"
|
|
329
|
+
aria-hidden="true"
|
|
330
|
+
/>
|
|
331
|
+
<div className="flyin__content-text-col">
|
|
332
|
+
<span className="flyin__content-text-title-row">
|
|
333
|
+
<strong>{translations.SRP.RETURN}</strong> {getDepartureSegment(flight?.return)?.departureAirportCode} -{' '}
|
|
334
|
+
{getArrivalSegment(flight?.return)?.arrivalAirportCode}
|
|
335
|
+
</span>
|
|
336
|
+
<span className="flyin__content-text">
|
|
337
|
+
{timeFromDateTime(getDepartureSegment(flight?.return)?.departureDateTime)} -{' '}
|
|
338
|
+
{timeFromDateTime(getArrivalSegment(flight?.return)?.arrivalDateTime)} ({durationTicksInHoursString(flight.return.durationInTicks)},{' '}
|
|
339
|
+
{getNumberOfStopsLabel(flight.return, translations.SRP.DIRECT, translations.SRP.STOPS, translations.SRP.STOP)}), {numberOfTravellers}{' '}
|
|
340
|
+
travellers
|
|
341
|
+
</span>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<div className="flyin__content-arrow-row">
|
|
346
|
+
<div className="flyin__content-arrow is-disabled" aria-disabled="true">
|
|
347
|
+
<Icon name="ui-arrow" className="flyin__content-arrow-icon" width={16} height={16} aria-hidden="true" />
|
|
348
|
+
</div>
|
|
349
|
+
<div className="flyin__content-arrow">
|
|
350
|
+
<Icon name="ui-arrow" className="flyin__content-arrow-icon flyin__content-arrow-icon--forward" width={16} height={16} aria-hidden="true" />
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<div className="flyin__content-cards-wrapper">
|
|
357
|
+
<div className="flyin__content-cards">
|
|
358
|
+
{uniqueReturnFlights.map((flightOption, index) => {
|
|
359
|
+
const firstSegment = first(flightOption.return.segments);
|
|
360
|
+
if (!firstSegment) return null;
|
|
361
|
+
const diff = getReturnPriceDiff(firstSegment.metaData.fareCode);
|
|
362
|
+
return (
|
|
363
|
+
<div
|
|
364
|
+
key={`return-flight-option-${index}`}
|
|
365
|
+
className={`flyin__content-card ${selectedReturnFareCode === firstSegment.metaData.fareCode ? 'flyin__content-card--selected' : ''}`}>
|
|
366
|
+
<div className="flyin__content-card-top">
|
|
367
|
+
<span className="flyin__content-card-top-tag">{firstSegment.metaData.fareMarketingName}</span>
|
|
368
|
+
{diff !== null && diff != 0 && (
|
|
369
|
+
<span
|
|
370
|
+
className={`flyin__content-card-top-price ${
|
|
371
|
+
diff > 0 ? 'flyin__content-card-top-price--increase' : diff < 0 ? 'flyin__content-card-top-price--decrease' : ''
|
|
372
|
+
}`}>
|
|
373
|
+
{diff > 0 ? `+€${diff}` : `-€${Math.abs(diff)}`}
|
|
374
|
+
</span>
|
|
375
|
+
)}
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<div className="flyin__content-card-middle">
|
|
379
|
+
<div className="flyin__content-card-middle-rows">
|
|
380
|
+
<div className="flyin__content-card-middle-row">
|
|
381
|
+
<span className="flyin__content-card-middle-row-left">Number of travellers</span>
|
|
382
|
+
<span className="flyin__content-card-middle-row-right">{numberOfTravellers}</span>
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
<div className="flyin__content-card-middle-row">
|
|
386
|
+
<span className="flyin__content-card-middle-row-left">Travel class</span>
|
|
387
|
+
<span className="flyin__content-card-middle-row-right">{firstSegment.metaData.fareMarketingName}</span>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div className="flyin__content-card-middle-row">
|
|
391
|
+
<span className="flyin__content-card-middle-row-left">Booking class</span>
|
|
392
|
+
<span className="flyin__content-card-middle-row-right">{firstSegment.bookingClassCode}</span>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<div className="flyin__content-card-middle-row">
|
|
396
|
+
<span className="flyin__content-card-middle-row-left">Fare basis</span>
|
|
397
|
+
<span className="flyin__content-card-middle-row-right">{firstSegment.metaData.fareCode}</span>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<div className="flyin__content-data">
|
|
401
|
+
{firstSegment.metaData.luggageCarryOn && (
|
|
402
|
+
<div className="flyin__content-data__item">
|
|
403
|
+
<div className="flyin__content-data__item-icon">
|
|
404
|
+
<Icon name="ui-bag" width={20} aria-hidden="true" />
|
|
405
|
+
</div>
|
|
406
|
+
<div className="flyin__content-data__item-content">
|
|
407
|
+
<div className="flyin__content-data__item-content-title">Carry-on luggage</div>
|
|
408
|
+
<div className="flyin__content-data__item-content-description">{firstSegment.metaData.luggageCarryOn.text}</div>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
)}
|
|
412
|
+
|
|
413
|
+
{firstSegment.metaData.luggageChecked && (
|
|
414
|
+
<div className="flyin__content-data__item">
|
|
415
|
+
<div className="flyin__content-data__item-icon">
|
|
416
|
+
<Icon name="ui-suitcase" width={20} />
|
|
417
|
+
</div>
|
|
418
|
+
<div className="flyin__content-data__item-content">
|
|
419
|
+
<div className="flyin__content-data__item-content-title">Checked luggage</div>
|
|
420
|
+
<div className="flyin__content-data__item-content-description">{firstSegment.metaData.luggageChecked.text}</div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
)}
|
|
424
|
+
|
|
425
|
+
{firstSegment.metaData.seatSelection && (
|
|
426
|
+
<div className="flyin__content-data__item">
|
|
427
|
+
<div className="flyin__content-data__item-icon">
|
|
428
|
+
<Icon name="ui-seat" width={20} />
|
|
429
|
+
</div>
|
|
430
|
+
<div className="flyin__content-data__item-content">
|
|
431
|
+
<div className="flyin__content-data__item-content-title">Seat selection</div>
|
|
432
|
+
<div className="flyin__content-data__item-content-description">{firstSegment.metaData.seatSelection.text}</div>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
)}
|
|
436
|
+
|
|
437
|
+
{firstSegment.metaData.cancel && (
|
|
438
|
+
<div className="flyin__content-data__item">
|
|
439
|
+
<div className="flyin__content-data__item-icon">
|
|
440
|
+
<Icon name="ui-refund" width={20} />
|
|
441
|
+
</div>
|
|
442
|
+
<div className="flyin__content-data__item-content">
|
|
443
|
+
<div className="flyin__content-data__item-content-title">Refund</div>
|
|
444
|
+
<div className="flyin__content-data__item-content-description">{firstSegment.metaData.cancel.text}</div>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
|
|
449
|
+
{firstSegment.metaData.other && (
|
|
450
|
+
<div className="flyin__content-data__item">
|
|
451
|
+
<div className="flyin__content-data__item-icon flyin__content-data__item-icon--other">
|
|
452
|
+
<Icon name="ui-else" width={20} />
|
|
453
|
+
</div>
|
|
454
|
+
<div className="flyin__content-data__item-content">
|
|
455
|
+
<div className="flyin__content-data__item-content-title">Other</div>
|
|
456
|
+
<ul className="flyin__content-data__item-content-description flyin__content-data__item-content-description--list">
|
|
457
|
+
{firstSegment.metaData.other.map((other, index) => (
|
|
458
|
+
<li key={`other-${index}`}>{other.text}</li>
|
|
459
|
+
))}
|
|
460
|
+
</ul>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
<div
|
|
468
|
+
className="flyin__content-card-button"
|
|
469
|
+
onClick={() => {
|
|
470
|
+
const fareCode = flightOption.return.segments?.[0]?.metaData?.fareCode;
|
|
471
|
+
setSelectedReturnFareCode(fareCode ?? null);
|
|
472
|
+
}}>
|
|
473
|
+
<div className={`cta ${selectedReturnFareCode === firstSegment.metaData.fareCode ? 'cta--selected' : ''}`}>
|
|
474
|
+
{' '}
|
|
475
|
+
{selectedReturnFareCode === firstSegment.metaData.fareCode ? 'Selected' : 'Select'}
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
);
|
|
480
|
+
})}
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
</>
|
|
484
|
+
)}
|
|
485
|
+
{!flightSearchDetailsLoading && (
|
|
486
|
+
<div className="flyin__footer">
|
|
487
|
+
<div className="flyin__footer__price">Total price: €{selectedCombinationFlight?.price?.toFixed(2)}</div>
|
|
488
|
+
<div className="flyin__button-wrapper">
|
|
489
|
+
<button className="cta cta--select" onClick={handleConfirm}>
|
|
490
|
+
{translations.PRODUCT.BOOK_NOW}
|
|
491
|
+
</button>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
)}
|
|
495
|
+
</>
|
|
496
|
+
);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
export default FlightsFlyIn;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import Icon from '../icon';
|
|
3
|
+
import { useFlightSearch } from '../../../search-results/components/flight/flight-search-context';
|
|
4
|
+
import { useDispatch } from 'react-redux';
|
|
5
|
+
import { setSelectedFlight, setSelectedFlightDetails } from '../../../search-results/store/search-results-slice';
|
|
6
|
+
import { SRPType } from '../../types';
|
|
7
|
+
import FlightsFlyIn from './flights-flyin';
|
|
8
|
+
import AccommodationFlyIn from './accommodation-flyin';
|
|
9
|
+
|
|
10
|
+
type FlyInProps = {
|
|
11
|
+
title: string;
|
|
12
|
+
srpType: SRPType;
|
|
13
|
+
isOpen: boolean;
|
|
14
|
+
setIsOpen: (open: boolean) => void;
|
|
15
|
+
className?: string;
|
|
16
|
+
onPanelRef?: (el: HTMLDivElement | null) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const FlyIn: React.FC<FlyInProps> = ({ title, srpType, isOpen, setIsOpen, className = '', onPanelRef }) => {
|
|
20
|
+
const dispatch = useDispatch();
|
|
21
|
+
const { onCancelSearch } = useFlightSearch();
|
|
22
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
// expose DOM node if needed
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
onPanelRef?.(panelRef.current);
|
|
27
|
+
return () => onPanelRef?.(null);
|
|
28
|
+
}, [onPanelRef]);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
// click outside detection
|
|
32
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
33
|
+
if (isOpen && panelRef.current && !panelRef.current.contains(event.target as Node)) {
|
|
34
|
+
handleClose();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
39
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
40
|
+
}, [isOpen, setIsOpen]);
|
|
41
|
+
|
|
42
|
+
// body scroll lock
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
document.body.style.overflow = isOpen ? 'hidden' : '';
|
|
45
|
+
return () => {
|
|
46
|
+
document.body.style.overflow = '';
|
|
47
|
+
};
|
|
48
|
+
}, [isOpen]);
|
|
49
|
+
|
|
50
|
+
const handleClose = () => {
|
|
51
|
+
if (isOpen && panelRef.current) {
|
|
52
|
+
if (srpType === 'flight') {
|
|
53
|
+
dispatch(setSelectedFlight(null));
|
|
54
|
+
dispatch(setSelectedFlightDetails(null));
|
|
55
|
+
onCancelSearch();
|
|
56
|
+
}
|
|
57
|
+
setIsOpen(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={`flyin ${isOpen ? 'flyin--active' : ''} ${className}`}>
|
|
63
|
+
<div className={`flyin__panel ${isOpen ? 'flyin__panel--active' : ''}`} ref={panelRef}>
|
|
64
|
+
<div className="flyin__content">
|
|
65
|
+
<div className="flyin__content-title-row">
|
|
66
|
+
<h3 className="flyin__content-title">{title}</h3>
|
|
67
|
+
<span className="flyin__close" onClick={() => handleClose()}>
|
|
68
|
+
<Icon name="ui-close" width={30} height={30} aria-hidden="true" />
|
|
69
|
+
</span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
{srpType === 'flight' && <FlightsFlyIn isOpen={isOpen} setIsOpen={setIsOpen} />}
|
|
73
|
+
{(srpType === 'hotel' || srpType === 'groupTour') && <AccommodationFlyIn isLoading={true} isOpen={isOpen} setIsOpen={setIsOpen} />}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default FlyIn;
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"EVENING_DEPARTURE": "مساءً (18:00 - 00:00)",
|
|
50
50
|
"FLIGHTS_FOUND_1": "",
|
|
51
51
|
"FLIGHTS_FOUND_2": "رحلات",
|
|
52
|
-
"FLIGHTS_FOUND_3": "تم العثور عليها"
|
|
52
|
+
"FLIGHTS_FOUND_3": "تم العثور عليها",
|
|
53
|
+
"SELECT_YOUR_FARE": "اختر السعر الخاص بك"
|
|
53
54
|
},
|
|
54
55
|
"PRODUCT": {
|
|
55
56
|
"STAY_INCLUDED": "الإقامة متضمنة",
|
|
@@ -372,6 +373,7 @@
|
|
|
372
373
|
"DEPARTURE_TIME_ASC": "وقت المغادرة تصاعدياً",
|
|
373
374
|
"DEPARTURE_TIME_DESC": "وقت المغادرة تنازلياً",
|
|
374
375
|
"DURATION_ASC": "المدة تصاعدياً",
|
|
375
|
-
"DURATION_DESC": "المدة تنازلياً"
|
|
376
|
+
"DURATION_DESC": "المدة تنازلياً",
|
|
377
|
+
"TRAVEL_GROUP": "مجموعة المسافرين"
|
|
376
378
|
}
|
|
377
379
|
}
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"EVENING_DEPARTURE": "Aften (18:00 - 00:00)",
|
|
50
50
|
"FLIGHTS_FOUND_1": "",
|
|
51
51
|
"FLIGHTS_FOUND_2": "fly",
|
|
52
|
-
"FLIGHTS_FOUND_3": "fundet"
|
|
52
|
+
"FLIGHTS_FOUND_3": "fundet",
|
|
53
|
+
"SELECT_YOUR_FARE": "Vælg din pris"
|
|
53
54
|
},
|
|
54
55
|
"PRODUCT": {
|
|
55
56
|
"STAY_INCLUDED": "Ophold inkluderet",
|
|
@@ -372,6 +373,7 @@
|
|
|
372
373
|
"DEPARTURE_TIME_ASC": "Afgangstid stigende",
|
|
373
374
|
"DEPARTURE_TIME_DESC": "Afgangstid faldende",
|
|
374
375
|
"DURATION_ASC": "Varighed stigende",
|
|
375
|
-
"DURATION_DESC": "Varighed faldende"
|
|
376
|
+
"DURATION_DESC": "Varighed faldende",
|
|
377
|
+
"TRAVEL_GROUP": "Rejseselskab"
|
|
376
378
|
}
|
|
377
379
|
}
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"EVENING_DEPARTURE": "Abend (18:00 - 00:00)",
|
|
50
50
|
"FLIGHTS_FOUND_1": "",
|
|
51
51
|
"FLIGHTS_FOUND_2": "Flüge",
|
|
52
|
-
"FLIGHTS_FOUND_3": "gefunden"
|
|
52
|
+
"FLIGHTS_FOUND_3": "gefunden",
|
|
53
|
+
"SELECT_YOUR_FARE": "Wählen Sie Ihren Tarif"
|
|
53
54
|
},
|
|
54
55
|
"PRODUCT": {
|
|
55
56
|
"STAY_INCLUDED": "Aufenthalt inbegriffen",
|
|
@@ -372,6 +373,7 @@
|
|
|
372
373
|
"DEPARTURE_RANGE": "Abflugzeitraum",
|
|
373
374
|
"DEPARTURE_AIRPORTS": "Abflughäfen",
|
|
374
375
|
"ARRIVAL_AIRPORTS": "Ankunftsflughäfen",
|
|
375
|
-
"PRICE": "Preis"
|
|
376
|
+
"PRICE": "Preis",
|
|
377
|
+
"TRAVEL_GROUP": "Reisegruppe"
|
|
376
378
|
}
|
|
377
379
|
}
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"EVENING_DEPARTURE": "Evening (18:00 - 00:00)",
|
|
50
50
|
"FLIGHTS_FOUND_1": "",
|
|
51
51
|
"FLIGHTS_FOUND_2": "flights",
|
|
52
|
-
"FLIGHTS_FOUND_3": "found"
|
|
52
|
+
"FLIGHTS_FOUND_3": "found",
|
|
53
|
+
"SELECT_YOUR_FARE": "Select your fare"
|
|
53
54
|
},
|
|
54
55
|
"PRODUCT": {
|
|
55
56
|
"STAY_INCLUDED": "Stay included",
|
|
@@ -376,6 +377,7 @@
|
|
|
376
377
|
"NIGHT_RANGE": "Night",
|
|
377
378
|
"DEPARTURE_RANGE": "Departure range",
|
|
378
379
|
"DEPARTURE_AIRPORTS": "Departure airports",
|
|
379
|
-
"ARRIVAL_AIRPORTS": "Arrival airports"
|
|
380
|
+
"ARRIVAL_AIRPORTS": "Arrival airports",
|
|
381
|
+
"TRAVEL_GROUP": "Travel group"
|
|
380
382
|
}
|
|
381
383
|
}
|