@qite/tide-booking-component 1.4.69 → 1.4.71

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 (70) hide show
  1. package/build/build-cjs/index.js +1299 -1058
  2. package/build/build-cjs/src/qsm/store/qsm-slice.d.ts +4 -4
  3. package/build/build-cjs/src/qsm/types.d.ts +2 -14
  4. package/build/build-cjs/src/search-results/components/filters/filters.d.ts +1 -1
  5. package/build/build-cjs/src/search-results/components/filters/utility.d.ts +2 -2
  6. package/build/build-cjs/src/search-results/components/group-tour/group-tour-card.d.ts +8 -0
  7. package/build/build-cjs/src/search-results/components/group-tour/group-tour-results.d.ts +6 -0
  8. package/build/build-cjs/src/search-results/components/hotel/hotel-accommodation-results.d.ts +0 -2
  9. package/build/build-cjs/src/search-results/store/search-results-slice.d.ts +5 -8
  10. package/build/build-cjs/src/search-results/types.d.ts +7 -2
  11. package/build/build-cjs/src/search-results/utils/search-results-utils.d.ts +3 -0
  12. package/build/build-cjs/src/shared/components/flyin/accommodation-flyin.d.ts +8 -0
  13. package/build/build-cjs/src/shared/components/flyin/flights-flyin.d.ts +7 -0
  14. package/build/build-cjs/src/shared/components/{flyin.d.ts → flyin/flyin.d.ts} +3 -2
  15. package/build/build-cjs/src/shared/types.d.ts +12 -0
  16. package/build/build-cjs/src/shared/utils/localization-util.d.ts +5 -0
  17. package/build/build-esm/index.js +1285 -1053
  18. package/build/build-esm/src/qsm/store/qsm-slice.d.ts +4 -4
  19. package/build/build-esm/src/qsm/types.d.ts +2 -14
  20. package/build/build-esm/src/search-results/components/filters/filters.d.ts +1 -1
  21. package/build/build-esm/src/search-results/components/filters/utility.d.ts +2 -2
  22. package/build/build-esm/src/search-results/components/group-tour/group-tour-card.d.ts +8 -0
  23. package/build/build-esm/src/search-results/components/group-tour/group-tour-results.d.ts +6 -0
  24. package/build/build-esm/src/search-results/components/hotel/hotel-accommodation-results.d.ts +0 -2
  25. package/build/build-esm/src/search-results/store/search-results-slice.d.ts +5 -8
  26. package/build/build-esm/src/search-results/types.d.ts +7 -2
  27. package/build/build-esm/src/search-results/utils/search-results-utils.d.ts +3 -0
  28. package/build/build-esm/src/shared/components/flyin/accommodation-flyin.d.ts +8 -0
  29. package/build/build-esm/src/shared/components/flyin/flights-flyin.d.ts +7 -0
  30. package/build/build-esm/src/shared/components/{flyin.d.ts → flyin/flyin.d.ts} +3 -2
  31. package/build/build-esm/src/shared/types.d.ts +12 -0
  32. package/build/build-esm/src/shared/utils/localization-util.d.ts +5 -0
  33. package/package.json +2 -2
  34. package/src/qsm/components/QSMContainer/qsm-container.tsx +16 -3
  35. package/src/qsm/store/qsm-slice.ts +4 -4
  36. package/src/qsm/types.ts +2 -15
  37. package/src/search-results/components/filters/filters.tsx +136 -293
  38. package/src/search-results/components/filters/utility.tsx +61 -2
  39. package/src/search-results/components/group-tour/group-tour-card.tsx +100 -0
  40. package/src/search-results/components/group-tour/group-tour-results.tsx +40 -0
  41. package/src/search-results/components/hotel/hotel-accommodation-results.tsx +13 -16
  42. package/src/search-results/components/hotel/hotel-card.tsx +11 -8
  43. package/src/search-results/components/icon.tsx +18 -0
  44. package/src/search-results/components/search-results-container/search-results-container.tsx +62 -30
  45. package/src/search-results/store/search-results-slice.ts +13 -7
  46. package/src/search-results/types.ts +9 -2
  47. package/src/search-results/utils/search-results-utils.ts +42 -0
  48. package/src/shared/components/flyin/accommodation-flyin.tsx +40 -0
  49. package/src/shared/components/flyin/flights-flyin.tsx +499 -0
  50. package/src/shared/components/flyin/flyin.tsx +79 -0
  51. package/src/shared/translations/ar-SA.json +4 -2
  52. package/src/shared/translations/da-DK.json +4 -2
  53. package/src/shared/translations/de-DE.json +4 -2
  54. package/src/shared/translations/en-GB.json +4 -2
  55. package/src/shared/translations/es-ES.json +4 -2
  56. package/src/shared/translations/fr-BE.json +4 -2
  57. package/src/shared/translations/fr-FR.json +4 -2
  58. package/src/shared/translations/is-IS.json +4 -2
  59. package/src/shared/translations/it-IT.json +4 -2
  60. package/src/shared/translations/ja-JP.json +4 -2
  61. package/src/shared/translations/nl-BE.json +4 -2
  62. package/src/shared/translations/nl-NL.json +4 -2
  63. package/src/shared/translations/no-NO.json +4 -2
  64. package/src/shared/translations/pl-PL.json +4 -2
  65. package/src/shared/translations/pt-PT.json +4 -2
  66. package/src/shared/translations/sv-SE.json +4 -2
  67. package/src/shared/types.ts +13 -0
  68. package/src/shared/utils/localization-util.ts +16 -0
  69. package/styles/components/_flyin.scss +10 -0
  70. package/src/shared/components/flyin.tsx +0 -546
