@qite/tide-booking-component 1.4.102 → 1.4.104

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.
Files changed (55) hide show
  1. package/build/build-cjs/index.js +1773 -877
  2. package/build/build-cjs/src/search-results/components/filters/filters.d.ts +2 -0
  3. package/build/build-cjs/src/search-results/components/flight/flight-selection/independent-flight-option.d.ts +3 -0
  4. package/build/build-cjs/src/search-results/components/hotel/hotel-accommodation-results.d.ts +1 -0
  5. package/build/build-cjs/src/search-results/store/search-results-selectors.d.ts +424 -0
  6. package/build/build-cjs/src/search-results/store/search-results-slice.d.ts +27 -8
  7. package/build/build-cjs/src/search-results/types.d.ts +14 -2
  8. package/build/build-cjs/src/search-results/utils/search-results-utils.d.ts +8 -6
  9. package/build/build-cjs/src/shared/components/flyin/flyin.d.ts +3 -3
  10. package/build/build-cjs/src/shared/components/flyin/packaging-flights-flyin.d.ts +7 -0
  11. package/build/build-cjs/src/shared/utils/localization-util.d.ts +2 -0
  12. package/build/build-esm/index.js +1747 -861
  13. package/build/build-esm/src/search-results/components/filters/filters.d.ts +2 -0
  14. package/build/build-esm/src/search-results/components/flight/flight-selection/independent-flight-option.d.ts +3 -0
  15. package/build/build-esm/src/search-results/components/hotel/hotel-accommodation-results.d.ts +1 -0
  16. package/build/build-esm/src/search-results/store/search-results-selectors.d.ts +424 -0
  17. package/build/build-esm/src/search-results/store/search-results-slice.d.ts +27 -8
  18. package/build/build-esm/src/search-results/types.d.ts +14 -2
  19. package/build/build-esm/src/search-results/utils/search-results-utils.d.ts +8 -6
  20. package/build/build-esm/src/shared/components/flyin/flyin.d.ts +3 -3
  21. package/build/build-esm/src/shared/components/flyin/packaging-flights-flyin.d.ts +7 -0
  22. package/build/build-esm/src/shared/utils/localization-util.d.ts +2 -0
  23. package/package.json +2 -2
  24. package/src/booking-wizard/features/flight-options/index.tsx +6 -2
  25. package/src/search-results/components/filters/filters.tsx +8 -9
  26. package/src/search-results/components/flight/flight-selection/independent-flight-option.tsx +31 -4
  27. package/src/search-results/components/hotel/hotel-accommodation-results.tsx +81 -25
  28. package/src/search-results/components/icon.tsx +1 -1
  29. package/src/search-results/components/search-results-container/search-results-container.tsx +194 -130
  30. package/src/search-results/store/search-results-selectors.ts +73 -0
  31. package/src/search-results/store/search-results-slice.ts +94 -14
  32. package/src/search-results/types.ts +14 -2
  33. package/src/search-results/utils/search-results-utils.ts +310 -58
  34. package/src/shared/components/flyin/flyin.tsx +102 -19
  35. package/src/shared/components/flyin/packaging-flights-flyin.tsx +164 -0
  36. package/src/shared/translations/ar-SA.json +2 -0
  37. package/src/shared/translations/da-DK.json +2 -0
  38. package/src/shared/translations/de-DE.json +2 -0
  39. package/src/shared/translations/en-GB.json +2 -0
  40. package/src/shared/translations/es-ES.json +2 -0
  41. package/src/shared/translations/fr-BE.json +2 -0
  42. package/src/shared/translations/fr-FR.json +2 -0
  43. package/src/shared/translations/is-IS.json +2 -0
  44. package/src/shared/translations/it-IT.json +2 -0
  45. package/src/shared/translations/ja-JP.json +2 -0
  46. package/src/shared/translations/nl-BE.json +2 -0
  47. package/src/shared/translations/nl-NL.json +2 -0
  48. package/src/shared/translations/no-NO.json +2 -0
  49. package/src/shared/translations/pl-PL.json +2 -0
  50. package/src/shared/translations/pt-PT.json +2 -0
  51. package/src/shared/translations/sv-SE.json +2 -0
  52. package/src/shared/utils/localization-util.ts +5 -2
  53. package/styles/components/_flight-option.scss +14 -1
  54. package/styles/components/_flyin.scss +16 -0
  55. package/styles/components/_search.scss +9 -1
