@qite/tide-booking-component 1.4.113 → 1.4.115

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 (23) hide show
  1. package/build/build-cjs/index.js +219 -130
  2. package/build/build-cjs/src/search-results/components/book-packaging-entry/index.d.ts +1 -0
  3. package/build/build-cjs/src/search-results/components/book-packaging-entry/wl-sidebar.d.ts +2 -0
  4. package/build/build-cjs/src/search-results/types.d.ts +1 -0
  5. package/build/build-cjs/src/shared/utils/booking-summary.d.ts +1 -0
  6. package/build/build-esm/index.js +219 -130
  7. package/build/build-esm/src/search-results/components/book-packaging-entry/index.d.ts +1 -0
  8. package/build/build-esm/src/search-results/components/book-packaging-entry/wl-sidebar.d.ts +2 -0
  9. package/build/build-esm/src/search-results/types.d.ts +1 -0
  10. package/build/build-esm/src/shared/utils/booking-summary.d.ts +1 -0
  11. package/package.json +1 -1
  12. package/src/search-results/components/book-packaging-entry/index.tsx +27 -7
  13. package/src/search-results/components/book-packaging-entry/wl-sidebar.tsx +27 -16
  14. package/src/search-results/components/flight/flight-selection/independent-flight-selection.tsx +5 -2
  15. package/src/search-results/components/hotel/hotel-accommodation-results.tsx +11 -2
  16. package/src/search-results/components/search-results-container/search-results-container.tsx +5 -1
  17. package/src/search-results/types.ts +1 -0
  18. package/src/shared/booking/summary.tsx +0 -1
  19. package/src/shared/components/flyin/flights-flyin.tsx +5 -2
  20. package/src/shared/components/flyin/flyin.tsx +10 -1
  21. package/src/shared/utils/booking-summary.tsx +11 -0
  22. package/src/shared/utils/tide-api-utils.ts +2 -2
  23. package/styles/components/_select-wrapper.scss +5 -0
@@ -16,13 +16,23 @@ import { useFormik } from 'formik';
16
16
  import { TravelersFormValues } from '../../../booking-wizard/types';
17
17
  import { setBookingNumber, setCurrentStep, setEditablePackagingEntry } from '../../store/search-results-slice';
18
18
  import validateForm from '../../../booking-wizard/features/travelers-form/validate-form';
19
- import { bookPackagingEntry, CountryItem, getCountries, PackagingEntry, PackagingRequestBase, TideClientConfig } from '@qite/tide-client';
19
+ import {
20
+ bookPackagingEntry,
21
+ CountryItem,
22
+ getCountries,
23
+ PackagingAccommodationResponse,
24
+ PackagingEntry,
25
+ PackagingRequestBase,
26
+ TideClientConfig
27
+ } from '@qite/tide-client';
20
28
  import SharedSummary from '../../../shared/booking/summary';
21
29
  import { renderEditablePackagingEntrySummaryOptions } from '../../../shared/utils/booking-summary';
22
30
  import SharedConfirmation from '../../../shared/booking/shared-confirmation';
31
+ import Spinner from '../spinner/spinner';
23
32
 
24
33
  interface BookPackagingEntryProps {
25
34
  activeSearchSeed: SearchSeed | null;
35
+ isLoading: boolean;
26
36
  isConfirmationPage?: boolean;
27
37
  }
28
38
 
@@ -49,16 +59,19 @@ const travellersSettings: SharedTravelersSettings = {
49
59
  mainBookerFormFields
50
60
  };
51
61
 