@@ -9,96 +9,21 @@ import Icon from '../icon';
9
9
  import { getTranslations } from '../../../shared/utils/localization-util';
10
10
 
11
11
  interface FiltersProps {
12
+ initialFilters: Filter[];
12
13
  filters: Filter[];
13
14
  isOpen: boolean;
14
15
  handleSetIsOpen: () => void;
15
- handleApplyFilters: () => void;
16
16
  isLoading?: boolean;
17
17
  }
18
18
 
19
- const initialFilters: Filter[] = [
20
- {
21
- property: 'regime',
22
- label: 'Regime',
23
- type: 'checkbox',
24
- options: [
25
- {
26
- label: 'Room only',
27
- value: ['RO', 'LO', 'OB'],
28
- isChecked: false
29
- },
30
- {
31
- label: 'Bed & Beakfast',
32
- value: ['BB', 'KO'],
33
- isChecked: false
34
- },
35
- {
36
- label: 'Half board',
37
- value: ['HB'],
38
- isChecked: false
39
- },
40
- {
41
- label: 'Full board',
42
- value: ['FB'],
43
- isChecked: false
44
- }
45
- ],
46
- isFrontendFilter: false
47
- },
48
- {
49
- property: 'price',
50
- label: 'Prijs',
51
- type: 'slider',
52
- isFrontendFilter: false,
53
- min: 2244,
54
- max: 6785
55
- },
56
- {
57
- property: 'rating',
58
- label: 'Rating',
59
- type: 'star-rating',
60
- isFrontendFilter: true
61
- },
62
- {
63
- property: 'theme',
64
- label: "Thema's",
65
- type: 'toggle',
66
- options: [
67
- {
68
- label: 'Adults',
69
- value: 1,
70
- isChecked: false
71
- },
72
- {
73
- label: 'Luxury',
74
- value: 2,
75
- isChecked: false
76
- },
77
- {
78
- label: 'Welness & Spa',
79
- value: 3,
80
- isChecked: false
81
- },
82
- {
83
- label: 'familie',
84
- value: 4,
85
- isChecked: false
86
- }
87
- ],
88
- isFrontendFilter: false
19
+ const Filters: React.FC<FiltersProps> = ({ initialFilters, filters, isOpen, handleSetIsOpen, isLoading }) => {
20
+ const context = useContext(SearchResultsConfigurationContext);
21
+ if (!context || !context.showFilters) {
22
+ return null;
89
23
  }
90
- ] as Filter[];
91
24
 
92
- const Filters: React.FC<FiltersProps> = ({ filters, isOpen, handleSetIsOpen, handleApplyFilters, isLoading }) => {
93
- const context = useContext(SearchResultsConfigurationContext);
94
25
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
95
26
  const [visibleFilters, setVisibleFilters] = useState<Record<string, boolean>>({});
96
- const [pendingFilters, setPendingFilters] = useState<Filter[]>([]);
97
- useEffect(() => {
98
- if (initialFilters.length > 0) {
99
- setPendingFilters(initialFilters);
100
- }
101
- }, [initialFilters]);
102
27
 
103
28
  const dispatch = useDispatch();
104
29
 
@@ -109,218 +34,153 @@ const Filters: React.FC<FiltersProps> = ({ filters, isOpen, handleSetIsOpen, han
109
34
  }));
110
35
  };
111
36
 
112
- const updatePendingFilter = (updatedFilter: Filter) => {
113
- setPendingFilters((prevFilters) => {
114
- const index = prevFilters.findIndex((f) => f.property === updatedFilter.property);
115
- if (index !== -1) {
116
- const newFilters = [...prevFilters];
117
- newFilters[index] = updatedFilter;
118
- return newFilters;
119
- } else {
120
- return [...prevFilters, updatedFilter];
121
- }
122
- });
123
- };
124
-
125
37
  const handleCheckBoxFilter = (filter: Filter, option: FilterOption) => {
126
- setPendingFilters((prev) => {
127
- const updated = prev.map((f) => {
128
- if (f.property !== filter.property) {
129
- return f;
130
- }
131
-
132
- return {
133
- ...f,
134
- options: f.options?.map((opt) => (opt.value === option.value ? { ...opt, isChecked: !opt.isChecked } : opt))
135
- };
136
- });
137
-
138
- if (!context?.useGlobalApplyFilterButton && !filter.useApplyFilterButton) {
139
- dispatch(setFilters(updated));
140
- context?.onFilterChange?.(updated);
141
- }
142
-
143
- return updated;
144
- });
145
- };
38
+ const updated = filters.map((f) => {
39
+ if (f.property !== filter.property) return f;
146
40
 
147
- const handleSliderMinChange = (filter: Filter, value: number) => {
148
- if (value < (filter.selectedMax ?? filter.max ?? 100)) {
149
- const updatedFilter = {
150
- ...filter,
151
- selectedMin: value
41
+ return {
42
+ ...f,
43
+ options: f.options?.map((opt) => (opt.value === option.value ? { ...opt, isChecked: !opt.isChecked } : opt))
152
44
  };
45
+ });
153
46
 
154
- updatePendingFilter(updatedFilter);
155
- if (!context?.useGlobalApplyFilterButton && !filter.useApplyFilterButton) {
156
- applyFilters();
157
- }
158
- }
47
+ dispatch(setFilters(updated));
159
48
  };
160
49
 
161
- const handleSliderMaxChange = (filter: Filter, value: number) => {
162
- if (value > (filter.selectedMin ?? filter.min ?? 0)) {
163
- const updatedFilter = {
164
- ...filter,
165
- selectedMax: value
166
- };
50
+ const handleSliderChange = (filter: Filter, newMin: number, newMax: number) => {
51
+ const updated = filters.map((f) => {
52
+ if (f.property !== filter.property) return f;
167
53
 
168
- updatePendingFilter(updatedFilter);
169
- if (!context?.useGlobalApplyFilterButton && !filter.useApplyFilterButton) {
170
- applyFilters();
171
- }
172
- }
173
- };
174
-
175
- const applyFilters = () => {
176
- dispatch(setFilters(pendingFilters));
177
- context?.onFilterChange?.(pendingFilters);
178
- handleApplyFilters();
54
+ return {
55
+ ...f,
56
+ selectedMin: newMin,
57
+ selectedMax: newMax
58
+ };
59
+ });
179
60
 
180
- if (isOpen) {
181
- handleSetIsOpen();
182
- }
61
+ dispatch(setFilters(updated));
183
62
  };
184
63
 
185
64
  const handleFullReset = () => {
186
65
  if (!isLoading) {
187
- setPendingFilters(initialFilters);
188
66
  dispatch(resetFilters(initialFilters));
189
67
  }
190
68
  };
191
69
 
192
- const handleResetPendingChanges = () => {
193
- setPendingFilters(filters); // ← back to last applied
194
- };
195
-
196
- const hasPendingChanges = (): boolean => {
197
- return JSON.stringify(pendingFilters) !== JSON.stringify(filters);
198
- };
199
-
200
- if (!context || !context.showFilters) {
201
- return null;
202
- }
203
-
204
70
  return (
205
- <>
206
- {/* ---------------- Filters ---------------- */}
207
-
208
- {/* ---------------- Filters desktop ---------------- */}
209
- <div className={`search__filters--modal ${isOpen ? 'is-open' : ''}`}>
210
- <div className="search__filters--background" onClick={() => handleSetIsOpen()}></div>
211
- <button className="search__filters--close" onClick={() => handleSetIsOpen()}>
212
- <Icon name="ui-close" height={24} />
213
- </button>
214
- <div className="search__filters">
215
- <div className="search__filter-row search__filter__header">
216
- <div className="search__filter-row-flex-title">
217
- <p className="search__filter-small-title">{translations.SRP.FILTERS}</p>
218
- </div>
219
- {!isLoading && (
220
- <a className="search__filter-reset" onClick={() => handleFullReset()}>
221
- {translations.SRP.RESET}
222
- </a>
223
- )}
224
- {/* <Icon name="ui-info" height={16} /> */}
71
+ <div className={`search__filters--modal ${isOpen ? 'is-open' : ''}`}>
72
+ <div className="search__filters--background" onClick={() => handleSetIsOpen()}></div>
73
+ <button className="search__filters--close" onClick={() => handleSetIsOpen()}>
74
+ <Icon name="ui-close" height={24} />
75
+ </button>
76
+ <div className="search__filters">
77
+ <div className="search__filter-row search__filter__header">
78
+ <div className="search__filter-row-flex-title">
79
+ <p className="search__filter-small-title">{translations.SRP.FILTERS}</p>
225
80
  </div>
226
- {isLoading ? (
227
- <Spinner />
228
- ) : (
229
- <>
230
- <div className="search__filters__group-container">
231
- {pendingFilters.map((filter, index) => {
232
- const isVisible = visibleFilters[filter.property] ?? true;
233
-
234
- return (
235
- <div key={`${filter.property}-${index}`} className="search__filter-group">
236
- <div
237
- className="search__filter-row search__filter-row--underline"
238
- onClick={() => toggleFilterVisibility(filter.property)}
239
- role="button"
240
- tabIndex={0}>
241
- <h6 className="search__filter-large-title">{filter.label}</h6>
242
- <svg
243
- id="search-chevron-up-icon"
244
- className={`search__filter-chevron-icon ${isVisible ? 'search__filter-chevron-icon--flipped' : ''} `}
245
- viewBox="0 0 10 6.063"
246
- width={10}
247
- height={6.063}>
248
- <path
249
- id="Path_62"
250
- data-name="Path 62"
251
- d="M245-617.937l-5-5L241.063-624,245-620.062,248.938-624,250-622.937Z"
252
- transform="translate(-240 624)"
253
- fill="#707070"
254
- />
255
- </svg>
256
- </div>
81
+ {!isLoading && (
82
+ <a className="search__filter-reset" onClick={() => handleFullReset()}>
83
+ {translations.SRP.RESET}
84
+ </a>
85
+ )}
86
+ </div>
87
+ {isLoading ? (
88
+ <Spinner />
89
+ ) : (
90
+ <>
91
+ <div className="search__filters__group-container">
92
+ {filters.map((filter, index) => {
93
+ const isVisible = visibleFilters[filter.property] ?? true;
94
+
95
+ return (
96
+ <div key={`${filter.property}-${index}`} className="search__filter-group">
97
+ <div
98
+ className="search__filter-row search__filter-row--underline"
99
+ onClick={() => toggleFilterVisibility(filter.property)}
100
+ role="button"
101
+ tabIndex={0}>
102
+ <h6 className="search__filter-large-title">{filter.label}</h6>
103
+ <svg
104
+ id="search-chevron-up-icon"
105
+ className={`search__filter-chevron-icon ${isVisible ? 'search__filter-chevron-icon--flipped' : ''} `}
106
+ viewBox="0 0 10 6.063"
107
+ width={10}
108
+ height={6.063}>
109
+ <path
110
+ id="Path_62"
111
+ data-name="Path 62"
112
+ d="M245-617.937l-5-5L241.063-624,245-620.062,248.938-624,250-622.937Z"
113
+ transform="translate(-240 624)"
114
+ fill="#707070"
115
+ />
116
+ </svg>
117
+ </div>
257
118
 
258
- {isVisible && filter.type === 'checkbox' && (
259
- <div className="search__filter-rows">
260
- {filter.options &&
261
- filter.options.map((option: FilterOption, optionIndex) => (
262
- <div className="search__filter-row search__filter-row--checkbox" key={`${option.label}-${optionIndex}`}>
263
- <div className="checkbox">
264
- <label className="checkbox__label">
265
- <input
266
- type="checkbox"
267
- className="checkbox__input checkbox__input--parent"
268
- checked={option.isChecked}
269
- onChange={(e) => handleCheckBoxFilter(filter, option)}
270
- />
271
- <span className="radiobutton__label-text">{option.label}</span>
272
- </label>
273
- </div>
119
+ {isVisible && filter.type === 'checkbox' && (
120
+ <div className="search__filter-rows">
121
+ {filter.options &&
122
+ filter.options.map((option: FilterOption, optionIndex) => (
123
+ <div className="search__filter-row search__filter-row--checkbox" key={`${option.label}-${optionIndex}`}>
124
+ <div className="checkbox">
125
+ <label className="checkbox__label">
126
+ <input
127
+ type="checkbox"
128
+ className="checkbox__input checkbox__input--parent"
129
+ checked={option.isChecked}
130
+ onChange={(e) => handleCheckBoxFilter(filter, option)}
131
+ />
132
+ <span className="radiobutton__label-text">{option.label}</span>
133
+ </label>
274
134
  </div>
275
- ))}
276
- </div>
277
- )}
278
-
279
- {isVisible && filter.type === 'toggle' && (
280
- <div className="search__filter-rows">
281
- {filter.options?.map((option: FilterOption, optionIndex) => (
282
- <div className="search__filter-row" key={`${option.label}-${optionIndex}`}>
283
- <span className="search__filter-toggle-label">{option.label}</span>
284
- <div className="checkbox"></div>
285
- <label className="checkbox__label">
286
- <input
287
- type="checkbox"
288
- className="checkbox__input"
289
- checked={option.isChecked}
290
- onChange={() => handleCheckBoxFilter(filter, option)}
291
- />
292
- </label>
293
135
  </div>
294
136
  ))}
295
- </div>
296
- )}
297
-
298
- {isVisible &&
299
- filter &&
300
- filter.type === 'slider' &&
301
- (() => {
302
- const min = filter.min ?? 0;
303
- const max = filter.max ?? 100;
304
- const selectedMin = filter.selectedMin ?? min;
305
- const selectedMax = filter.selectedMax ?? max;
306
- const range = max - min || 1;
307
-
308
- return (
309
- <MultiRangeFilter
310
- min={min}
311
- max={max}
312
- selectedMin={selectedMin}
313
- selectedMax={selectedMax}
314
- valueFormatter={(value) => `${value}`}
315
- onChange={(newMin, newMax) => {
316
- handleSliderMinChange(filter, newMin);
317
- handleSliderMaxChange(filter, newMax);
318
- }}
319
- />
320
- );
321
- })()}
137
+ </div>
138
+ )}
139
+
140
+ {isVisible && filter.type === 'toggle' && (
141
+ <div className="search__filter-rows">
142
+ {filter.options?.map((option: FilterOption, optionIndex) => (
143
+ <div className="search__filter-row" key={`${option.label}-${optionIndex}`}>
144
+ <span className="search__filter-toggle-label">{option.label}</span>
145
+ <div className="checkbox"></div>
146
+ <label className="checkbox__label">
147
+ <input
148
+ type="checkbox"
149
+ className="checkbox__input"
150
+ checked={option.isChecked}
151
+ onChange={() => handleCheckBoxFilter(filter, option)}
152
+ />
153
+ </label>
154
+ </div>
155
+ ))}
156
+ </div>
157
+ )}
158
+
159
+ {isVisible &&
160
+ filter &&
161
+ filter.type === 'slider' &&
162
+ (() => {
163
+ const min = filter.min ?? 0;
164
+ const max = filter.max ?? 100;
165
+ const selectedMin = filter.selectedMin ?? min;
166
+ const selectedMax = filter.selectedMax ?? max;
167
+ const range = max - min || 1;
168
+
169
+ return (
170
+ <MultiRangeFilter
171
+ min={min}
172
+ max={max}
173
+ selectedMin={selectedMin}
174
+ selectedMax={selectedMax}
175
+ valueFormatter={(value) => `${value}`}
176
+ onChange={(newMin, newMax) => {
177
+ handleSliderChange(filter, newMin, newMax);
178
+ }}
179
+ />
180
+ );
181
+ })()}
322
182
 
323
- {isVisible &&
183
+ {/* {isVisible &&
324
184
  filter.property === 'rating' &&
325
185
  filter.type === 'star-rating' &&
326
186
  (() => {
@@ -355,32 +215,15 @@ const Filters: React.FC<FiltersProps> = ({ filters, isOpen, handleSetIsOpen, han
355
215
  ))}
356
216
  </div>
357
217
  );
358
- })()}
359
- </div>
360
- );
361
- })}
362
- {context.useGlobalApplyFilterButton && hasPendingChanges() && (
363
- <div className="search__filters__actions">
364
- <button
365
- className="cta--secondary "
366
- onClick={() => {
367
- handleResetPendingChanges();
368
- handleSetIsOpen();
369
- }}
370
- disabled={isLoading}>
371
- {translations.SRP.CANCEL}
372
- </button>
373
- <button className="cta" onClick={() => applyFilters()} disabled={isLoading}>
374
- {translations.SRP.APPLY}
375
- </button>
218
+ })()} */}
376
219
  </div>
377
- )}
378
- </div>
379
- </>
380
- )}
381
- </div>
220
+ );
221
+ })}
222
+ </div>
223
+ </>
224
+ )}
382
225
  </div>
383
- </>
226
+ </div>
384
227
  );
385
228
  };
386
229
 
@@ -1,7 +1,7 @@
1
1
  import { BookingPackageItem } from '@qite/tide-client/build/types';
2
- import { Filter } from '../../types';
2
+ import { Filter, TideTag } from '../../types';
3
3
 
4
- export const enrichFiltersWithResults = (results: BookingPackageItem[], filters: Filter[] | undefined): Filter[] => {
4
+ export const enrichFiltersWithResults = (results: BookingPackageItem[], filters: Filter[] | undefined, tags: TideTag[]): Filter[] => {
5
5
  if (!results || results.length === 0 || !filters) {
6
6
  return filters ?? [];
7
7
  }
@@ -14,6 +14,65 @@ export const enrichFiltersWithResults = (results: BookingPackageItem[], filters:
14
14
  filter.max = Math.ceil(Math.max(...prices));
15
15
  }
16
16
  }
17
+
18
+ if (filter.property === 'accommodation') {
19
+ const map = new Map<string, { name?: string; code: string }>();
20
+
21
+ results.forEach((r) => {
22
+ if (r.accommodationCode) {
23
+ map.set(r.accommodationCode, {
24
+ name: r.accommodationName,
25
+ code: r.accommodationCode
26
+ });
27
+ }
28
+ });
29
+
30
+ console.log('map', map);
31
+
32
+ filter.options = Array.from(map.values()).map((accommodation) => ({
33
+ label: accommodation.name ?? accommodation.code,
34
+ value: accommodation.code,
35
+ isChecked: false
36
+ }));
37
+ }
38
+
39
+ if (filter.property === 'regime') {
40
+ const map = new Map<string, { name?: string; code: string }>();
41
+
42
+ results.forEach((r) => {
43
+ if (r.regimeCode) {
44
+ map.set(r.regimeCode, {
45
+ name: r.regimeName,
46
+ code: r.regimeCode
47
+ });
48
+ }
49
+ });
50
+
51
+ filter.options = Array.from(map.values()).map((regime) => ({
52
+ label: regime.name ?? regime.code,
53
+ value: regime.code,
54
+ isChecked: false
55
+ }));
56
+ }
57
+
58
+ if (filter.property === 'theme') {
59
+ const map = new Map<number, { name: string; id: number }>();
60
+
61
+ results.forEach((r) => {
62
+ r.tagIds?.forEach((tagId) => {
63
+ const tag = tags.find((t) => t.id === tagId);
64
+ if (tag && tag.id != null && tag.name != null) {
65
+ map.set(tag.id, { name: tag.name, id: tag.id });
66
+ }
67
+ });
68
+ });
69
+
70
+ filter.options = Array.from(map.values()).map((theme) => ({
71
+ label: theme.name,
72
+ value: theme.id,
73
+ isChecked: false
74
+ }));
75
+ }
17
76
  }
18
77
 
19
78
  return filters;
@@ -0,0 +1,100 @@
1
+ import React from 'react';
2
+ import Icon from '../icon';
3
+ import { BookingPackageItem } from '@qite/tide-client';
4
+ import { format } from 'date-fns';
5
+ import { calculateDays, calculateNights, formatPrice, getTranslations } from '../../../shared/utils/localization-util';
6
+ import { SearchResultsRootState } from '../../store/search-results-store';
7
+ import { useDispatch, useSelector } from 'react-redux';
8
+ import { setSelectedSearchResult } from '../../store/search-results-slice';
9
+
10
+ interface GroupTourCardProps {
11
+ result: BookingPackageItem;
12
+ languageCode?: string;
13
+ }
14
+
15
+ const GroupTourCard: React.FC<GroupTourCardProps> = ({ result, languageCode }) => {
16
+ const dispatch = useDispatch();
17
+ const translations = getTranslations(languageCode ?? 'en-GB');
18
+ const { selectedSearchResultId } = useSelector((state: SearchResultsRootState) => state.searchResults);
19
+
20
+ const genders = result.allotment?.travellerGenders || [];
21
+ const maleCount = genders.filter((g) => g === 0).length;
22
+ const femaleCount = genders.filter((g) => g === 1).length;
23
+ const otherCount = genders.filter((g) => g === 2).length;
24
+
25
+ const handleChange = (productId: number) => {
26
+ dispatch(setSelectedSearchResult(productId));
27
+ };
28
+
29
+ return (
30
+ <div className="search__result-card">
31
+ <div className="search__result-card__allotment">
32
+ <div className="search__result-card__allotment__title__wrapper">
33
+ <h3 className="search__result-card__allotment__title">
34
+ {result.name}
35
+ <span className="search__result-card__allotment__badge">
36
+ <Icon name="ui-circle-check" width={14} height={14} />
37
+ GAR
38
+ </span>
39
+ </h3>
40
+
41
+ <div className="search__result-card__allotment__container">
42
+ <div className="search__result-card__allotment__header">
43
+ <div className="search__result-card__allotment__wrapper">
44
+ <div className="search__result-card__allotment__date">
45
+ <Icon name="ui-plane" height={16} />
46
+ <div className="search__result-card__allotment__date--from">{format(new Date(result.fromDate), 'dd/MM/yyyy')}</div>
47
+ </div>
48
+ <div className="search__result-card__allotment__info">
49
+ <span>
50
+ <Icon name="ui-calendar" height={16} />
51
+ </span>{' '}
52
+ {calculateDays(result.stayFromDate, result.stayToDate)} {translations.PRODUCT.DAYS} -{' '}
53
+ <span>
54
+ <Icon name="ui-moon" height={16} />
55
+ </span>
56
+ {calculateNights(result.stayFromDate, result.stayToDate)} {translations.SRP.NIGHTS}
57
+ </div>
58
+ </div>
59
+ </div>
60
+ {result.allotment && (
61
+ <div className="search__result-card__allotment__info">
62
+ <span className="search__result-card__allotment__info__group">{translations?.SRP.TRAVEL_GROUP}</span>
63
+ <div className="search__result-card__allotment__persons">
64
+ <div className="search__result-card__allotment__person">
65
+ <Icon name="ui-men" width={16} height={16} />
66
+ <span>{maleCount} p.</span>
67
+ </div>
68
+ <div className="search__result-card__allotment__person">
69
+ <Icon name="ui-women" width={16} height={16} />
70
+ <span>{femaleCount} p.</span>
71
+ </div>
72
+ <div className="search__result-card__allotment__person">
73
+ <Icon name="ui-other" width={16} height={16} />
74
+ <span>{otherCount} p.</span>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ )}
79
+ </div>
80
+ </div>
81
+ <div className="search__result-card__allotment__footer">
82
+ <div className="search__result-card__allotment__price__wrapper">
83
+ <div className="search__result-card__allotment__price">
84
+ {formatPrice(result.price, result.currencyCode, languageCode)} {translations.PRODUCT.PER_PERSON}
85
+ </div>
86
+ {/* <div className="search__result-card__allotment__price__info">Gelieve € 450,00 zakgeld mee te nemen</div> */}
87
+ </div>
88
+ <button
89
+ type="button"
90
+ className={`cta ${selectedSearchResultId === result.productId ? 'cta--selected' : 'cta--select'}`}
91
+ onClick={() => handleChange(result.productId)}>
92
+ {selectedSearchResultId === result.productId ? translations?.SHARED.SELECTED : translations?.SHARED.SELECT}
93
+ </button>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ );
98
+ };
99
+
100
+ export default GroupTourCard;