@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.
- package/build/build-cjs/booking-product/components/date-range-picker/index.d.ts +1 -0
- package/build/build-cjs/booking-product/components/dates.d.ts +5 -0
- package/build/build-cjs/booking-product/components/list-view.d.ts +8 -0
- package/build/build-cjs/booking-product/constants.d.ts +1 -0
- package/build/build-cjs/booking-product/types.d.ts +2 -0
- package/build/build-cjs/booking-product/utils/api.d.ts +6 -1
- package/build/build-cjs/booking-wizard/features/booking/booking-slice.d.ts +8 -1
- package/build/build-cjs/booking-wizard/features/booking/selectors.d.ts +3 -0
- package/build/build-cjs/booking-wizard/features/sidebar/sidebar-util.d.ts +1 -1
- package/build/build-cjs/booking-wizard/features/sidebar/sidebar.d.ts +1 -0
- package/build/build-cjs/booking-wizard/features/travelers-form/travelers-form-slice.d.ts +1 -0
- package/build/build-cjs/booking-wizard/types.d.ts +1 -0
- package/build/build-cjs/index.js +5219 -1052
- package/build/build-cjs/search-results/store/search-results-slice.d.ts +2 -0
- package/build/build-cjs/search-results/types.d.ts +2 -2
- package/build/build-cjs/shared/utils/localization-util.d.ts +17 -2
- package/build/build-esm/booking-product/components/date-range-picker/index.d.ts +1 -0
- package/build/build-esm/booking-product/components/dates.d.ts +5 -0
- package/build/build-esm/booking-product/components/list-view.d.ts +8 -0
- package/build/build-esm/booking-product/constants.d.ts +1 -0
- package/build/build-esm/booking-product/types.d.ts +2 -0
- package/build/build-esm/booking-product/utils/api.d.ts +6 -1
- package/build/build-esm/booking-wizard/features/booking/booking-slice.d.ts +8 -1
- package/build/build-esm/booking-wizard/features/booking/selectors.d.ts +3 -0
- package/build/build-esm/booking-wizard/features/sidebar/sidebar-util.d.ts +1 -1
- package/build/build-esm/booking-wizard/features/sidebar/sidebar.d.ts +1 -0
- package/build/build-esm/booking-wizard/features/travelers-form/travelers-form-slice.d.ts +1 -0
- package/build/build-esm/booking-wizard/types.d.ts +1 -0
- package/build/build-esm/index.js +5211 -1045
- package/build/build-esm/search-results/store/search-results-slice.d.ts +2 -0
- package/build/build-esm/search-results/types.d.ts +2 -2
- package/build/build-esm/shared/utils/localization-util.d.ts +17 -2
- package/package.json +1 -1
- package/src/booking-product/components/date-range-picker/index.tsx +29 -16
- package/src/booking-product/components/dates.tsx +28 -5
- package/src/booking-product/components/list-view.tsx +54 -0
- package/src/booking-product/components/product.tsx +152 -20
- package/src/booking-product/constants.ts +1 -0
- package/src/booking-product/settings-context.ts +3 -1
- package/src/booking-product/types.ts +2 -0
- package/src/booking-product/utils/api.ts +9 -3
- package/src/search-results/components/flight/flight-card.tsx +1 -1
- package/src/search-results/components/hotel/hotel-accommodation-results.tsx +11 -6
- package/src/search-results/components/hotel/hotel-card.tsx +15 -1
- package/src/search-results/components/search-results-container/search-results-container.tsx +69 -29
- package/src/search-results/features/flights/flight-search-results-self-contained.tsx +0 -3
- package/src/search-results/features/hotels/hotel-search-results-self-contained.tsx +0 -3
- package/src/search-results/store/search-results-slice.ts +7 -1
- package/src/search-results/types.ts +2 -2
- package/src/shared/translations/ar-SA.json +249 -0
- package/src/shared/translations/da-DK.json +249 -0
- package/src/shared/translations/de-DE.json +249 -0
- package/src/shared/translations/en-GB.json +3 -1
- package/src/shared/translations/es-ES.json +249 -0
- package/src/shared/translations/fr-BE.json +3 -1
- package/src/shared/translations/fr-FR.json +249 -0
- package/src/shared/translations/is-IS.json +249 -0
- package/src/shared/translations/it-IT.json +249 -0
- package/src/shared/translations/ja-JP.json +249 -0
- package/src/shared/translations/nl-BE.json +3 -1
- package/src/shared/translations/nl-NL.json +249 -0
- package/src/shared/translations/no-NO.json +249 -0
- package/src/shared/translations/pl-PL.json +249 -0
- package/src/shared/translations/pt-PT.json +249 -0
- package/src/shared/translations/sv-SE.json +249 -0
- package/src/shared/utils/localization-util.ts +107 -12
- package/styles/components/_form.scss +6 -0
- 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:
|
|
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
|
-
'
|
|
253
|
-
'
|
|
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,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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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.
|
|
51
|
-
const to =
|
|
61
|
+
if (searchType === 1 && props.toDateByFromDate) {
|
|
62
|
+
const to = props.toDateByFromDate(day);
|
|
52
63
|
setToDate(to);
|
|
53
64
|
|
|
54
|
-
if (
|
|
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
|
-
|
|
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
|
|
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=
|
|
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={
|
|
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 {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 {
|
|
27
|
-
|
|
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
|
|
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
|
-
}
|
|
87
|
-
}
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(
|
|
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,
|
|
186
|
+
params['startDate'] = format(dateRange.fromDate, DATE_FORMAT);
|
|
133
187
|
}
|
|
134
188
|
|
|
135
189
|
if (dateRange?.toDate) {
|
|
136
|
-
params['endDate'] = format(dateRange.toDate,
|
|
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
|
-
|
|
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';
|
|
@@ -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={() =>
|
|
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.
|
|
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
|
-
|
|
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 => {
|