52
- const BookPackagingEntry: React.FC<BookPackagingEntryProps> = ({ activeSearchSeed, isConfirmationPage }) => {
62
+ const BookPackagingEntry: React.FC<BookPackagingEntryProps> = ({ activeSearchSeed, isLoading, isConfirmationPage }) => {
53
63
  const context = useContext(SearchResultsConfigurationContext);
54
64
  const dispatch = useDispatch();
55
65
 
56
- const { editablePackagingEntry, priceDetails, currentStep, bookingNumber } = useSelector((state: SearchResultsRootState) => state.searchResults);
66
+ const { editablePackagingEntry, priceDetails, currentStep, bookingNumber, selectedPackagingAccoResultCode, packagingAccoResults } = useSelector(
67
+ (state: SearchResultsRootState) => state.searchResults
68
+ );
57
69
 
58
70
  const [countries, setCountries] = useState<CountryItem[]>([]);
59
71
  const [userValidated, setUserValidated] = useState(true);
60
72
  const [remarks, setRemarks] = useState('');
61
73
  const [isSubmitting, setIsSubmitting] = useState(false);
74
+ const [selectedPackagingAccoResult, setSelectedPackagingAccoResult] = useState<PackagingAccommodationResponse | null>(null);
62
75
 
63
76
  const translations = useMemo(() => getTranslations(context?.languageCode ?? 'en-GB'), [context?.languageCode]);
64
77
 
@@ -117,7 +130,14 @@ const BookPackagingEntry: React.FC<BookPackagingEntryProps> = ({ activeSearchSee
117
130
  }
118
131
  }, [isConfirmationPage, dispatch]);
119
132
 
120
- if (!context || !editablePackagingEntry || !priceDetails || !config) return null;
133
+ useEffect(() => {
134
+ const selectedPackagingAccoResult = packagingAccoResults?.find((result) => result.code === selectedPackagingAccoResultCode);
135
+ if (selectedPackagingAccoResult) {
136
+ setSelectedPackagingAccoResult(selectedPackagingAccoResult);
137
+ }
138
+ }, [selectedPackagingAccoResultCode, packagingAccoResults]);
139
+
140
+ if (!context || !editablePackagingEntry || !priceDetails || !config) return <Spinner label={translations.SUMMARY.PROCESS_BOOKING} />;
121
141
 
122
142
  const handleSummarySubmit: React.FormEventHandler<HTMLFormElement> = async (e) => {
123
143
  e.preventDefault();
@@ -165,11 +185,10 @@ const BookPackagingEntry: React.FC<BookPackagingEntryProps> = ({ activeSearchSee
165
185
  window.location.href = bookingResponse.paymentUrl;
166
186
  } else {
167
187
  dispatch(setCurrentStep(2));
188
+ setIsSubmitting(false);
168
189
  }
169
190
  } catch (error) {
170
191
  dispatch(setCurrentStep(3));
171
- } finally {
172
- setIsSubmitting(false);
173
192
  }
174
193
  };
175
194
 
@@ -186,6 +205,7 @@ const BookPackagingEntry: React.FC<BookPackagingEntryProps> = ({ activeSearchSee
186
205
  {step + 1}.&nbsp;{stepLabels[step]}
187
206
  </>
188
207
  )}>
208
+ {isConfirmationPage && isLoading && <Spinner label={translations.SUMMARY.PROCESS_BOOKING} />}
189
209
  {currentStep === 0 && (
190
210
  <SharedTravelersForm
191
211
  formik={formik}
@@ -237,7 +257,7 @@ const BookPackagingEntry: React.FC<BookPackagingEntryProps> = ({ activeSearchSee
237
257
 
238
258
  <div className="backdrop" id="backdrop"></div>
239
259
 
240
- <WLSidebar activeSearchSeed={activeSearchSeed} />
260
+ <WLSidebar activeSearchSeed={activeSearchSeed} packagingAccoResult={selectedPackagingAccoResult} />
241
261
  </div>
242
262
  </div>
243
263
  );
@@ -10,12 +10,14 @@ import { SearchSeed } from '../../types';
10
10
  import { getTravelersText } from '../../../booking-wizard/features/sidebar/sidebar-util';
11
11
  import { RoomTraveler } from '../../../booking-wizard/types';
12
12
 
13
- import { BookingPackageFlightMetaData, BookingPriceDetail, PackagingEntryLine } from '@qite/tide-client';
13
+ import { BookingPackageFlightMetaData, BookingPriceDetail, PackagingAccommodationResponse, PackagingEntryLine, PortalQsmType } from '@qite/tide-client';
14
14
  import Spinner from '../spinner/spinner';
15
15
  import SharedSidebar from '../../../shared/booking/shared-sidebar';
16
+ import { getImageSrcFromHtml } from '../../../shared/utils/booking-summary';
16
17
 
17
18
  interface WLSidebarProps {
18
19
  activeSearchSeed: SearchSeed | null;
20
+ packagingAccoResult: PackagingAccommodationResponse | null;
19
21
  }
20
22
 
21
23
  const mapToSidebarFlightMetaData = (entryLine: PackagingEntryLine) => {
@@ -66,18 +68,12 @@ const selectSeparatePackagePriceDetails = (priceDetails: BookingPriceDetail[]) =
66
68
  return result;
67
69
  };
68
70
 
69
- const WLSidebar: React.FC<WLSidebarProps> = ({ activeSearchSeed }) => {
71
+ const WLSidebar: React.FC<WLSidebarProps> = ({ activeSearchSeed, packagingAccoResult }) => {
70
72
  const context = useContext(SearchResultsConfigurationContext);
71
- if (!context) {
72
- return null;
73
- }
74
- const translations = getTranslations(context.languageCode ?? 'en-GB');
73
+
75
74
  const { editablePackagingEntry, priceDetails } = useSelector((state: SearchResultsRootState) => state.searchResults);
76
75
 
77
- // Map editablePackagingEntry to sidebar props (example, adjust as needed)
78
- if (!editablePackagingEntry) {
79
- return null;
80
- }
76
+ const translations = getTranslations(context?.languageCode ?? 'en-GB');
81
77
 
82
78
  const sortedLines = useMemo(() => {
83
79
  return [...(editablePackagingEntry?.lines ?? [])].sort((a, b) => {
@@ -92,16 +88,31 @@ const WLSidebar: React.FC<WLSidebarProps> = ({ activeSearchSeed }) => {
92
88
  });
93
89
  }, [editablePackagingEntry]);
94
90
 
91
+ const accoImage = useMemo(() => {
92
+ return getImageSrcFromHtml(packagingAccoResult?.contents);
93
+ }, [packagingAccoResult?.contents]);
94
+
95
+ if (!context || !editablePackagingEntry) {
96
+ return null;
97
+ }
98
+
95
99
  const firstEntryLine = first(sortedLines);
96
100
  const accommodationLines = editablePackagingEntry.lines.filter((line) => line.serviceType === ACCOMMODATION_SERVICE_TYPE);
97
101
  const accommodationLine = first(accommodationLines) ?? firstEntryLine;
98
102
 
99
103
  const location =
100
- accommodationLine?.location?.name ??
101
- accommodationLine?.oord?.name ??
102
- accommodationLine?.region?.name ??
103
- accommodationLine?.country?.name ??
104
- firstEntryLine?.location?.name;
104
+ context.searchConfiguration.qsmType === PortalQsmType.Accommodation
105
+ ? packagingAccoResult?.name ??
106
+ accommodationLine?.location?.name ??
107
+ accommodationLine?.oord?.name ??
108
+ accommodationLine?.region?.name ??
109
+ accommodationLine?.country?.name ??
110
+ firstEntryLine?.location?.name
111
+ : accommodationLine?.location?.name ??
112
+ accommodationLine?.oord?.name ??
113
+ accommodationLine?.region?.name ??
114
+ accommodationLine?.country?.name ??
115
+ firstEntryLine?.location?.name;
105
116
 
106
117
  const rooms =
107
118
  activeSearchSeed?.rooms.map((room) => {
@@ -136,7 +147,7 @@ const WLSidebar: React.FC<WLSidebarProps> = ({ activeSearchSeed }) => {
136
147
  return (
137
148
  <SharedSidebar
138
149
  productName={location ?? ''}
139
- thumbnailUrl={context.destinationImage?.url}
150
+ thumbnailUrl={accoImage ?? context.destinationImage?.url}
140
151
  translations={translations}
141
152
  travelerRooms={travelerRooms}
142
153
  startDateText={first(sortedLines)?.from && formatDate(new Date(first(sortedLines)!.from))}
@@ -94,9 +94,12 @@ const IndependentFlightSelection: React.FC<IndependentFlightSelectionProps> = ({
94
94
  }, [searchResults, selectedReturnKey]);
95
95
 
96
96
  const firstResultDate = uniqueOutwardFlights.length > 0 ? uniqueOutwardFlights[0].outward.segments[0].departureDateTime : null;
97
+ const firstResultReturnDate = uniqueReturnFlights.length > 0 ? uniqueReturnFlights[0].return.segments[0].departureDateTime : null;
97
98
 
98
99
  const firstResultDay = firstResultDate ? format(firstResultDate, 'd') : null;
99
100
  const firstResultMonth = firstResultDate ? format(firstResultDate, 'MMM') : null;
101
+ const firstResultReturnDay = firstResultReturnDate ? format(firstResultReturnDate, 'd') : null;
102
+ const firstResultReturnMonth = firstResultReturnDate ? format(firstResultReturnDate, 'MMM') : null;
100
103
 
101
104
  return (
102
105
  <>
@@ -138,8 +141,8 @@ const IndependentFlightSelection: React.FC<IndependentFlightSelectionProps> = ({
138
141
 
139
142
  <div className="search__results__label search__results__label--secondary">
140
143
  <div className="search__results__label__date">
141
- {/* <p className="search__results__label__date-date">{format(day, 'd', { locale: getLocale(context?.languageCode ?? 'en-GB') })}</p>
142
- <p>{format(day, 'MMM', { locale: getLocale(context?.languageCode ?? 'en-GB') })}</p> */}
144
+ <p className="search__results__label__date-date">{firstResultReturnDay}</p>
145
+ <p>{firstResultReturnMonth}</p>
143
146
  </div>
144
147
  <div className="search__results__label__text">
145
148
  <Icon name="ui-flight" height={16} fill="white" />
@@ -175,12 +175,21 @@ const HotelAccommodationResults: React.FC<HotelAccommodationResultsProps> = ({ i
175
175
  const visibleResults = React.useMemo(() => {
176
176
  const shouldShowAll = context?.searchConfiguration.qsmType !== PortalQsmType.AccommodationAndFlight || isFlyIn;
177
177
 
178
+ let filteredMapperResults = mappedResults;
179
+
180
+ if (selectedPackagingAccoResult) {
181
+ filteredMapperResults = mappedResults.filter((result) => result.code !== selectedPackagingAccoResult.code);
182
+ }
183
+
178
184
  if (shouldShowAll) {
179
- return mappedResults;
185
+ if (isFlyIn) {
186
+ return mappedResults;
187
+ }
188
+ return filteredMapperResults;
180
189
  }
181
190
 
182
191
  if (selectedPackagingAccoResult) {
183
- return mappedResults.filter((result) => result.code !== selectedPackagingAccoResult.code).slice(0, 2);
192
+ return filteredMapperResults.filter((result) => result.code !== selectedPackagingAccoResult.code).slice(0, 2);
184
193
  }
185
194
 
186
195
  return mappedResults.slice(0, 3);
@@ -1489,7 +1489,11 @@ const SearchResultsContainer: React.FC<SearchResultsContainerProps> = ({ onBooki
1489
1489
  {context && (
1490
1490
  <div className="search">
1491
1491
  {bookPackagingEntry ? (
1492
- <BookPackagingEntry activeSearchSeed={activeSearchSeed} isConfirmationPage={isBookingConfirmation} />
1492
+ <BookPackagingEntry
1493
+ activeSearchSeed={activeSearchSeed}
1494
+ isLoading={itineraryIsLoading || pricesAreLoading}
1495
+ isConfirmationPage={isBookingConfirmation}
1496
+ />
1493
1497
  ) : (
1494
1498
  <div className="search__container">
1495
1499
  {context.searchConfiguration.qsmType === PortalQsmType.Flight && (
@@ -60,6 +60,7 @@ export interface SearchResultsConfiguration {
60
60
 
61
61
  destinationImage?: { url: string; alt: string };
62
62
  onBook?: (result: BookingPackage) => void;
63
+ onFlightBook?: (result: ExtendedFlightSearchResponseItem) => void;
63
64
  packagingEntry?: PackagingEntry | null;
64
65
 
65
66
  generatePaymentUrl?: boolean;
@@ -1,7 +1,6 @@
1
1
  import { compact, findIndex, isEmpty, isNil, uniqBy } from 'lodash';
2
2
  import React, { ReactNode, useEffect, useState } from 'react';
3
3
  import { SummaryCheckbox, TravelersFormValues } from '../../booking-wizard/types';
4
- import Loader from '../components/loader';
5
4
  import { buildClassName } from '../utils/class-util';
6
5
  import Icon from '../components/icon';
7
6
  import Spinner from '../../search-results/components/spinner/spinner';
@@ -140,10 +140,13 @@ const FlightsFlyIn: React.FC<FlightsFlyInProps> = ({ isOpen, setIsOpen }) => {
140
140
  };
141
141
 
142
142
  // TODO: go to booking page?
143
- const handleConfirm = () => {
143
+ const onHandleConfirm = () => {
144
144
  if (isOpen) {
145
145
  onCancelSearch();
146
146
  setIsOpen(false);
147
+ if (context?.onFlightBook && selectedCombinationFlight) {
148
+ context.onFlightBook(selectedCombinationFlight);
149
+ }
147
150
  }
148
151
  };
149
152
 
@@ -492,7 +495,7 @@ const FlightsFlyIn: React.FC<FlightsFlyInProps> = ({ isOpen, setIsOpen }) => {
492
495
  {translations.SHARED.TOTAL_PRICE}: €{selectedCombinationFlight?.price?.toFixed(2)}
493
496
  </div>
494
497
  <div className="flyin__button-wrapper">
495
- <button className="cta cta--select" onClick={handleConfirm}>
498
+ <button className="cta cta--select" onClick={onHandleConfirm}>
496
499
  {translations.PRODUCT.BOOK_NOW}
497
500
  </button>
498
501
  </div>
@@ -4,6 +4,7 @@ import { useFlightSearch } from '../../../search-results/components/flight/fligh
4
4
  import { useDispatch, useSelector } from 'react-redux';
5
5
  import {
6
6
  resetFilters,
7
+ setBookPackagingEntry,
7
8
  setFilters,
8
9
  setFlyInType,
9
10
  setSelectedFlight,
@@ -125,6 +126,14 @@ const FlyIn: React.FC<FlyInProps> = ({
125
126
  }
126
127
  };
127
128
 
129
+ const onHandleConfirm = () => {
130
+ if (context?.packagingEntry) {
131
+ handleConfirm?.();
132
+ } else {
133
+ dispatch(setBookPackagingEntry(true));
134
+ }
135
+ };
136
+
128
137
  return (
129
138
  <div
130
139
  className={`flyin ${isOpen ? 'flyin--active' : ''} ${className} ${isPackageEditFlow || flyInType === 'acco-results' ? 'flyin--large' : ''} ${
@@ -211,7 +220,7 @@ const FlyIn: React.FC<FlyInProps> = ({
211
220
  )}
212
221
 
213
222
  {(srpType === PortalQsmType.Accommodation || srpType === PortalQsmType.AccommodationAndFlight) && flyInType === 'acco-details' && (
214
- <AccommodationFlyIn isLoading={detailsLoading} handleConfirm={handleConfirm!} />
223
+ <AccommodationFlyIn isLoading={detailsLoading} handleConfirm={onHandleConfirm} />
215
224
  )}
216
225
 
217
226
  {srpType === PortalQsmType.AccommodationAndFlight && (flyInType === 'flight-outward-results' || flyInType === 'flight-return-results') && (
@@ -44,3 +44,14 @@ export const renderEditablePackagingEntrySummaryOptions = (editablePackagingEntr
44
44
  );
45
45
  });
46
46
  };
47
+
48
+ export const getImageSrcFromHtml = (html?: string | null): string | undefined => {
49
+ if (!html || typeof window === 'undefined') {
50
+ return undefined;
51
+ }
52
+
53
+ const doc = new DOMParser().parseFromString(html, 'text/html');
54
+ const img = doc.querySelector('img');
55
+
56
+ return img?.getAttribute('src') ?? undefined;
57
+ };
@@ -3,8 +3,8 @@ import { isNil } from 'lodash';
3
3
  import { ApiSettingsState } from '../types';
4
4
 
5
5
  export const tideConnection = {
6
- // host: 'https://localhost:44341',
7
- host: 'https://preview-tide.tidesoftware.be',
6
+ host: 'https://localhost:44341',
7
+ // host: 'https://preview-tide.tidesoftware.be',
8
8
  apiKey: 'e9b95d79-de4c-41d6-ab7e-3dd429873058',
9
9
  catalogueIds: [1],
10
10
  officeId: 1
@@ -13,6 +13,7 @@
13
13
  border-left: 2px solid var(--tide-booking-room-options-dropdown-select-icon-color);
14
14
  border-bottom: 2px solid var(--tide-booking-room-options-dropdown-select-icon-color);
15
15
  transform: rotate(-45deg);
16
+ pointer-events: none;
16
17
  }
17
18
  }
18
19
 
@@ -30,6 +31,10 @@
30
31
  border: --tide-booking-room-options-dropdown-select-border-focus;
31
32
  }
32
33
 
34
+ &:hover {
35
+ cursor: pointer;
36
+ }
37
+
33
38
  option {
34
39
  appearance: none;
35
40
  display: flex;