@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.
Files changed (43) hide show
  1. package/build/build-cjs/index.js +621 -2822
  2. package/build/build-cjs/search-results/store/search-results-slice.d.ts +6 -3
  3. package/build/build-cjs/search-results/types.d.ts +9 -12
  4. package/build/build-esm/index.js +617 -2625
  5. package/build/build-esm/search-results/store/search-results-slice.d.ts +6 -3
  6. package/build/build-esm/search-results/types.d.ts +9 -12
  7. package/package.json +2 -3
  8. package/src/booking-product/components/dates.tsx +9 -4
  9. package/src/booking-wizard/components/step-indicator.tsx +11 -2
  10. package/src/booking-wizard/features/booking/booking-slice.ts +27 -2
  11. package/src/booking-wizard/features/booking/booking.tsx +32 -15
  12. package/src/booking-wizard/features/booking/selectors.ts +6 -0
  13. package/src/booking-wizard/features/flight-options/index.tsx +27 -3
  14. package/src/booking-wizard/features/product-options/option-room.tsx +1 -1
  15. package/src/booking-wizard/features/product-options/options-form.tsx +14 -4
  16. package/src/booking-wizard/features/room-options/room.tsx +1 -1
  17. package/src/booking-wizard/features/sidebar/index.tsx +4 -1
  18. package/src/booking-wizard/features/sidebar/sidebar-util.ts +1 -3
  19. package/src/booking-wizard/features/sidebar/sidebar.tsx +112 -104
  20. package/src/booking-wizard/features/travelers-form/travelers-form-slice.ts +1 -0
  21. package/src/booking-wizard/features/travelers-form/travelers-form.tsx +146 -10
  22. package/src/booking-wizard/settings-context.ts +2 -1
  23. package/src/booking-wizard/types.ts +1 -0
  24. package/src/qsm/components/search-input-group/index.tsx +17 -8
  25. package/src/search-results/components/hotel/hotel-accommodation-results.tsx +78 -83
  26. package/src/search-results/components/hotel/hotel-card.tsx +54 -24
  27. package/src/search-results/components/icon.tsx +13 -0
  28. package/src/search-results/components/item-picker/index.tsx +5 -7
  29. package/src/search-results/components/search-results-container/search-results-container.tsx +72 -117
  30. package/src/search-results/components/tab-views/index.tsx +22 -3
  31. package/src/search-results/features/flights/flight-search-results-self-contained.tsx +0 -13
  32. package/src/search-results/features/hotels/hotel-flight-search-results-self-contained.tsx +1 -8
  33. package/src/search-results/features/hotels/hotel-search-results-self-contained.tsx +1 -14
  34. package/src/search-results/features/roundtrips/roundtrip-search-results-self-contained.tsx +1 -9
  35. package/src/search-results/store/search-results-slice.ts +11 -4
  36. package/src/search-results/types.ts +11 -16
  37. package/src/shared/translations/en-GB.json +5 -1
  38. package/src/shared/translations/fr-BE.json +5 -1
  39. package/src/shared/translations/nl-BE.json +5 -1
  40. package/styles/components/_form.scss +51 -2
  41. package/styles/components/_passenger-picker.scss +3 -2
  42. package/styles/components/_qsm.scss +1 -1
  43. 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 mockedHotelResults = [
12
- {
13
- type: 'hotel',
14
- id: 'HTFSWILLCARL',
15
- title: 'HTFSWILLCARL',
16
- image: 'https://images.unsplash.com/photo-1573790387438-4da905039392?q=80&w=1925&auto=format&fit=crop',
17
- description: '2 persoons kamer',
18
- location: 'Tenerif, Spanje',
19
- price: '$2244',
20
- ctaText: 'Bekijk details',
21
- days: '7 nights',
22
- flightInfo: null,
23
- accommodation: null,
24
- regime: null,
25
- stars: 5
26
- } as HotelResult,
27
- {
28
- type: 'hotel',
29
- id: 'HTFSSOFTROCK',
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: 'HTFSCONBEL',
60
- title: 'HTFSCONBEL',
61
- image: 'https://images.unsplash.com/photo-1573790387438-4da905039392?q=80&w=1925&auto=format&fit=crop',
62
- description: '5 persoons kamer',
63
- location: 'Tenerif, Spanje',
64
- price: '$6784.8',
65
- ctaText: 'Bekijk details',
66
- days: '7 nights',
67
- flightInfo: null,
68
- accommodation: null,
69
- regime: null,
70
- stars: 5
71
- } as HotelResult
72
- ] as HotelResult[];
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
- const renderResults = (results: any[]) => {
76
- const renderedResults = results.map((result, index) => {
77
- if (context?.showCustomCards && context?.customCardRenderer) {
78
- return (
79
- <div key={`${result.id}-${index}`} className="search__result-card">
80
- {context.customCardRenderer(result)}
81
- </div>
82
- );
83
- }
84
- return <HotelCard key={`${result.id}-${index}`} result={result} />;
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
- return <div className="search__results__cards">{renderedResults}</div>;
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">19</p>
95
- <p>Jan</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>Accomodation</strong>
100
+ Select <strong>Accommodation</strong>
100
101
  </h3>
101
102
  </div>
102
103
  </div>
103
- {isLoading ? (
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, { useContext } from 'react';
1
+ import React from 'react';
2
2
  import { HotelResult } from '../../types';
3
- import SearchResultsConfigurationContext from '../../search-results-configuration-context';
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 className="search__result-card">
13
- <div className="search__result-card__wrapper">
14
- <div className="search__result-card__image-wrapper">
15
- <img src={result.image} alt={result.title} className="search__result-card__image" />
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
- <div className="search__result-card__content">
18
- {result.location && (
19
- <div className="search__result-card__location">
20
- <svg id="ui-marker" viewBox="0 0 13.33 20">
21
- <path
22
- d="M6.67,0A6.67,6.67,0,0,0,0,6.67,6.78,6.78,0,0,0,.8,9.84l5.5,10a.42.42,0,0,0,.57.16A.49.49,0,0,0,7,19.79l5.5-10A6.67,6.67,0,0,0,6.67,0Zm0,10A3.34,3.34,0,1,1,10,6.67,3.34,3.34,0,0,1,6.67,10Z"
23
- transform="translate(0 0)"></path>
24
- </svg>
25
- {result.location}
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 &gt; 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
- <label className={'dropdown__input ' + classModifier}>
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
- </label>
71
+ </div>
74
72
  );
75
73
  };
76
74