@qite/tide-booking-component 1.4.31 → 1.4.32

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 (68) hide show
  1. package/build/build-cjs/booking-product/components/date-range-picker/index.d.ts +1 -0
  2. package/build/build-cjs/booking-product/components/dates.d.ts +5 -0
  3. package/build/build-cjs/booking-product/components/list-view.d.ts +8 -0
  4. package/build/build-cjs/booking-product/constants.d.ts +1 -0
  5. package/build/build-cjs/booking-product/types.d.ts +2 -0
  6. package/build/build-cjs/booking-product/utils/api.d.ts +6 -1
  7. package/build/build-cjs/booking-wizard/features/booking/booking-slice.d.ts +8 -1
  8. package/build/build-cjs/booking-wizard/features/booking/selectors.d.ts +3 -0
  9. package/build/build-cjs/booking-wizard/features/sidebar/sidebar-util.d.ts +1 -1
  10. package/build/build-cjs/booking-wizard/features/sidebar/sidebar.d.ts +1 -0
  11. package/build/build-cjs/booking-wizard/features/travelers-form/travelers-form-slice.d.ts +1 -0
  12. package/build/build-cjs/booking-wizard/types.d.ts +1 -0
  13. package/build/build-cjs/index.js +5219 -1052
  14. package/build/build-cjs/search-results/store/search-results-slice.d.ts +2 -0
  15. package/build/build-cjs/search-results/types.d.ts +2 -2
  16. package/build/build-cjs/shared/utils/localization-util.d.ts +17 -2
  17. package/build/build-esm/booking-product/components/date-range-picker/index.d.ts +1 -0
  18. package/build/build-esm/booking-product/components/dates.d.ts +5 -0
  19. package/build/build-esm/booking-product/components/list-view.d.ts +8 -0
  20. package/build/build-esm/booking-product/constants.d.ts +1 -0
  21. package/build/build-esm/booking-product/types.d.ts +2 -0
  22. package/build/build-esm/booking-product/utils/api.d.ts +6 -1
  23. package/build/build-esm/booking-wizard/features/booking/booking-slice.d.ts +8 -1
  24. package/build/build-esm/booking-wizard/features/booking/selectors.d.ts +3 -0
  25. package/build/build-esm/booking-wizard/features/sidebar/sidebar-util.d.ts +1 -1
  26. package/build/build-esm/booking-wizard/features/sidebar/sidebar.d.ts +1 -0
  27. package/build/build-esm/booking-wizard/features/travelers-form/travelers-form-slice.d.ts +1 -0
  28. package/build/build-esm/booking-wizard/types.d.ts +1 -0
  29. package/build/build-esm/index.js +5211 -1045
  30. package/build/build-esm/search-results/store/search-results-slice.d.ts +2 -0
  31. package/build/build-esm/search-results/types.d.ts +2 -2
  32. package/build/build-esm/shared/utils/localization-util.d.ts +17 -2
  33. package/package.json +1 -1
  34. package/src/booking-product/components/date-range-picker/index.tsx +29 -16
  35. package/src/booking-product/components/dates.tsx +28 -5
  36. package/src/booking-product/components/list-view.tsx +54 -0
  37. package/src/booking-product/components/product.tsx +152 -20
  38. package/src/booking-product/constants.ts +1 -0
  39. package/src/booking-product/settings-context.ts +3 -1
  40. package/src/booking-product/types.ts +2 -0
  41. package/src/booking-product/utils/api.ts +9 -3
  42. package/src/search-results/components/flight/flight-card.tsx +1 -1
  43. package/src/search-results/components/hotel/hotel-accommodation-results.tsx +11 -6
  44. package/src/search-results/components/hotel/hotel-card.tsx +15 -1
  45. package/src/search-results/components/search-results-container/search-results-container.tsx +69 -29
  46. package/src/search-results/features/flights/flight-search-results-self-contained.tsx +0 -3
  47. package/src/search-results/features/hotels/hotel-search-results-self-contained.tsx +0 -3
  48. package/src/search-results/store/search-results-slice.ts +7 -1
  49. package/src/search-results/types.ts +2 -2
  50. package/src/shared/translations/ar-SA.json +249 -0
  51. package/src/shared/translations/da-DK.json +249 -0
  52. package/src/shared/translations/de-DE.json +249 -0
  53. package/src/shared/translations/en-GB.json +3 -1
  54. package/src/shared/translations/es-ES.json +249 -0
  55. package/src/shared/translations/fr-BE.json +3 -1
  56. package/src/shared/translations/fr-FR.json +249 -0
  57. package/src/shared/translations/is-IS.json +249 -0
  58. package/src/shared/translations/it-IT.json +249 -0
  59. package/src/shared/translations/ja-JP.json +249 -0
  60. package/src/shared/translations/nl-BE.json +3 -1
  61. package/src/shared/translations/nl-NL.json +249 -0
  62. package/src/shared/translations/no-NO.json +249 -0
  63. package/src/shared/translations/pl-PL.json +249 -0
  64. package/src/shared/translations/pt-PT.json +249 -0
  65. package/src/shared/translations/sv-SE.json +249 -0
  66. package/src/shared/utils/localization-util.ts +107 -12
  67. package/styles/components/_form.scss +6 -0
  68. package/styles/components/_search.scss +1 -0