@@ -1,9 +1,7 @@
1
- import React, { useContext, useEffect, useState } from 'react';
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
- dispatch(setFilters(updated));
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
- dispatch(setFilters(updated));
60
+ setFilters(updated);
62
61
  };
63
62
 
64
63
  const handleFullReset = () => {
65
64
  if (!isLoading) {
66
- dispatch(resetFilters(initialFilters));
65
+ resetFilters(initialFilters);
67
66
  }
68
67
  };
69
68
 
@@ -1,5 +1,5 @@
1
1
  import React, { useContext, useState } from 'react';
2
- import { durationTicksInHoursString, getTranslations, longFormatDate, timeFromDateTime } from '../../../../shared/utils/localization-util';
2
+ import { durationTicksInHoursString, formatPrice, getTranslations, longFormatDate, timeFromDateTime } from '../../../../shared/utils/localization-util';
3
3
  import Icon from '../../icon';
4
4
  import SearchResultsConfigurationContext from '../../../search-results-configuration-context';
5
5
  import { getArrivalSegment, getDepartureSegment, getFlightSegments, getNumberOfStopsLabel } from '../../../utils/flight-utils';
@@ -11,18 +11,32 @@ interface IndependentFlightOptionProps {
11
11
  guid: string | null;
12
12
  selectedGuid?: string | null;
13
13
  isOutward: boolean;
14
+ showSelectedState?: boolean;
15
+ currentSelectedPrice?: number;
16
+ price?: number;
14
17
  }
15
18
 
