@qite/tide-booking-component 1.4.29 → 1.4.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/build-cjs/index.js +621 -2822
- package/build/build-cjs/search-results/store/search-results-slice.d.ts +6 -3
- package/build/build-cjs/search-results/types.d.ts +9 -12
- package/build/build-esm/index.js +617 -2625
- package/build/build-esm/search-results/store/search-results-slice.d.ts +6 -3
- package/build/build-esm/search-results/types.d.ts +9 -12
- package/package.json +2 -3
- package/src/booking-product/components/dates.tsx +9 -4
- package/src/booking-wizard/components/step-indicator.tsx +11 -2
- package/src/booking-wizard/features/booking/booking-slice.ts +27 -2
- package/src/booking-wizard/features/booking/booking.tsx +32 -15
- package/src/booking-wizard/features/booking/selectors.ts +6 -0
- package/src/booking-wizard/features/flight-options/index.tsx +27 -3
- package/src/booking-wizard/features/product-options/option-room.tsx +1 -1
- package/src/booking-wizard/features/product-options/options-form.tsx +14 -4
- package/src/booking-wizard/features/room-options/room.tsx +1 -1
- package/src/booking-wizard/features/sidebar/index.tsx +4 -1
- package/src/booking-wizard/features/sidebar/sidebar-util.ts +1 -3
- package/src/booking-wizard/features/sidebar/sidebar.tsx +112 -104
- package/src/booking-wizard/features/travelers-form/travelers-form-slice.ts +1 -0
- package/src/booking-wizard/features/travelers-form/travelers-form.tsx +146 -10
- package/src/booking-wizard/settings-context.ts +2 -1
- package/src/booking-wizard/types.ts +1 -0
- package/src/qsm/components/search-input-group/index.tsx +17 -8
- package/src/search-results/components/hotel/hotel-accommodation-results.tsx +78 -83
- package/src/search-results/components/hotel/hotel-card.tsx +54 -24
- package/src/search-results/components/icon.tsx +13 -0
- package/src/search-results/components/item-picker/index.tsx +5 -7
- package/src/search-results/components/search-results-container/search-results-container.tsx +72 -117
- package/src/search-results/components/tab-views/index.tsx +22 -3
- package/src/search-results/features/flights/flight-search-results-self-contained.tsx +0 -13
- package/src/search-results/features/hotels/hotel-flight-search-results-self-contained.tsx +1 -8
- package/src/search-results/features/hotels/hotel-search-results-self-contained.tsx +1 -14
- package/src/search-results/features/roundtrips/roundtrip-search-results-self-contained.tsx +1 -9
- package/src/search-results/store/search-results-slice.ts +11 -4
- package/src/search-results/types.ts +11 -16
- package/src/shared/translations/en-GB.json +5 -1
- package/src/shared/translations/fr-BE.json +5 -1
- package/src/shared/translations/nl-BE.json +5 -1
- package/styles/components/_form.scss +51 -2
- package/styles/components/_passenger-picker.scss +3 -2
- package/styles/components/_qsm.scss +1 -1
- package/styles/qsm/_qsm.scss +67 -6
|
@@ -2,111 +2,106 @@ import React from 'react';
|
|
|
2
2
|
import { HotelResult, SearchResultsConfiguration } from '../../types';
|
|
3
3
|
import Spinner from '../spinner/spinner';
|
|
4
4
|
import HotelCard from './hotel-card';
|
|
5
|
+
import { useSelector } from 'react-redux';
|
|
6
|
+
import { SearchResultsRootState } from '../../store/search-results-store';
|
|
7
|
+
import { BookingPackageItem } from '@qite/tide-client/build/types';
|
|
8
|
+
import { format, parseISO } from 'date-fns';
|
|
5
9
|
|
|
6
10
|
interface HotelAccommodationResultsProps {
|
|
7
11
|
isLoading: boolean;
|
|
8
12
|
context: SearchResultsConfiguration;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
const
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
title: 'HTFSSOFTROCK',
|
|
31
|
-
image: 'https://images.unsplash.com/photo-1573790387438-4da905039392?q=80&w=1925&auto=format&fit=crop',
|
|
32
|
-
description: '3 persoons kamer',
|
|
33
|
-
location: 'Tenerif, Spanje',
|
|
34
|
-
price: '$2244',
|
|
35
|
-
ctaText: 'Bekijk details',
|
|
36
|
-
days: '7 nights',
|
|
37
|
-
flightInfo: null,
|
|
38
|
-
accommodation: null,
|
|
39
|
-
regime: null,
|
|
40
|
-
stars: 5
|
|
41
|
-
} as HotelResult,
|
|
42
|
-
{
|
|
43
|
-
type: 'hotel',
|
|
44
|
-
id: 'HTFSROYGAR',
|
|
45
|
-
title: 'HTFSROYGAR',
|
|
46
|
-
image: 'https://images.unsplash.com/photo-1573790387438-4da905039392?q=80&w=1925&auto=format&fit=crop',
|
|
47
|
-
description: '4 persoons kamer',
|
|
48
|
-
location: 'Tenerif, Spanje',
|
|
49
|
-
price: '$2496',
|
|
50
|
-
ctaText: 'Bekijk details',
|
|
51
|
-
days: '7 nights',
|
|
52
|
-
flightInfo: null,
|
|
53
|
-
accommodation: null,
|
|
54
|
-
regime: null,
|
|
55
|
-
stars: 5
|
|
56
|
-
} as HotelResult,
|
|
57
|
-
{
|
|
15
|
+
const renderResults = (results: BookingPackageItem[], context: SearchResultsConfiguration, cmsMap: Map<any, any>, activeTab: string | null) => {
|
|
16
|
+
const renderedResults = results.map((result, index) => {
|
|
17
|
+
const cmsItem = cmsMap.get(result.productId);
|
|
18
|
+
const mappedResult: HotelResult = mapSearchResult(result, cmsItem);
|
|
19
|
+
if (context?.showCustomCards && context?.customCardRenderer) {
|
|
20
|
+
return (
|
|
21
|
+
<div key={`${mappedResult.id}-${index}`} className="search__result-card">
|
|
22
|
+
{context.customCardRenderer(mappedResult)}
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return <HotelCard key={`${mappedResult.id}-${index}`} result={mappedResult} />;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return <div className={`search__results__cards ${activeTab ? `search__results__cards--${activeTab}` : ''}`}>{renderedResults}</div>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const mapSearchResult = (searchResult: BookingPackageItem, cmsItem: any): HotelResult => {
|
|
33
|
+
return {
|
|
58
34
|
type: 'hotel',
|
|
59
|
-
id:
|
|
60
|
-
title:
|
|
61
|
-
image:
|
|
62
|
-
description:
|
|
63
|
-
location:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
accommodation:
|
|
69
|
-
regime:
|
|
70
|
-
stars:
|
|
71
|
-
}
|
|
72
|
-
|
|
35
|
+
id: searchResult.code,
|
|
36
|
+
title: cmsItem?.content?.general?.title || searchResult.name,
|
|
37
|
+
image: cmsItem?.content?.images?.thumbnailPicture?.url,
|
|
38
|
+
description: cmsItem?.content?.descriptions?.introductionTitle || '',
|
|
39
|
+
location:
|
|
40
|
+
searchResult.locationName && searchResult.countryName ? `${searchResult.locationName}, ${searchResult.countryName}` : cmsItem?.parentItem?.name || '',
|
|
41
|
+
price: formatPrice(searchResult.price),
|
|
42
|
+
ctaText: 'View details',
|
|
43
|
+
days: calculateNights(searchResult.stayFromDate, searchResult.stayToDate),
|
|
44
|
+
accommodation: searchResult.accommodationName,
|
|
45
|
+
regime: searchResult.regimeName,
|
|
46
|
+
stars: cmsItem?.content?.general?.stars || searchResult.hotelStars
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const formatPrice = (price: number) => {
|
|
51
|
+
if (!price) {
|
|
52
|
+
return 'Price unavailable';
|
|
53
|
+
}
|
|
54
|
+
return `$${Math.round(price).toLocaleString()}`;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const calculateNights = (fromDate: Date, toDate: Date): string => {
|
|
58
|
+
const from = new Date(fromDate).getTime(); // returns a number
|
|
59
|
+
const to = new Date(toDate).getTime(); // returns a number
|
|
60
|
+
const diffTime = Math.abs(to - from);
|
|
61
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
62
|
+
return `${diffDays} nights`;
|
|
63
|
+
};
|
|
73
64
|
|
|
74
65
|
const HotelAccommodationResults: React.FC<HotelAccommodationResultsProps> = ({ isLoading, context }) => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
66
|
+
if (isLoading) {
|
|
67
|
+
return <>{context?.customSpinner ?? <Spinner />}</>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { results, activeTab } = useSelector((state: SearchResultsRootState) => state.searchResults);
|
|
71
|
+
|
|
72
|
+
if (!results.length) {
|
|
73
|
+
return <div className="no-results">{context.noResultsLabel || 'No results found.'}</div>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const cmsMap = React.useMemo(() => {
|
|
77
|
+
const map = new Map();
|
|
78
|
+
context.cmsHotelData?.forEach((item) => {
|
|
79
|
+
const id = item?.content?.general?.product?.tideId;
|
|
80
|
+
if (id) map.set(id, item);
|
|
85
81
|
});
|
|
82
|
+
return map;
|
|
83
|
+
}, [context.cmsHotelData]);
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
const firstResult = results?.[0];
|
|
86
|
+
|
|
87
|
+
const firstResultDay = firstResult?.fromDate ? format(parseISO(firstResult.fromDate), 'd') : null;
|
|
88
|
+
|
|
89
|
+
const firstResultMonth = firstResult?.fromDate ? format(parseISO(firstResult.fromDate), 'MMM') : null;
|
|
89
90
|
|
|
90
91
|
return (
|
|
91
92
|
<>
|
|
92
93
|
<div className="search__results__label search__results__label--secondary">
|
|
93
94
|
<div className="search__results__label__date">
|
|
94
|
-
<p className="search__results__label__date-date">
|
|
95
|
-
<p>
|
|
95
|
+
<p className="search__results__label__date-date">{firstResultDay}</p>
|
|
96
|
+
<p>{firstResultMonth}</p>
|
|
96
97
|
</div>
|
|
97
98
|
<div className="search__results__label__text">
|
|
98
99
|
<h3>
|
|
99
|
-
Select <strong>
|
|
100
|
+
Select <strong>Accommodation</strong>
|
|
100
101
|
</h3>
|
|
101
102
|
</div>
|
|
102
103
|
</div>
|
|
103
|
-
{
|
|
104
|
-
context.customSpinner ?? <Spinner />
|
|
105
|
-
) : mockedHotelResults.length === 0 ? (
|
|
106
|
-
<div className="no-results">{context.noResultsLabel || 'No results found.'}</div>
|
|
107
|
-
) : (
|
|
108
|
-
renderResults(mockedHotelResults)
|
|
109
|
-
)}
|
|
104
|
+
{renderResults(results, context, cmsMap, activeTab)}
|
|
110
105
|
</>
|
|
111
106
|
);
|
|
112
107
|
};
|
|
@@ -1,38 +1,68 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import { HotelResult } from '../../types';
|
|
3
|
-
import
|
|
3
|
+
import Icon from '../icon';
|
|
4
4
|
|
|
5
5
|
interface HotelCardProps {
|
|
6
6
|
result: HotelResult;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
const HotelCard: React.FC<HotelCardProps> = ({ result }) => {
|
|
10
|
-
const context = useContext(SearchResultsConfigurationContext);
|
|
11
10
|
return (
|
|
12
|
-
<div
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
<div
|
|
12
|
+
key={result.id}
|
|
13
|
+
className="search__result-card__wrapper"
|
|
14
|
+
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.02)')}
|
|
15
|
+
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}>
|
|
16
|
+
<div className="search__result-card__img-wrapper">
|
|
17
|
+
<img src={result.image} alt={result.title} className="search__result-card__img" />
|
|
18
|
+
<div className="search__result-card__price__wrapper">
|
|
19
|
+
<span className="search__result-card__price__label">Total price</span>
|
|
20
|
+
<span className="search__result-card__price">{result.price}</span>
|
|
16
21
|
</div>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
</div>
|
|
23
|
+
<div className="search__result-card__content">
|
|
24
|
+
<div className="search__result-card__content__wrapper">
|
|
25
|
+
<div className="search__result-card__header">
|
|
26
|
+
<div className="search__result-card__header__wrapper">
|
|
27
|
+
{result.stars && (
|
|
28
|
+
<div className="rating">
|
|
29
|
+
{[...Array(result.stars)].map((_, index) => (
|
|
30
|
+
<Icon name="ui-star" key={`rating-star-${index + 1}`} width={14} height={14} />
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
)}
|
|
34
|
+
<h3 className="search__result-card__title">{result.title}</h3>
|
|
26
35
|
</div>
|
|
27
|
-
)}
|
|
28
|
-
<h3 className="search__result-card__title">{result.title}</h3>
|
|
29
|
-
{result.description && <p className="search__result-card__description">{result.description}</p>}
|
|
30
|
-
<div className="search__result-card__footer">
|
|
31
|
-
<span className="search__result-card__price">{result.price}</span>
|
|
32
|
-
<button className="search__result-card__button" onClick={() => context?.onResultClick?.(result.id)}>
|
|
33
|
-
{result.ctaText}
|
|
34
|
-
</button>
|
|
35
36
|
</div>
|
|
37
|
+
<a className="search__result-card__location">
|
|
38
|
+
<Icon name="ui-location" height={16} />
|
|
39
|
+
{result.location}
|
|
40
|
+
{/* - Show on map > 1.1 km from Center */}
|
|
41
|
+
</a>
|
|
42
|
+
<div className="search__result-card__options">
|
|
43
|
+
{/* <div className="search__result-card__option">
|
|
44
|
+
<Icon name="ui-wifi" height={16} />
|
|
45
|
+
Free Wi-Fi
|
|
46
|
+
</div> */}
|
|
47
|
+
{result.accommodation && (
|
|
48
|
+
<div className="search__result-card__option">
|
|
49
|
+
<Icon name="ui-bed" height={16} />
|
|
50
|
+
{result.accommodation}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
{result.regime && (
|
|
54
|
+
<div className="search__result-card__option">
|
|
55
|
+
<Icon name="ui-utensils" height={16} />
|
|
56
|
+
{result.regime}
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
<p className="search__result-card__description">{result.description}</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="search__result-card__footer">
|
|
63
|
+
<button type="button" className="cta cta--select" onClick={() => console.log('Clicked on customCard with id:', result.id)}>
|
|
64
|
+
{result.ctaText}
|
|
65
|
+
</button>
|
|
36
66
|
</div>
|
|
37
67
|
</div>
|
|
38
68
|
</div>
|
|
@@ -127,6 +127,19 @@ const Icon: React.FC<IconProps> = ({ name, className, title, width, height }) =>
|
|
|
127
127
|
</svg>
|
|
128
128
|
);
|
|
129
129
|
|
|
130
|
+
case 'ui-utensils':
|
|
131
|
+
return (
|
|
132
|
+
<svg
|
|
133
|
+
className={['icon', `icon--${name}`, className].filter((className) => !isEmpty(className)).join(' ')}
|
|
134
|
+
width={width}
|
|
135
|
+
height={height}
|
|
136
|
+
viewBox="0 0 416 512">
|
|
137
|
+
<HTMLComment text="!Font Awesome Free v5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc." />
|
|
138
|
+
{title && <title>{title}</title>}
|
|
139
|
+
<path d="M207.9 15.2c.8 4.7 16.1 94.5 16.1 128.8 0 52.3-27.8 89.6-68.9 104.6L168 486.7c.7 13.7-10.2 25.3-24 25.3H80c-13.7 0-24.7-11.5-24-25.3l12.9-238.1C27.7 233.6 0 196.2 0 144 0 109.6 15.3 19.9 16.1 15.2 19.3-5.1 61.4-5.4 64 16.3v141.2c1.3 3.4 15.1 3.2 16 0 1.4-25.3 7.9-139.2 8-141.8 3.3-20.8 44.7-20.8 47.9 0 .2 2.7 6.6 116.5 8 141.8.9 3.2 14.8 3.4 16 0V16.3c2.6-21.6 44.8-21.4 48-1.1zm119.2 285.7l-15 185.1c-1.2 14 9.9 26 23.9 26h56c13.3 0 24-10.7 24-24V24c0-13.2-10.7-24-24-24-82.5 0-221.4 178.5-64.9 300.9z" />
|
|
140
|
+
</svg>
|
|
141
|
+
);
|
|
142
|
+
|
|
130
143
|
case 'ui-flight':
|
|
131
144
|
return (
|
|
132
145
|
<svg
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { useDispatch } from 'react-redux';
|
|
3
2
|
import { TravelClass, TravelType } from '../../types';
|
|
4
3
|
|
|
5
4
|
interface ItemPickerProps {
|
|
@@ -12,8 +11,6 @@ interface ItemPickerProps {
|
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
const ItemPicker: React.FC<ItemPickerProps> = ({ items, selection, label, placeholder, classModifier, onPick }) => {
|
|
15
|
-
const dispatch = useDispatch();
|
|
16
|
-
|
|
17
14
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
18
15
|
const [openDirection, setOpenDirection] = useState<'down' | 'up'>('down');
|
|
19
16
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -21,8 +18,8 @@ const ItemPicker: React.FC<ItemPickerProps> = ({ items, selection, label, placeh
|
|
|
21
18
|
const toggleButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
22
19
|
|
|
23
20
|
const handlePick = (picked: string) => {
|
|
24
|
-
dispatch(onPick(picked));
|
|
25
21
|
setIsDropdownOpen(false);
|
|
22
|
+
onPick(picked);
|
|
26
23
|
};
|
|
27
24
|
|
|
28
25
|
useEffect(() => {
|
|
@@ -49,10 +46,11 @@ const ItemPicker: React.FC<ItemPickerProps> = ({ items, selection, label, placeh
|
|
|
49
46
|
}, [isDropdownOpen]);
|
|
50
47
|
|
|
51
48
|
return (
|
|
52
|
-
<
|
|
49
|
+
<div className={'dropdown__input ' + classModifier}>
|
|
53
50
|
<span className="dropdown__label">{label}</span>
|
|
54
|
-
<div className="dropdown">
|
|
51
|
+
<div className="dropdown" ref={dropdownRef}>
|
|
55
52
|
<button
|
|
53
|
+
type="button"
|
|
56
54
|
className={`dropdown-toggle ${isDropdownOpen ? 'dropdown-toggle--open' : ''}`}
|
|
57
55
|
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
|
58
56
|
ref={toggleButtonRef}>
|
|
@@ -70,7 +68,7 @@ const ItemPicker: React.FC<ItemPickerProps> = ({ items, selection, label, placeh
|
|
|
70
68
|
</ul>
|
|
71
69
|
)}
|
|
72
70
|
</div>
|
|
73
|
-
</
|
|
71
|
+
</div>
|
|
74
72
|
);
|
|
75
73
|
};
|
|
76
74
|
|