@@ -2,6 +2,7 @@ import { Filter } from '../types';
2
2
  import { BookingPackageItem } from '@qite/tide-client/build/types';
3
3
  export interface SearchResultsState {
4
4
  results: BookingPackageItem[];
5
+ selectedHotelId: number | null;
5
6
  isLoading: boolean;
6
7
  filters: Filter[];
7
8
  sortKey: string | null;
@@ -14,6 +15,7 @@ export declare const setResults: import('@reduxjs/toolkit').ActionCreatorWithPay
14
15
  },
15
16
  'searchResults/setResults'
16
17
  >,
18
+ setSelectedHotel: import('@reduxjs/toolkit').ActionCreatorWithPayload<number | null, 'searchResults/setSelectedHotel'>,
17
19
  setIsLoading: import('@reduxjs/toolkit').ActionCreatorWithPayload<boolean, 'searchResults/setIsLoading'>,
18
20
  setFilters: import('@reduxjs/toolkit').ActionCreatorWithPayload<Filter[], 'searchResults/setFilters'>,
19
21
  resetFilters: import('@reduxjs/toolkit').ActionCreatorWithPayload<Filter[], 'searchResults/resetFilters'>,
@@ -18,7 +18,6 @@ export interface SearchResultsConfiguration {
18
18
  showRoundTripResults?: boolean;
19
19
  showCustomCards?: boolean;
20
20
  customCardRenderer?: (result: SearchResult) => ReactNode;
21
- onResultClick?: (id: string) => void;
22
21
  showMapView?: boolean;
23
22
  noResultsLabel?: string;
24
23
  isLoading?: boolean;
@@ -37,6 +36,7 @@ export interface SearchResultsConfiguration {
37
36
  searchResultCTA?: string;
38
37
  };
39
38
  cmsHotelData?: any[];
39
+ languageCode?: string;
40
40
  }
41
41
  export type FilterType = 'checkbox' | 'toggle' | 'slider' | 'star-rating';
42
42
  export type FilterProperty = 'regime' | 'max-duration' | 'price' | 'rating' | 'theme';
@@ -65,7 +65,7 @@ export interface PaginationConfig {
65
65
  }
66
66
  export type SearchResult = HotelResult | FlightResult | RoundTripResult;