16
- const IndependentFlightOption: React.FC<IndependentFlightOptionProps> = ({ item, onSelect, guid, selectedGuid, isOutward }) => {
19
+ const IndependentFlightOption: React.FC<IndependentFlightOptionProps> = ({
20
+ item,
21
+ onSelect,
22
+ guid,
23
+ selectedGuid,
24
+ isOutward,
25
+ showSelectedState,
26
+ currentSelectedPrice,
27
+ price
28
+ }) => {
17
29
  const context = useContext(SearchResultsConfigurationContext);
18
30
  const language = context?.languageCode ?? 'en-GB';
19
31
  const translations = getTranslations(language);
20
32
 
21
33
  const [detailsOpen, setDetailsOpen] = useState(false);
22
34
 
35
+ const priceDifference = price && currentSelectedPrice ? price - currentSelectedPrice : null;
36
+
23
37
  return (
24
38
  <div className="search__result-card" key={`flight-${item.code}`}>
25
- <div className="flight">
39
+ <div className={`flight ${showSelectedState && selectedGuid === guid ? 'flight--selected' : ''}`}>
26
40
  <div className="flight__option">
27
41
  <div className="flight__content">
28
42
  <div className="flight__flights">
@@ -30,7 +44,20 @@ const IndependentFlightOption: React.FC<IndependentFlightOptionProps> = ({ item,
30
44
  <div className="flight__flight__header">
31
45
  <div className="flight__status__container"></div>
32
46
  <div className="flight__price">
33
- {/* <span className="price">{formatPrice(item.price, 'EUR', context?.languageCode ?? 'en-GB')}</span> */}
47
+ {price != null &&
48
+ price > 0 &&
49
+ (isOutward ? (
50
+ <span className="price">
51
+ {translations.QSM.ROUNDTRIP + ' ' + translations.SRP.PRICE}&nbsp;{formatPrice(price, 'EUR', context?.languageCode ?? 'en-GB')}
52
+ </span>
53
+ ) : (
54
+ priceDifference != null &&
55
+ Math.abs(priceDifference) > 0 && (
56
+ <span className="price">
57
+ {priceDifference > 0 ? '+' : '-'}&nbsp;{formatPrice(Math.abs(priceDifference), 'EUR', context?.languageCode ?? 'en-GB')}
58
+ </span>
59
+ )
60
+ ))}
34
61
  <button type="button" className={`cta ${selectedGuid === guid ? 'cta--selected' : 'cta--select'}`} onClick={() => onSelect?.()}>
35
62
  {selectedGuid === guid ? 'Selected' : 'Select'}
36
63
  </button>
@@ -1,17 +1,20 @@
1
- import React, { useContext } from 'react';
2
- import { HotelResult, SearchResultsConfiguration } from '../../types';
1
+ import React, { useContext, useState } from 'react';
2
+ import { FlyInType, HotelResult, SearchResultsConfiguration } from '../../types';
3
3
  import Spinner from '../spinner/spinner';
4
4
  import HotelCard from './hotel-card';
5
- import { useSelector } from 'react-redux';
5
+ import { useDispatch, useSelector } from 'react-redux';
6
6
  import { SearchResultsRootState } from '../../store/search-results-store';
7
7
  import { BookingPackageItem, PackagingAccommodationResponse, PortalQsmType } from '@qite/tide-client';
8
8
  import { format, parseISO } from 'date-fns';
9
9
  import { calculateNights, formatPrice, getTranslations } from '../../../shared/utils/localization-util';
10
10
  import SearchResultsConfigurationContext from '../../search-results-configuration-context';
11
11
  import { first } from 'lodash';
12
+ import Icon from '../icon';
13
+ import { setFlyInIsOpen, setFlyInType } from '../../store/search-results-slice';
12
14
 
13
15
  interface HotelAccommodationResultsProps {
14
16
  isLoading: boolean;
17
+ isFlyIn?: boolean;
15
18
  }
16
19
 
17
20
  const getLocation = (locationName?: string, regionName?: string, countryName?: string, fallback?: string): string => {
@@ -70,7 +73,14 @@ const mapPackagingAccoResult = (searchResult: PackagingAccommodationResponse, cm
70
73
  };
71
74
  };
72
75
 
73
- const renderHotelResults = (results: HotelResult[], context: SearchResultsConfiguration, activeTab: string | null, translations: any) => {
76
+ const renderHotelResults = (
77
+ results: HotelResult[],
78
+ context: SearchResultsConfiguration,
79
+ activeTab: string | null,
80
+ translations: any,
81
+ selectedPackagingAccoResult?: HotelResult,
82
+ isFlyIn?: boolean
83
+ ) => {
74
84
  const renderedResults = results.map((result, index) => {
75
85
  const key = `${result.id ?? result.code}-${index}`;
76
86
 
@@ -85,19 +95,28 @@ const renderHotelResults = (results: HotelResult[], context: SearchResultsConfig
85
95
  return <HotelCard key={key} result={result} translations={translations} />;
86
96
  });
87
97
 
88
- return <div className={`search__results__cards ${activeTab ? `search__results__cards--${activeTab}` : ''}`}>{renderedResults}</div>;
98
+ return (
99
+ <div className={`search__results__cards ${activeTab ? `search__results__cards--${activeTab}` : ''}`}>
100
+ {selectedPackagingAccoResult && !isFlyIn && (
101
+ <HotelCard key={selectedPackagingAccoResult.code} result={selectedPackagingAccoResult} translations={translations} />
102
+ )}
103
+ {renderedResults}
104
+ </div>
105
+ );
89
106
  };
90
107
 
91
- const HotelAccommodationResults: React.FC<HotelAccommodationResultsProps> = ({ isLoading }) => {
108
+ const HotelAccommodationResults: React.FC<HotelAccommodationResultsProps> = ({ isLoading, isFlyIn }) => {
92
109
  const context = useContext(SearchResultsConfigurationContext);
93
-
110
+ const dispatch = useDispatch();
94
111
  if (!context) {
95
112
  return null;
96
113
  }
97
114
 
98
115
  const translations = getTranslations(context.languageCode ?? 'en-GB');
99
116
 
100
- const { filteredResults, filteredPackagingAccoResults, activeTab } = useSelector((state: SearchResultsRootState) => state.searchResults);
117
+ const { filteredResults, filteredPackagingAccoResults, packagingAccoResults, activeTab, flyInIsOpen, selectedPackagingAccoResultCode } = useSelector(
118
+ (state: SearchResultsRootState) => state.searchResults
119
+ );
101
120
 
102
121
  const cmsMap = React.useMemo(() => {
103
122
  const map = new Map();
@@ -146,29 +165,66 @@ const HotelAccommodationResults: React.FC<HotelAccommodationResultsProps> = ({ i
146
165
  const firstResultDay = firstResultDate ? format(parseISO(firstResultDate), 'd') : null;
147
166
  const firstResultMonth = firstResultDate ? format(parseISO(firstResultDate), 'MMM') : null;
148
167
 
149
- if (isLoading) {
150
- return <>{context.customSpinner ?? <Spinner />}</>;
151
- }
168
+ const selectedPackagingAccoResult = React.useMemo(() => {
169
+ const selectedResult = packagingAccoResults.find((result) => result.code === selectedPackagingAccoResultCode);
170
+ if (selectedResult) {
171
+ return mapPackagingAccoResult(selectedResult, cmsMap.get(selectedResult.code), context.languageCode, translations);
172
+ }
173
+ }, [packagingAccoResults, selectedPackagingAccoResultCode, cmsMap, context.languageCode, translations]);
152
174
 
153
- if (mappedResults.length === 0) {
154
- return <div className="no-results">{translations.SRP.NO_RESULTS}</div>;
155
- }
175
+ const visibleResults = React.useMemo(() => {
176
+ const shouldShowAll = context?.searchConfiguration.qsmType !== PortalQsmType.AccommodationAndFlight || isFlyIn;
177
+
178
+ if (shouldShowAll) {
179
+ return mappedResults;
180
+ }
181
+
182
+ if (selectedPackagingAccoResult) {
183
+ return mappedResults.filter((result) => result.code !== selectedPackagingAccoResult.code).slice(0, 2);
184
+ }
185
+
186
+ return mappedResults.slice(0, 3);
187
+ }, [context?.searchConfiguration.qsmType, mappedResults, isFlyIn, selectedPackagingAccoResult]);
188
+
189
+ const handleShowMoreHotels = (flyInType: FlyInType) => {
190
+ dispatch(setFlyInType(flyInType));
191
+ dispatch(setFlyInIsOpen(true));
192
+ };
156
193
 
157
194
  return (
158
195
  <>
159
- <div className="search__results__label search__results__label--secondary">
160
- <div className="search__results__label__date">
161
- <p className="search__results__label__date-date">{firstResultDay}</p>
162
- <p>{firstResultMonth}</p>
196
+ {!isFlyIn && (
197
+ <div className="search__results__label search__results__label--secondary">
198
+ <div className="search__results__label__date">
199
+ {firstResultDay && firstResultMonth ? (
200
+ <>
201
+ <p className="search__results__label__date-date">{firstResultDay}</p>
202
+ <p>{firstResultMonth}</p>
203
+ </>
204
+ ) : (
205
+ <Icon name="ui-bed" height={16} fill="white" />
206
+ )}
207
+ </div>
208
+ <div className="search__results__label__text">
209
+ <h3>
210
+ {translations.SRP.SELECT} <strong>{translations.SRP.ACCOMMODATION}</strong>
211
+ </h3>
212
+ </div>
163
213
  </div>
164
- <div className="search__results__label__text">
165
- <h3>
166
- {translations.SRP.SELECT} <strong>{translations.SRP.ACCOMMODATION}</strong>
167
- </h3>
214
+ )}
215
+ {isLoading ? (
216
+ <>{context.customSpinner ?? <Spinner />}</>
217
+ ) : (
218
+ renderHotelResults(visibleResults, context, activeTab, translations, selectedPackagingAccoResult, isFlyIn)
219
+ )}
220
+ {packagingAccoResults.length > 3 && !isFlyIn && (
221
+ <div className="search__results__cards__actions">
222
+ <button className="cta cta--secondary" onClick={() => handleShowMoreHotels('acco-results')}>
223
+ {translations.SRP.SHOW_MORE}
224
+ </button>
168
225
  </div>
169
- </div>
170
-
171
- {renderHotelResults(mappedResults, context, activeTab, translations)}
226
+ )}
227
+ {mappedResults.length === 0 && !isLoading && <div className="no-results">{translations.SRP.NO_RESULTS}</div>}
172
228
  </>
173
229
  );
174
230
  };
@@ -134,7 +134,7 @@ const Icon: React.FC<IconProps> = ({ name, className, title, width, height, fill
134
134
  {title && <title>{title}</title>}
135
135
  <path
136
136
  d="M32 32c17.7 0 32 14.3 32 32l0 256 224 0 0-160c0-17.7 14.3-32 32-32l224 0c53 0 96 43 96 96l0 224c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-32-224 0-32 0L64 416l0 32c0 17.7-14.3 32-32 32s-32-14.3-32-32L0 64C0 46.3 14.3 32 32 32zm144 96a80 80 0 1 1 0 160 80 80 0 1 1 0-160z"
137
- fill="currentColor"
137
+ fill={fill ?? 'currentColor'}
138
138
  />
139
139
  </svg>
140
140
  );