67
67
  export interface BaseSearchResult {
68
- id: string;
68
+ id: number;
69
69
  title: string;
70
70
  image: string;
71
71
  description?: string;
@@ -77,6 +77,8 @@ export declare const getTranslations: (language: string) => {
77
77
  WHO_YOU_TRAVELING_WITH: string;
78
78
  TRAVEL_PERIOD: string;
79
79
  CLOSE: string;
80
+ NIGHTS: string;
81
+ DAYS: string;
80
82
  };
81
83
  MAIN: {
82
84
  PREPARING_BOOKING: string;
@@ -249,9 +251,22 @@ export declare const getTranslations: (language: string) => {
249
251
  };
250
252
  };
251
253
  export declare const locales: {
252
- 'nl-BE': Locale;
253
- 'fr-BE': Locale;
254
+ 'ar-SA': Locale;
255
+ 'da-DK': Locale;
256
+ 'de-DE': Locale;
254
257
  'en-GB': Locale;
258
+ 'es-ES': Locale;
259
+ 'fr-BE': Locale;
260
+ 'fr-FR': Locale;
261
+ 'is-IS': Locale;
262
+ 'it-IT': Locale;
263
+ 'nl-BE': Locale;
264
+ 'nl-NL': Locale;
265
+ 'no-NO': Locale;
266
+ 'pl-PL': Locale;
267
+ 'pt-PT': Locale;
268
+ 'sv-SE': Locale;
269
+ 'ja-JP': Locale;
255
270
  };
256
271
  export declare function getLocale(code: string): Locale;
257
272
  export declare const getPriceDifferenceText: (price: number, currencyCode: string) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qite/tide-booking-component",
3
- "version": "1.4.31",
3
+ "version": "1.4.32",
4
4
  "description": "React Booking wizard & Booking product component for Tide",
5
5
  "main": "build/build-cjs/index.js",
6
6
  "module": "build/build-esm/index.js",
@@ -1,8 +1,9 @@
1
1
  import { endOfDay, endOfMonth, getMonth, getYear, isAfter, isEqual, isWithinInterval, startOfDay } from 'date-fns';
2
2
 
3
3
  import { isNil } from 'lodash';
4
- import React, { useEffect, useState } from 'react';
4
+ import React, { useContext, useEffect, useState } from 'react';
5
5
  import Calendar from './calendar';
6
+ import SettingsContext from '../../settings-context';
6
7
 
7
8
  interface DateRangePickerProps {
8
9
  fromDate?: Date;
@@ -17,6 +18,7 @@ interface DateRangePickerProps {
17
18
  onFromDateChange?: (date?: Date) => void;
18
19
  onToDateChange?: (date?: Date) => void;
19
20
  onFocusMonthChange?: (focusMonth: { month: number; year: number }) => void;
21
+ toDateByFromDate?: (fromDate?: Date) => Date | undefined;
20
22
  }
21
23
 
22
24
  const DateRangePicker: React.FC<DateRangePickerProps> = (props) => {
@@ -29,39 +31,50 @@ const DateRangePicker: React.FC<DateRangePickerProps> = (props) => {
29
31
  }
30
32
  );
31
33
  const [isWaitingForToDate, setWaitingForToDate] = useState<boolean>(false);
34
+ const { searchType = 0 } = useContext(SettingsContext);
32
35
 
33
36
  const handleDayClick = (day: Date) => {
34
37
  const { onSelectionChange } = props;
35
38
 
36
39
  if (isWaitingForToDate && !isNil(fromDate) && isAfter(day, fromDate)) {
37
- setToDate(day);
38
- setWaitingForToDate(false);
39
-
40
- if (props.onToDateChange) {
41
- props.onToDateChange(undefined);
42
- }
40
+ if (searchType === 1) {
41
+ // Only allow selecting the exact toDate
42
+ const expectedToDate = props.toDateByFromDate?.(fromDate);
43
+ if (expectedToDate && isEqual(day, expectedToDate)) {
44
+ setToDate(day);
45
+ setWaitingForToDate(false);
46
+
47
+ props.onToDateChange?.(undefined);
48
+ onSelectionChange?.(fromDate, day);
49
+ }
50
+ } else {
51
+ // searchType === 0 (original behavior)
52
+ setToDate(day);
53
+ setWaitingForToDate(false);
43
54
 
44
- if (onSelectionChange) {
45
- onSelectionChange(fromDate, day);
55
+ props.onToDateChange?.(undefined);
56
+ onSelectionChange?.(fromDate, day);
46
57
  }
47
58
  } else {
48
59
  setFromDate(day);
49
60
 
50
- if (props.duration) {
51
- const to = new Date(Date.UTC(day.getFullYear(), day.getMonth(), day.getDate() + props.duration));
61
+ if (searchType === 1 && props.toDateByFromDate) {
62
+ const to = props.toDateByFromDate(day);
52
63
  setToDate(to);
53
64
 
54
- if (onSelectionChange) {
55
- onSelectionChange(day, to);
65
+ if (to) {
66
+ onSelectionChange?.(day, to);
56
67
  }
68
+ } else if (props.duration) {
69
+ const to = new Date(Date.UTC(day.getFullYear(), day.getMonth(), day.getDate() + props.duration));
70
+ setToDate(to);
71
+ onSelectionChange?.(day, to);
57
72
  } else {
58
73
  setToDate(undefined);
59
74
  setWaitingForToDate(true);
60
75
  }
61
76
 
62
- if (props.onFromDateChange) {
63
- props.onFromDateChange(day);
64
- }
77
+ props.onFromDateChange?.(day);
65
78
  }
66
79
  };
67
80
 
@@ -1,4 +1,4 @@
1
- import { format, isBefore, startOfDay } from 'date-fns';
1
+ import { format, isBefore, startOfDay, isSameDay } from 'date-fns';
2
2
  import React, { useContext, useEffect, useState } from 'react';
3
3
  import { usePopper } from 'react-popper';
4
4
  import { buildClassName } from '../../shared/utils/class-util';
@@ -12,9 +12,11 @@ interface DatesProps {
12
12
  value?: DateRange;
13
13
  duration?: number;
14
14
  onChange?: (value: DateRange) => void;
15
+ availableDatePairs?: { fromDate: Date; toDate: Date }[];
16
+ isLoading?: boolean;
15
17
  }
16
18
 
17
- const Dates: React.FC<DatesProps> = ({ value, duration, onChange }) => {
19
+ const Dates: React.FC<DatesProps> = ({ value, duration, onChange, availableDatePairs, isLoading }) => {
18
20
  const { language } = useContext(SettingsContext);
19
21
  const translations = getTranslations(language);
20
22
  const mql = typeof window !== 'undefined' ? window.matchMedia('(min-width: 992px)') : undefined;
@@ -23,6 +25,7 @@ const Dates: React.FC<DatesProps> = ({ value, duration, onChange }) => {
23
25
  const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
24
26
  const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
25
27
  const [panelActive, setPanelActive] = useState<boolean>(false);
28
+ const { searchType = 0 } = useContext(SettingsContext);
26
29
 
27
30
  const { styles, attributes } = usePopper(referenceElement, popperElement, {
28
31
  placement: mql?.matches ? 'top' : 'bottom',
@@ -50,6 +53,21 @@ const Dates: React.FC<DatesProps> = ({ value, duration, onChange }) => {
50
53
  if (!panelActive) setPanelActive(true);
51
54
  };
52
55
 
56
+ // Only allow selecting fromDates that are in availableDatePairs
57
+ const disabledDaysFunction = (date: Date) => {
58
+ if (!availableDatePairs || availableDatePairs.length === 0) {
59
+ return isBefore(date, startOfDay(new Date()));
60
+ }
61
+ return !availableDatePairs.some((pair) => isSameDay(pair.fromDate, date));
62
+ };
63
+
64
+ // Given a fromDate, find the corresponding toDate
65
+ const findToDateByFromDate = (fromDate?: Date) => {
66
+ if (!fromDate || !availableDatePairs) return undefined;
67
+ const found = availableDatePairs.find((pair) => isSameDay(pair.fromDate, fromDate));
68
+ return found ? found.toDate : undefined;
69
+ };
70
+
53
71
  const handleSelectionChange = (fromDate?: Date, toDate?: Date) => {
54
72
  if (onChange) {
55
73
  onChange({ fromDate, toDate });
@@ -77,7 +95,11 @@ const Dates: React.FC<DatesProps> = ({ value, duration, onChange }) => {
77
95
  {translations.PRODUCT.TRAVEL_PERIOD}
78
96
  </div>
79
97
  <div className="form__group form__group--datepicker form__group--icon">
80
- <div className="form__group-input">
98
+ <div
99
+ className={
100
+ 'form__group-input' +
101
+ ((searchType === 1 && availableDatePairs && availableDatePairs.length === 0) || isLoading ? ' form__group-input--disabled' : '')
102
+ }>
81
103
  <label className="form__label">{translations.PRODUCT.DEPARTURE}</label>
82
104
  <input
83
105
  type="text"
@@ -89,7 +111,7 @@ const Dates: React.FC<DatesProps> = ({ value, duration, onChange }) => {
89
111
  />
90
112
  </div>
91
113
 
92
- <div className="form__group-input">
114
+ <div className={'form__group-input' + (searchType === 1 || isLoading ? ' form__group-input--disabled' : '')}>
93
115
  <label className="form__label">{translations.PRODUCT.RETURN}</label>
94
116
  <input
95
117
  type="text"
@@ -113,7 +135,8 @@ const Dates: React.FC<DatesProps> = ({ value, duration, onChange }) => {
113
135
  fromDate={value?.fromDate}
114
136
  toDate={value?.toDate}
115
137
  duration={duration}
116
- disabledDaysFunction={(date) => isBefore(date, startOfDay(new Date()))}
138
+ disabledDaysFunction={disabledDaysFunction}
139
+ toDateByFromDate={findToDateByFromDate}
117
140
  onSelectionChange={handleSelectionChange}
118
141
  />
119
142
  {!mql?.matches && (
@@ -0,0 +1,54 @@
1
+ import { BookingPackageItem } from '@qite/tide-client';
2
+ import React, { useContext } from 'react';
3
+ import { getTranslations } from '../../shared/utils/localization-util';
4
+ import SettingsContext from '../settings-context';
5
+ import { differenceInCalendarDays } from 'date-fns';
6
+ import { isEmpty } from 'lodash';
7
+
8
+ interface ListViewProps {
9
+ searchResults: BookingPackageItem[];
10
+ onSelect?: (selectedItem: BookingPackageItem) => void;
11
+ }
12
+
13
+ const ListView: React.FC<ListViewProps> = ({ searchResults, onSelect }) => {
14
+ const { language } = useContext(SettingsContext);
15
+ const translations = getTranslations(language);
16
+
17
+ const formatDate = (dateString: string) => {
18
+ return new Intl.DateTimeFormat(language).format(new Date(dateString));
19
+ };
20
+
21
+ const handleOptionChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
22
+ const selectedDate = event.target.value;
23
+ const selectedItem = searchResults.find((item) => item.fromDate === selectedDate);
24
+ if (selectedItem && onSelect) {
25
+ onSelect(selectedItem);
26
+ }
27
+ };
28
+
29
+ const getNightsAndDays = (result: BookingPackageItem) => {
30
+ const nights = differenceInCalendarDays(new Date(result.toDate), new Date(result.fromDate));
31
+ return `${nights} ${translations.PRODUCT.NIGHTS}, ${nights + 1} ${translations.PRODUCT.DAYS}`;
32
+ };
33
+
34
+ const getDayLongFormat = (dateString: string) => {
35
+ const day = new Date(dateString).toLocaleDateString(language, { weekday: 'long' });
36
+ return day.charAt(0).toUpperCase() + day.slice(1);
37
+ };
38
+
39
+ return (
40
+ <div className="dropdown">
41
+ <select name="selectDate" id="selectDate" onChange={handleOptionChange}>
42
+ {!isEmpty(searchResults) &&
43
+ searchResults.map((result, index) => (
44
+ <option key={index} value={result.fromDate}>
45
+ {getDayLongFormat(result.fromDate)} {formatDate(result.fromDate)} - {getDayLongFormat(result.toDate)} {formatDate(result.toDate)} ({' '}
46
+ {getNightsAndDays(result)} ) - {result.price.toFixed(2)} {result.currencyCode}
47
+ </option>
48
+ ))}
49
+ </select>
50
+ </div>
51
+ );
52
+ };
53
+
54
+ export default ListView;
@@ -1,8 +1,15 @@
1
1
  import JsonURL from '@jsonurl/jsonurl';
2
- import { BookingPackageDetailsRequest, BookingPackagePax, BookingPackageRequest, BookingPackageRequestRoom } from '@qite/tide-client/build/types';
3
- import { addDays, addMonths, format, formatISO } from 'date-fns';
4
- import { omit } from 'lodash';
5
- import React, { useContext, useEffect, useState } from 'react';
2
+ import {
3
+ BookingPackageDetailsRequest,
4
+ BookingPackageItem,
5
+ BookingPackagePax,
6
+ BookingPackageRequest,
7
+ BookingPackageRequestRoom,
8
+ BookingPackageSearchRequest
9
+ } from '@qite/tide-client/build/types';
10
+ import { addDays, addMonths, addYears, format, formatISO } from 'date-fns';
11
+ import { isEmpty, now, omit } from 'lodash';
12
+ import React, { useContext, useEffect, useRef, useState } from 'react';
6
13
  import { ApiSettingsState } from '../../shared/types';
7
14
  import { getTranslations } from '../../shared/utils/localization-util';
8
15
  import { getDateAsDateFromParams, getRoomsFromParams } from '../../shared/utils/query-string-util';
@@ -13,7 +20,9 @@ import { formatPriceByMode } from '../utils/price';
13
20
  import Dates from './dates';
14
21
  import Footer from './footer';
15
22
  import Header from './header';
23
+ import ListView from './list-view';
16
24
  import Rooms from './rooms';
25
+ import { DATE_FORMAT } from '../constants';
17
26
 
18
27
  interface ProductProps {
19
28
  productCode: string;
@@ -23,8 +32,21 @@ interface ProductProps {
23
32
  }
24
33
 
25
34
  const Product: React.FC<ProductProps> = ({ productCode, productName, duration, rating }) => {
26
- const { apiKey, apiUrl, officeId, agentId, catalogueId, includeFlights, language, basePath, priceMode, addProductToQuery, isOffer } =
27
- useContext(SettingsContext);
35
+ const {
36
+ apiKey,
37
+ apiUrl,
38
+ officeId,
39
+ agentId,
40
+ catalogueId,
41
+ includeFlights,
42
+ language,
43
+ basePath,
44
+ priceMode,
45
+ addProductToQuery,
46
+ isOffer,
47
+ displayMode = 'calendar',
48
+ searchType = 0
49
+ } = useContext(SettingsContext);
28
50
  const translations = getTranslations(language);
29
51
 
30
52
  const [loaded, setLoaded] = useState<boolean>(false);
@@ -34,11 +56,19 @@ const Product: React.FC<ProductProps> = ({ productCode, productName, duration, r
34
56
  const [hasFlight, setHasFlight] = useState<boolean>(false);
35
57
  const [hasTransfer, setHasTransfer] = useState<boolean>(false);
36
58
  const [rooms, setRooms] = useState<ProductRoom[]>([{ adults: 2, children: 0, childAges: [] }]);
59
+ const [searchResponse, setSearchResponse] = useState<BookingPackageItem[]>([]);
37
60
  const [dateRange, setDateRange] = useState<DateRange>();
38
61
  const [packageProductName, setPackageProductName] = useState<string>(productName);
39
62
  const [currencyCode, setCurrencyCode] = useState<string>('');
63
+ const [searchResults, setSearchResults] = useState<BookingPackageItem[]>([]);
64
+ const skipNextFetchRef = useRef(false);
40
65
 
41
66
  const fetchPackage = async (signal: AbortSignal) => {
67
+ if (displayMode === 'list' && searchType !== 1) {
68
+ console.error('The combination of searchType and displayMode is not supported.' + " Please set searchType to 1 when using displayMode 'list'.");
69
+ return;
70
+ }
71
+
42
72
  if (loaded && productCode && dateRange?.fromDate && dateRange?.toDate && rooms) {
43
73
  const apiSettingsState =
44
74
  apiKey && apiUrl
@@ -72,7 +102,7 @@ const Product: React.FC<ProductProps> = ({ productCode, productName, duration, r
72
102
  return requestRoom;
73
103
  });
74
104
 
75
- const request = {
105
+ const detailsRequest: BookingPackageRequest<BookingPackageDetailsRequest> = {
76
106
  officeId: officeId,
77
107
  agentId: agentId,
78
108
  payload: {
@@ -83,13 +113,40 @@ const Product: React.FC<ProductProps> = ({ productCode, productName, duration, r
83
113
  toDate: endDate,
84
114
  includeFlights: includeFlights,
85
115
  rooms: requestRooms
86
- } as BookingPackageDetailsRequest
87
- } as BookingPackageRequest<BookingPackageDetailsRequest>;
116
+ }
117
+ };
118
+
119
+ const searchRequest: BookingPackageRequest<BookingPackageSearchRequest> = {
120
+ officeId: officeId,
121
+ agentId: agentId,
122
+ payload: {
123
+ searchType: 1,
124
+ useExactDates: false,
125
+ earliestFromOffset: 0,
126
+ latestToOffset: 0,
127
+ catalogueIds: [catalogueId],
128
+ productCodes: [productCode],
129
+ fromDate: new Date(now()).toISOString(),
130
+ toDate: addYears(new Date(now()), 1).toISOString(),
131
+ includeFlights: includeFlights,
132
+ rooms: requestRooms
133
+ }
134
+ };
88
135
 
89
136
  setIsLoading(true);
90
- const response = await packageApi.fetchDetails(request, signal, language, apiSettingsState);
91
- if (!response.errorCode && response.payload) {
92
- const selectedOption = response.payload.options.find((x) => x.isSelected);
137
+
138
+ let detailResponse;
139
+ if (displayMode === 'calendar' && searchType === 0) {
140
+ detailResponse = await packageApi.fetchDetails(detailsRequest, signal, language, apiSettingsState);
141
+ }
142
+
143
+ if (searchType === 1) {
144
+ setSearchResponse(await packageApi.fetchSearch(searchRequest, signal, apiSettingsState));
145
+ }
146
+
147
+ if (detailResponse && !detailResponse.errorCode && detailResponse.payload) {
148
+ const selectedOption = detailResponse.payload.options.find((x) => x.isSelected);
149
+
93
150
  if (selectedOption) {
94
151
  const hasFlight = selectedOption.includedServiceTypes.some((x) => x === 7);
95
152
  const hasTranfer = selectedOption.includedServiceTypes.some((x) => x === 13);
@@ -97,11 +154,8 @@ const Product: React.FC<ProductProps> = ({ productCode, productName, duration, r
97
154
  setPrice(selectedOption.price);
98
155
  setHasFlight(hasFlight);
99
156
  setHasTransfer(hasTranfer);
100
- setCurrencyCode(response.payload.currencyCode);
101
-
102
- if (!productName) {
103
- setPackageProductName(selectedOption.name);
104
- }
157
+ setCurrencyCode(detailResponse.payload.currencyCode);
158
+ setPackageProductName(selectedOption.name);
105
159
  }
106
160
  } else {
107
161
  setPrice(undefined);
@@ -129,11 +183,11 @@ const Product: React.FC<ProductProps> = ({ productCode, productName, duration, r
129
183
  }
130
184
 
131
185
  if (dateRange?.fromDate) {
132
- params['startDate'] = format(dateRange.fromDate, 'yyyy-MM-dd');
186
+ params['startDate'] = format(dateRange.fromDate, DATE_FORMAT);
133
187
  }
134
188
 
135
189
  if (dateRange?.toDate) {
136
- params['endDate'] = format(dateRange.toDate, 'yyyy-MM-dd');
190
+ params['endDate'] = format(dateRange.toDate, DATE_FORMAT);
137
191
  }
138
192
 
139
193
  params['catalogueId'] = catalogueId.toString();
@@ -158,10 +212,67 @@ const Product: React.FC<ProductProps> = ({ productCode, productName, duration, r
158
212
  };
159
213
 
160
214
  const handleDateChange = (value: DateRange) => {
215
+ if (searchType === 1) {
216
+ skipNextFetchRef.current = true;
217
+ }
161
218
  setDateRange(value);
219
+
220
+ if (value.fromDate && value.toDate && searchType === 1 && searchResults.length > 0) {
221
+ const from = format(value.fromDate, DATE_FORMAT);
222
+ const to = format(value.toDate, DATE_FORMAT);
223
+
224
+ const selectedItem = searchResults.find((item) => {
225
+ const itemFrom = format(new Date(item.fromDate), DATE_FORMAT);
226
+ const itemTo = format(new Date(item.toDate), DATE_FORMAT);
227
+
228
+ return itemFrom === from && itemTo === to;
229
+ });
230
+
231
+ if (selectedItem) {
232
+ setPrice(selectedItem.price);
233
+ setPackageProductName(selectedItem.name);
234
+ }
235
+ }
236
+ };
237
+
238
+ const handleDateSelect = (selectedItem: BookingPackageItem) => {
239
+ if (searchType === 1) {
240
+ skipNextFetchRef.current = true;
241
+ }
242
+ const fromDate = new Date(selectedItem.fromDate);
243
+ const toDate = new Date(selectedItem.toDate);
244
+
245
+ setDateRange({ fromDate, toDate });
246
+ setPrice(selectedItem.price);
247
+ setPackageProductName(selectedItem.name);
162
248
  };
163
249
 
164
250
  useEffect(() => {
251
+ if (searchResponse && !isEmpty(searchResponse)) {
252
+ setSearchResults(searchResponse);
253
+ const selectedItem = searchResponse.find(
254
+ (item) => new Date(item.fromDate).getTime() === dateRange?.fromDate?.getTime() && new Date(item.toDate).getTime() === dateRange?.toDate?.getTime()
255
+ );
256
+ if (selectedItem) {
257
+ setPrice(selectedItem.price);
258
+ setPackageProductName(selectedItem.name);
259
+ } else {
260
+ setPrice(searchResponse[0].price);
261
+ setPackageProductName(searchResponse[0].name);
262
+ }
263
+ } else {
264
+ setSearchResults([]);
265
+ setIsLoading(false);
266
+ setPackageProductName(translations.PRODUCT.NOT_AVAILABLE);
267
+ }
268
+ }, [searchResponse]);
269
+
270
+ useEffect(() => {
271
+ if (searchType === 1 && skipNextFetchRef.current) {
272
+ skipNextFetchRef.current = false;
273
+ return;
274
+ }
275
+
165
276
  const controller = new AbortController();
166
277
  const { signal } = controller;
167
278
 
@@ -207,6 +318,15 @@ const Product: React.FC<ProductProps> = ({ productCode, productName, duration, r
207
318
  setLoaded(true);
208
319
  }, [location]);
209
320
 
321
+ useEffect(() => {
322
+ if (searchType === 1 && searchResults.length > 0 && !dateRange) {
323
+ const first = searchResults[0];
324
+ setDateRange({ fromDate: new Date(first.fromDate), toDate: new Date(first.toDate) });
325
+ setPackageProductName(first.name);
326
+ setPrice(first.price);
327
+ }
328
+ }, [searchResults]);
329
+
210
330
  const personCount = rooms.reduce((s, r) => s + r.adults + r.children, 0);
211
331
  const durationCount = 0;
212
332
  const priceText = formatPriceByMode(
@@ -225,7 +345,19 @@ const Product: React.FC<ProductProps> = ({ productCode, productName, duration, r
225
345
  <Header name={packageProductName} rating={rating} priceText={priceText} isLoading={isLoading} hasFlight={hasFlight} hasTransfer={hasTransfer} />
226
346
  <div className="booking-product__body">
227
347
  <Rooms rooms={rooms} isDisabled={roomsIsDisabled} setIsDisabled={setRoomsIsDisabled} onChange={handleRoomChange} />
228
- <Dates value={dateRange} duration={duration} onChange={handleDateChange} />
348
+ {displayMode === 'calendar' && (
349
+ <Dates
350
+ value={dateRange}
351
+ duration={duration}
352
+ isLoading={isLoading}
353
+ onChange={handleDateChange}
354
+ availableDatePairs={searchResults.map((item) => ({
355
+ fromDate: new Date(item.fromDate),
356
+ toDate: new Date(item.toDate)
357
+ }))}
358
+ />
359
+ )}
360
+ {displayMode === 'list' && <ListView searchResults={searchResults} onSelect={handleDateSelect} />}
229
361
  </div>
230
362
  <Footer priceText={priceText} isLoading={isLoading} isOffer={isOffer} roomsIsDisabled={roomsIsDisabled} handleBookClick={handleBookClick} />
231
363
  </div>
@@ -0,0 +1 @@
1
+ export const DATE_FORMAT = 'yyyy-MM-dd';
@@ -8,7 +8,9 @@ const SettingsContext = React.createContext<ProductSettingsContextProps>({
8
8
  catalogueId: 1,
9
9
  language: 'nl-BE',
10
10
  basePath: 'boeken',
11
- priceMode: 0
11
+ priceMode: 0,
12
+ displayMode: 'calendar',
13
+ searchType: 0
12
14
  });
13
15
 
14
16
  export default SettingsContext;
@@ -11,6 +11,8 @@ export interface Settings {
11
11
  apiKey?: string;
12
12
  addProductToQuery?: boolean;
13
13
  isOffer?: boolean;
14
+ displayMode?: 'list' | 'calendar';
15
+ searchType?: number;
14
16
 
15
17
  alternativeActionText?: string;
16
18
  alternativeAction?: () => void;
@@ -1,5 +1,5 @@
1
- import { details } from '@qite/tide-client';
2
- import { BookingPackage, BookingPackageDetailsRequest, BookingPackageRequest, TideResponse } from '@qite/tide-client/build/types';
1
+ import { details, search } from '@qite/tide-client';
2
+ import { BookingPackage, BookingPackageDetailsRequest, BookingPackageRequest, BookingPackageSearchRequest, TideResponse } from '@qite/tide-client/build/types';
3
3
  import { ApiSettingsState } from '../../shared/types';
4
4
  import { buildTideClientConfig } from '../../shared/utils/tide-api-utils';
5
5
 
@@ -13,8 +13,14 @@ const fetchDetails = async (
13
13
  return await details(tideClientConfig, request, signal, languageCode);
14
14
  };
15
15
 
16
+ const fetchSearch = async (request: BookingPackageRequest<BookingPackageSearchRequest>, signal: AbortSignal, apiSettings?: ApiSettingsState) => {
17
+ const tideClientConfig = buildTideClientConfig(apiSettings);
18
+ return await search(tideClientConfig, request, signal);
19
+ };
20
+
16
21
  const packageApi = {
17
- fetchDetails
22
+ fetchDetails,
23
+ fetchSearch
18
24
  };
19
25
 
20
26
  export default packageApi;
@@ -26,7 +26,7 @@ const FlightCard: React.FC<FlightCardProps> = ({ result }) => {
26
26
  <p className="search__price-card-price">{result.price}</p>
27
27
  </div>
28
28
  <div>
29
- <button className="search__price-card-button" onClick={() => context?.onResultClick?.(result.id)}>
29
+ <button className="search__price-card-button" onClick={() => console.log('clicked result with ID:', result.id)}>
30
30
  {result.ctaText}
31
31
  </button>
32
32
  </div>
@@ -15,7 +15,7 @@ interface HotelAccommodationResultsProps {
15
15
  const renderResults = (results: BookingPackageItem[], context: SearchResultsConfiguration, cmsMap: Map<any, any>, activeTab: string | null) => {
16
16
  const renderedResults = results.map((result, index) => {
17
17
  const cmsItem = cmsMap.get(result.productId);
18
- const mappedResult: HotelResult = mapSearchResult(result, cmsItem);
18
+ const mappedResult: HotelResult = mapSearchResult(result, cmsItem, context.languageCode);
19
19
  if (context?.showCustomCards && context?.customCardRenderer) {
20
20
  return (
21
21
  <div key={`${mappedResult.id}-${index}`} className="search__result-card">
@@ -29,16 +29,16 @@ const renderResults = (results: BookingPackageItem[], context: SearchResultsConf
29
29
  return <div className={`search__results__cards ${activeTab ? `search__results__cards--${activeTab}` : ''}`}>{renderedResults}</div>;
30
30
  };
31
31
 
32
- const mapSearchResult = (searchResult: BookingPackageItem, cmsItem: any): HotelResult => {
32
+ const mapSearchResult = (searchResult: BookingPackageItem, cmsItem: any, languageCode?: string): HotelResult => {
33
33
  return {
34
34
  type: 'hotel',
35
- id: searchResult.code,
35
+ id: searchResult.productId,
36
36
  title: cmsItem?.content?.general?.title || searchResult.name,
37
37
  image: cmsItem?.content?.images?.thumbnailPicture?.url,
38
38
  description: cmsItem?.content?.descriptions?.introductionTitle || '',
39
39
  location:
40
40
  searchResult.locationName && searchResult.countryName ? `${searchResult.locationName}, ${searchResult.countryName}` : cmsItem?.parentItem?.name || '',
41
- price: formatPrice(searchResult.price),
41
+ price: formatPrice(searchResult.currencyCode, searchResult.price, languageCode),
42
42
  ctaText: 'View details',
43
43
  days: calculateNights(searchResult.stayFromDate, searchResult.stayToDate),
44
44
  accommodation: searchResult.accommodationName,
@@ -47,11 +47,16 @@ const mapSearchResult = (searchResult: BookingPackageItem, cmsItem: any): HotelR
47
47
  };
48
48
  };
49
49
 
50
- const formatPrice = (price: number) => {
50
+ const formatPrice = (currencyCode: string, price: number, languageCode?: string) => {
51
51
  if (!price) {
52
52
  return 'Price unavailable';
53
53
  }
54
- return `$${Math.round(price).toLocaleString()}`;
54
+
55
+ return new Intl.NumberFormat(languageCode ?? 'en-GB', {
56
+ style: 'currency',
57
+ currency: currencyCode,
58
+ currencyDisplay: 'symbol'
59
+ }).format(price);
55
60
  };
56
61
 
57
62
  const calculateNights = (fromDate: Date, toDate: Date): string => {