@qite/tide-booking-component 1.4.38 → 1.4.40

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 (99) hide show
  1. package/build/build-cjs/booking-wizard/types.d.ts +1 -0
  2. package/build/build-cjs/content/components/login.d.ts +3 -0
  3. package/build/build-cjs/index.js +7618 -1952
  4. package/build/build-cjs/qsm/components/double-search-input-group/index.d.ts +2 -1
  5. package/build/build-cjs/qsm/components/search-input/index.d.ts +1 -0
  6. package/build/build-cjs/qsm/components/search-input-group/index.d.ts +3 -1
  7. package/build/build-cjs/qsm/store/qsm-slice.d.ts +6 -2
  8. package/build/build-cjs/qsm/types.d.ts +17 -32
  9. package/build/build-cjs/search-results/components/filters/flight-filters.d.ts +8 -0
  10. package/build/build-cjs/search-results/components/flight/flight-accommodation-results.d.ts +4 -1
  11. package/build/build-cjs/search-results/components/flight/flight-option.d.ts +7 -0
  12. package/build/build-cjs/search-results/components/flight/flight-search-context/index.d.ts +36 -0
  13. package/build/build-cjs/search-results/components/icon.d.ts +1 -0
  14. package/build/build-cjs/search-results/components/item-picker/index.d.ts +5 -3
  15. package/build/build-cjs/search-results/components/search-results-container/flight-search-results.d.ts +6 -0
  16. package/build/build-cjs/search-results/store/search-results-slice.d.ts +2 -0
  17. package/build/build-cjs/search-results/types.d.ts +31 -1
  18. package/build/build-cjs/search-results/utils/flight-utils.d.ts +16 -0
  19. package/build/build-cjs/shared/components/flyin.d.ts +9 -0
  20. package/build/build-cjs/shared/types.d.ts +6 -0
  21. package/build/build-cjs/shared/utils/localization-util.d.ts +91 -0
  22. package/build/build-esm/booking-wizard/types.d.ts +1 -0
  23. package/build/build-esm/content/components/login.d.ts +3 -0
  24. package/build/build-esm/index.js +8053 -2356
  25. package/build/build-esm/qsm/components/double-search-input-group/index.d.ts +2 -1
  26. package/build/build-esm/qsm/components/search-input/index.d.ts +1 -0
  27. package/build/build-esm/qsm/components/search-input-group/index.d.ts +3 -1
  28. package/build/build-esm/qsm/store/qsm-slice.d.ts +6 -2
  29. package/build/build-esm/qsm/types.d.ts +17 -32
  30. package/build/build-esm/search-results/components/filters/flight-filters.d.ts +8 -0
  31. package/build/build-esm/search-results/components/flight/flight-accommodation-results.d.ts +4 -1
  32. package/build/build-esm/search-results/components/flight/flight-option.d.ts +7 -0
  33. package/build/build-esm/search-results/components/flight/flight-search-context/index.d.ts +36 -0
  34. package/build/build-esm/search-results/components/icon.d.ts +1 -0
  35. package/build/build-esm/search-results/components/item-picker/index.d.ts +5 -3
  36. package/build/build-esm/search-results/components/search-results-container/flight-search-results.d.ts +6 -0
  37. package/build/build-esm/search-results/store/search-results-slice.d.ts +2 -0
  38. package/build/build-esm/search-results/types.d.ts +31 -1
  39. package/build/build-esm/search-results/utils/flight-utils.d.ts +16 -0
  40. package/build/build-esm/shared/components/flyin.d.ts +9 -0
  41. package/build/build-esm/shared/types.d.ts +6 -0
  42. package/build/build-esm/shared/utils/localization-util.d.ts +91 -0
  43. package/package.json +4 -3
  44. package/rollup.config.js +2 -2
  45. package/src/booking-product/components/dates.tsx +1 -1
  46. package/src/booking-wizard/features/booking/booking-slice.ts +4 -2
  47. package/src/booking-wizard/types.ts +1 -0
  48. package/src/content/components/login.tsx +162 -0
  49. package/src/content/components/slider.tsx +1 -1
  50. package/src/content/features/content-page/content-page-self-contained.tsx +56 -75
  51. package/src/qsm/components/QSMContainer/qsm-container.tsx +197 -75
  52. package/src/qsm/components/double-search-input-group/index.tsx +14 -75
  53. package/src/qsm/components/mobile-filter-modal/index.tsx +18 -11
  54. package/src/qsm/components/search-input/index.tsx +9 -2
  55. package/src/qsm/components/search-input-group/index.tsx +19 -31
  56. package/src/qsm/components/travel-class-picker/index.tsx +1 -0
  57. package/src/qsm/components/travel-input/index.tsx +4 -4
  58. package/src/qsm/components/travel-input-group/index.tsx +4 -3
  59. package/src/qsm/components/travel-nationality-picker/index.tsx +1 -0
  60. package/src/qsm/components/travel-type-picker/index.tsx +1 -0
  61. package/src/qsm/qsm-configuration-context.ts +6 -17
  62. package/src/qsm/store/qsm-slice.ts +13 -1
  63. package/src/qsm/types.ts +19 -39
  64. package/src/search-results/components/filters/flight-filters.tsx +671 -0
  65. package/src/search-results/components/flight/flight-accommodation-results.tsx +20 -562
  66. package/src/search-results/components/flight/flight-banner.tsx +1 -1
  67. package/src/search-results/components/flight/flight-option.tsx +243 -0
  68. package/src/search-results/components/flight/flight-search-context/index.tsx +508 -0
  69. package/src/search-results/components/hotel/hotel-card.tsx +0 -1
  70. package/src/search-results/components/icon.tsx +84 -44
  71. package/src/search-results/components/item-picker/index.tsx +16 -11
  72. package/src/search-results/components/search-results-container/flight-search-results.tsx +120 -0
  73. package/src/search-results/components/search-results-container/search-results-container.tsx +85 -70
  74. package/src/search-results/store/search-results-slice.ts +6 -0
  75. package/src/search-results/types.ts +37 -1
  76. package/src/search-results/utils/flight-utils.ts +106 -0
  77. package/src/shared/components/flyin.tsx +334 -0
  78. package/src/shared/translations/ar-SA.json +13 -1
  79. package/src/shared/translations/da-DK.json +13 -1
  80. package/src/shared/translations/de-DE.json +13 -1
  81. package/src/shared/translations/en-GB.json +13 -1
  82. package/src/shared/translations/es-ES.json +13 -1
  83. package/src/shared/translations/fr-BE.json +13 -1
  84. package/src/shared/translations/fr-FR.json +13 -1
  85. package/src/shared/translations/is-IS.json +13 -1
  86. package/src/shared/translations/it-IT.json +13 -1
  87. package/src/shared/translations/ja-JP.json +13 -1
  88. package/src/shared/translations/nl-BE.json +13 -1
  89. package/src/shared/translations/nl-NL.json +13 -1
  90. package/src/shared/translations/no-NO.json +13 -1
  91. package/src/shared/translations/pl-PL.json +13 -1
  92. package/src/shared/translations/pt-PT.json +13 -1
  93. package/src/shared/translations/sv-SE.json +13 -1
  94. package/src/shared/types.ts +7 -0
  95. package/src/shared/utils/localization-util.ts +71 -0
  96. package/styles/booking-search-results.scss +1 -0
  97. package/styles/components/_flyin.scss +550 -0
  98. package/styles/components/_login.scss +133 -0
  99. package/styles/content-blocks.scss +1 -0
@@ -14,16 +14,33 @@ import Icon from '../icon';
14
14
  import TravelNationalityPicker from '../travel-nationality-picker';
15
15
  import { addDays, addMonths } from 'date-fns';
16
16
  import { DateRange } from '../../../booking-product/types';
17
- import { QSMState, setFromDate, setToDate } from '../../store/qsm-slice';
18
- import { FieldConfig } from '../../types';
17
+ import { QSMState, setFromDate, setSelectedQsmType, setToDate, setTripType } from '../../store/qsm-slice';
18
+ import { BaseFieldConfig, DoubleFieldConfig, QsmType } from '../../types';
19
19
  import { getTranslations } from '../../../shared/utils/localization-util';
20
20
 
21
21
  const QSMContainer: React.FC = () => {
22
22
  const dispatch = useDispatch();
23
23
  const isMobile = useMediaQuery('(max-width: 768px)');
24
24
  const qsmState = useSelector((state: QSMRootState) => state.qsm);
25
- const { mobileFilterType, fromDate, toDate } = qsmState;
26
- const { searchFields, askTravelers, submitIcon, onSubmit, travelTypes, languageCode } = useContext(QSMConfigurationContext);
25
+ const { qsmType, mobileFilterType, fromDate, toDate, tripType } = qsmState;
26
+ const {
27
+ askTravelers,
28
+ askRooms,
29
+ askNationality,
30
+ askTravelClass,
31
+ askTravelType,
32
+ submitIcon,
33
+ onSubmit,
34
+ travelTypes,
35
+ languageCode,
36
+ departureAirport,
37
+ destinationAirport,
38
+ returnAirport,
39
+ destination,
40
+ allowOneWay,
41
+ allowRoundtrip,
42
+ allowOpenJaw
43
+ } = useContext(QSMConfigurationContext);
27
44
  const translations = getTranslations(languageCode ?? 'en-GB');
28
45
 
29
46
  useEffect(() => {
@@ -50,63 +67,112 @@ const QSMContainer: React.FC = () => {
50
67
  dispatch(setToDate(value.toDate?.toISOString()));
51
68
  };
52
69
 
70
+ const handleTripTypeChange = (value: 'oneway' | 'roundtrip' | 'openjaw') => {
71
+ dispatch(setTripType(value));
72
+ };
73
+
74
+ const handleQsmTypeChange = (value: QsmType) => {
75
+ dispatch(setSelectedQsmType(value));
76
+ };
77
+
53
78
  const handleSubmit = () => {
54
79
  if (!onSubmit) return;
55
80
 
56
- const { fromDate, toDate, travelers, selectedTravelClass, selectedTravelType, selectedNationality, adults, kids, babies, rooms, selectedFlexRange } =
57
- qsmState;
58
-
59
- const selectedTravelTypeValue = travelTypes.find((t) => t.label === selectedTravelType);
81
+ const { fromDate, toDate, selectedTravelClass, selectedTravelType, selectedNationality, rooms, tripType, adults, kids, babies } = qsmState;
60
82
 
83
+ const selectedTravelTypeValue = travelTypes?.find((t) => t.label === selectedTravelType);
61
84
  const payload = {
62
85
  fromDate,
63
86
  toDate,
64
87
  travelClass: selectedTravelClass,
65
88
  travelType: selectedTravelTypeValue,
66
89
  nationality: selectedNationality,
67
- rooms
68
- };
90
+ tripType
91
+ } as any;
92
+
93
+ if (askRooms) {
94
+ payload.rooms = rooms;
95
+ } else {
96
+ payload.travelers = { adults, kids, babies };
97
+ }
98
+
99
+ // Filter out undefined fields before passing to addSearchFieldsToPayload
100
+ const searchFields = [departureAirport, destinationAirport, returnAirport, destination].filter((field): field is BaseFieldConfig => field !== undefined);
69
101
 
70
102
  addSearchFieldsToPayload(payload, searchFields, qsmState);
71
103
  onSubmit(payload);
104
+ console.log('Submitted QSM data:', payload);
72
105
  };
73
106
 
74
- const addSearchFieldsToPayload = (payload: any, fields: FieldConfig[], state: QSMState) => {
107
+ const addSearchFieldsToPayload = (payload: any, fields: BaseFieldConfig[], state: QSMState) => {
75
108
  fields.forEach((field) => {
76
- if (field.type === 'single') {
77
- const key = field.fieldKey;
78
- const option = field.options.find((opt) => opt.value === state[key]);
79
- payload[key] = option ?? state[key];
80
- }
81
-
82
- if (field.type === 'double' && field.fields) {
83
- // recursively add each nested field
84
- field.fields.forEach((nestedField) => {
85
- const key = nestedField.fieldKey;
86
- const option = nestedField.options.find((opt) => opt.value === state[key]);
87
- payload[key] = option ?? state[key];
88
- });
89
- }
109
+ const fieldKey = field.fieldKey;
110
+ const option = field.options.find((opt) => opt.value === state[fieldKey]);
111
+ payload[fieldKey] = option?.key ?? state[fieldKey];
112
+
113
+ // if (field.type === 'double' && field.fields) {
114
+ // // recursively add each nested field
115
+ // field.fields.forEach((nestedField) => {
116
+ // const key = nestedField.fieldKey;
117
+ // const option = nestedField.options.find((opt) => opt.value === state[key]);
118
+ // payload[key] = option ?? state[key];
119
+ // });
120
+ // }
90
121
  });
91
122
  };
92
123
 
124
+ const originDestinationField = useMemo<DoubleFieldConfig | undefined>(() => {
125
+ if (!fromDate || !toDate) return undefined;
126
+ const searchFields = [departureAirport, destinationAirport].filter((field): field is BaseFieldConfig => field !== undefined);
127
+ return {
128
+ type: 'double',
129
+ fieldKey: 'locationGroup',
130
+ showReverse: true,
131
+ fields: searchFields
132
+ };
133
+ }, [fromDate, toDate, departureAirport, destinationAirport]);
134
+
135
+ const openJawReturnDestinationField = useMemo<DoubleFieldConfig | undefined>(() => {
136
+ if (!fromDate || !toDate || !allowOpenJaw || !departureAirport || !returnAirport) return undefined;
137
+
138
+ const mirroredDepartureField: BaseFieldConfig = {
139
+ ...departureAirport,
140
+ label: 'Bestemming',
141
+ placeholder: 'Kies bestemming'
142
+ };
143
+
144
+ return {
145
+ type: 'double',
146
+ fieldKey: 'openjawLocationGroup',
147
+ showReverse: false,
148
+ disableReturnField: true,
149
+ fields: [returnAirport, mirroredDepartureField]
150
+ };
151
+ }, [fromDate, toDate, departureAirport, returnAirport, allowOpenJaw]);
152
+
93
153
  return (
94
154
  <div className="qsm">
95
155
  <div className="qsm__content">
96
156
  <div className="qsm__tabs">
97
- <button type="button" className="qsm__tab">
157
+ <button
158
+ type="button"
159
+ className={`qsm__tab ${qsmType == 'multidestination' ? 'qsm__tab--active' : ''}`}
160
+ onClick={() => handleQsmTypeChange('multidestination')}>
98
161
  <span className="qsm__tab__icons">
99
162
  <Icon name="ui-location" height={16} />
100
163
  </span>
101
164
  {translations.QSM.MULTIDESTINATION}
102
165
  </button>
103
- <button type="button" className="qsm__tab">
166
+ <button type="button" className={`qsm__tab ${qsmType == 'package' ? 'qsm__tab--active' : ''}`} onClick={() => handleQsmTypeChange('package')}>
104
167
  <span className="qsm__tab__icons">
105
168
  <Icon name="ui-suitcase" height={16} />
106
169
  </span>
107
170
  {translations.QSM.PACKAGES}
108
171
  </button>
109
- <button type="button" className="qsm__tab qsm__tab--active">
172
+ <button
173
+ type="button"
174
+ className={`qsm__tab ${qsmType == 'hotel-flight' ? 'qsm__tab--active' : ''}`}
175
+ onClick={() => handleQsmTypeChange('hotel-flight')}>
110
176
  <span className="qsm__tab__icons">
111
177
  <Icon name="ui-backforward" height={14} />
112
178
  +
@@ -114,37 +180,37 @@ const QSMContainer: React.FC = () => {
114
180
  </span>
115
181
  {translations.QSM.TRANSPORT_HOTEL}
116
182
  </button>
117
- <button type="button" className="qsm__tab">
183
+ <button type="button" className={`qsm__tab ${qsmType == 'hotel' ? 'qsm__tab--active' : ''}`} onClick={() => handleQsmTypeChange('hotel')}>
118
184
  <span className="qsm__tab__icons">
119
185
  <Icon name="ui-bed" height={16} />
120
186
  </span>
121
187
  {translations.QSM.ACCOMMODATION}
122
188
  </button>
123
- <button type="button" className="qsm__tab">
189
+ <button type="button" className={`qsm__tab ${qsmType == 'flight' ? 'qsm__tab--active' : ''}`} onClick={() => handleQsmTypeChange('flight')}>
124
190
  <span className="qsm__tab__icons">
125
191
  <Icon name="ui-flight" height={16} />
126
192
  </span>
127
193
  {translations.QSM.TRANSPORTS}
128
194
  </button>
129
- <button type="button" className="qsm__tab">
195
+ <button type="button" className={`qsm__tab ${qsmType == 'ticket' ? 'qsm__tab--active' : ''}`} onClick={() => handleQsmTypeChange('ticket')}>
130
196
  <span className="qsm__tab__icons">
131
197
  <Icon name="ui-ticket" height={16} />
132
198
  </span>
133
199
  {translations.QSM.TICKET_ONLY}
134
200
  </button>
135
- <button type="button" className="qsm__tab">
201
+ <button type="button" className={`qsm__tab ${qsmType == 'car' ? 'qsm__tab--active' : ''}`} onClick={() => handleQsmTypeChange('car')}>
136
202
  <span className="qsm__tab__icons">
137
203
  <Icon name="ui-car" height={16} />
138
204
  </span>
139
205
  {translations.QSM.RENT_A_CAR}
140
206
  </button>
141
- <button type="button" className="qsm__tab">
207
+ <button type="button" className={`qsm__tab ${qsmType == 'transfers' ? 'qsm__tab--active' : ''}`} onClick={() => handleQsmTypeChange('transfers')}>
142
208
  <span className="qsm__tab__icons">
143
209
  <Icon name="ui-backforward" height={16} />
144
210
  </span>
145
211
  {translations.QSM.TRANSFERS}
146
212
  </button>
147
- <button type="button" className="qsm__tab">
213
+ <button type="button" className={`qsm__tab ${qsmType == 'cruises' ? 'qsm__tab--active' : ''}`} onClick={() => handleQsmTypeChange('cruises')}>
148
214
  <span className="qsm__tab__icons">
149
215
  <Icon name="ui-ship" height={16} />
150
216
  </span>
@@ -152,52 +218,108 @@ const QSMContainer: React.FC = () => {
152
218
  </button>
153
219
  </div>
154
220
  <div className="qsm__filter">
155
- <div className="radiobutton-group qsm__filter__inputgroup">
156
- <div className="radiobutton">
157
- <label className="radiobutton__label">
158
- <input
159
- type="radio"
160
- name="mainBookerId"
161
- // onChange={handleMainBookerChange}
162
- // onBlur={formik.handleBlur}
163
- value=""
164
- // checked={formik.values.mainBookerId === travelerValues.id}
165
- checked={true}
166
- readOnly
167
- className="radiobutton__input"
168
- />
169
- <span>{translations.QSM.ONE_ACCOMMODATION}</span>
170
- </label>
171
- </div>
172
- <div className="radiobutton">
173
- <label className="radiobutton__label">
174
- <input
175
- type="radio"
176
- name="mainBookerId"
177
- // onChange={handleMainBookerChange}
178
- // onBlur={formik.handleBlur}
179
- value=""
180
- // checked={formik.values.mainBookerId === travelerValues.id}
181
- className="radiobutton__input"
182
- disabled={true}
183
- />
184
- <span>{translations.QSM.MULTIPLE_ACCOMMODATIONS}</span>
185
- </label>
221
+ {qsmType === 'hotel' ||
222
+ (qsmType == 'hotel-flight' && (
223
+ <div className="radiobutton-group qsm__filter__inputgroup">
224
+ <div className="radiobutton">
225
+ <label className="radiobutton__label">
226
+ <input
227
+ type="radio"
228
+ name="numberOfAccommodations"
229
+ // onChange={handleMainBookerChange}
230
+ // onBlur={formik.handleBlur}
231
+ value=""
232
+ checked={true}
233
+ readOnly
234
+ className="radiobutton__input"
235
+ />
236
+ <span>{translations.QSM.ONE_ACCOMMODATION}</span>
237
+ </label>
238
+ </div>
239
+ <div className="radiobutton">
240
+ <label className="radiobutton__label">
241
+ <input
242
+ type="radio"
243
+ name="numberOfAccommodations"
244
+ // onChange={handleMainBookerChange}
245
+ // onBlur={formik.handleBlur}
246
+ value=""
247
+ className="radiobutton__input"
248
+ disabled={true}
249
+ />
250
+ <span>{translations.QSM.MULTIPLE_ACCOMMODATIONS}</span>
251
+ </label>
252
+ </div>
253
+ </div>
254
+ ))}
255
+ {qsmType === 'flight' && (
256
+ <div className="radiobutton-group qsm__filter__inputgroup">
257
+ {allowOneWay && (
258
+ <div className="radiobutton">
259
+ <label className="radiobutton__label">
260
+ <input
261
+ type="radio"
262
+ name="tripType"
263
+ value="oneway"
264
+ checked={tripType === 'oneway'}
265
+ onChange={() => handleTripTypeChange('oneway')}
266
+ className="radiobutton__input"
267
+ />
268
+ <span>One way</span>
269
+ </label>
270
+ </div>
271
+ )}
272
+
273
+ {allowRoundtrip && (
274
+ <div className="radiobutton">
275
+ <label className="radiobutton__label">
276
+ <input
277
+ type="radio"
278
+ name="tripType"
279
+ value="roundtrip"
280
+ checked={tripType === 'roundtrip'}
281
+ onChange={() => handleTripTypeChange('roundtrip')}
282
+ className="radiobutton__input"
283
+ />
284
+ <span>Roundtrip</span>
285
+ </label>
286
+ </div>
287
+ )}
288
+
289
+ {allowOpenJaw && (
290
+ <div className="radiobutton">
291
+ <label className="radiobutton__label">
292
+ <input
293
+ type="radio"
294
+ name="tripType"
295
+ value="openjaw"
296
+ checked={tripType === 'openjaw'}
297
+ onChange={() => handleTripTypeChange('openjaw')}
298
+ className="radiobutton__input"
299
+ />
300
+ <span>Open Jaw</span>
301
+ </label>
302
+ </div>
303
+ )}
186
304
  </div>
187
- </div>
305
+ )}
188
306
  <div className="qsm__filter__classgroup">
189
- <TravelClassPicker />
190
- <TravelTypePicker />
191
- <TravelNationalityPicker />
307
+ {qsmType !== 'hotel' && qsmType !== 'car' && qsmType !== 'ticket' && qsmType !== 'cruises' && qsmType !== 'transfers' && askTravelClass && (
308
+ <TravelClassPicker />
309
+ )}
310
+ {qsmType !== 'multidestination' && qsmType !== 'car' && qsmType !== 'flight' && qsmType !== 'transfers' && askTravelType && <TravelTypePicker />}
311
+ {askNationality && <TravelNationalityPicker />}
192
312
  </div>
193
313
  </div>
194
314
  <div className="qsm__input-group">
195
- {searchFields.map((field, idx) => {
196
- if (field.type === 'double') {
197
- return <DoubleSearchInputGroup key={idx} fieldKey={field.fieldKey} showReverse={field.showReverse} />;
198
- }
199
- return <SearchInputGroup key={idx} fieldKey={field.fieldKey} />;
200
- })}
315
+ {/* TODO, determine which fields to show for what type of QSM */}
316
+ {(qsmType == 'flight' || qsmType == 'hotel-flight') && originDestinationField && (
317
+ <DoubleSearchInputGroup fieldConfig={originDestinationField} showReverse={originDestinationField.showReverse} />
318
+ )}
319
+ {qsmType == 'flight' && tripType == 'openjaw' && openJawReturnDestinationField && (
320
+ <DoubleSearchInputGroup fieldConfig={openJawReturnDestinationField} showReverse={openJawReturnDestinationField.showReverse} />
321
+ )}
322
+ {qsmType == 'hotel' && destination && <SearchInputGroup fieldConfig={destination} />}
201
323
 
202
324
  <Dates value={dateRange} onChange={handleDateChange} />
203
325
 
@@ -1,92 +1,25 @@
1
- import React, { useContext, useMemo, useEffect } from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { useDispatch, useSelector } from 'react-redux';
3
3
  import { QSMRootState } from '../../store/qsm-store';
4
4
  import { setFieldValue } from '../../store/qsm-slice';
5
- import QSMConfigurationContext from '../../qsm-configuration-context';
6
5
  import SearchInputGroup from '../search-input-group';
7
6
  import { FieldConfig, DoubleFieldConfig } from '../../types';
8
7
 
9
8
  const isDouble = (f: FieldConfig): f is DoubleFieldConfig => f.type === 'double';
10
9
 
11
10
  interface Props {
12
- fieldKey: string;
11
+ fieldConfig: DoubleFieldConfig;
13
12
  showReverse?: boolean;
14
13
  }
15
14
 
16
- const DoubleSearchInputGroup: React.FC<Props> = ({ fieldKey, showReverse = false }) => {
17
- const { searchFields, allowOpenJaw = false } = useContext(QSMConfigurationContext);
15
+ const DoubleSearchInputGroup: React.FC<Props> = ({ fieldConfig, showReverse = false }) => {
18
16
  const dispatch = useDispatch();
19
- const cfg = searchFields.find((f) => f.fieldKey === fieldKey);
20
- if (!cfg || !isDouble(cfg)) {
21
- return null;
22
- }
23
-
24
- const maybeFields = cfg.fields as unknown as any[];
25
- const isOpenJaw = allowOpenJaw && maybeFields.length === 4;
26
-
27
- if (isOpenJaw) {
28
- const [from1Cfg, to1Cfg, from2Cfg, to2Cfg] = maybeFields;
29
-
30
- const from1Val = useSelector((s: QSMRootState) => (s.qsm as any)[from1Cfg.fieldKey] ?? '');
31
- const to2Val = useSelector((s: QSMRootState) => (s.qsm as any)[to2Cfg.fieldKey] ?? '');
32
-
33
- // keep last "to" in sync with first "from"
34
- useEffect(() => {
35
- if (from1Val && from1Val !== to2Val) {
36
- dispatch(
37
- setFieldValue({
38
- fieldKey: to2Cfg.fieldKey,
39
- value: from1Val
40
- })
41
- );
42
- }
43
- if (!from1Val && to2Val) {
44
- dispatch(
45
- setFieldValue({
46
- fieldKey: to2Cfg.fieldKey,
47
- value: ''
48
- })
49
- );
50
- }
51
- }, [from1Val, to2Val, dispatch, to2Cfg.fieldKey]);
52
17
 
53
- return (
54
- <>
55
- {/* Row 1 : A ➜ B */}
56
- <div className="qsm__double-input qsm__double-input--splittable">
57
- <div className="qsm__input-wrapper qsm__input-wrapper--splittable">
58
- <SearchInputGroup fieldKey={from1Cfg.fieldKey} isDoubleInput />
59
- </div>
60
- <div className="qsm__reverse-wrapper">
61
- <div className="qsm__input-line qsm__input-line--splittable" />
62
- </div>
63
- <div className="qsm__input-wrapper qsm__input-wrapper--splittable">
64
- <SearchInputGroup fieldKey={to1Cfg.fieldKey} isSecondInput isDoubleInput />
65
- </div>
66
- </div>
67
-
68
- {/* Row 2 : C ➜ A (A is readonly) */}
69
- <div className="qsm__double-input qsm__double-input--splittable u-mt-2">
70
- <div className="qsm__input-wrapper qsm__input-wrapper--splittable">
71
- <SearchInputGroup fieldKey={from2Cfg.fieldKey} isDoubleInput />
72
- </div>
73
- <div className="qsm__reverse-wrapper">
74
- <div className="qsm__input-line qsm__input-line--splittable" />
75
- </div>
76
- <div className="qsm__input-wrapper qsm__input-wrapper--splittable">
77
- <SearchInputGroup fieldKey={to2Cfg.fieldKey} isSecondInput isDoubleInput readOnlyForced={Boolean(from1Val)} />
78
- </div>
79
- </div>
80
- </>
81
- );
18
+ if (!fieldConfig || !isDouble(fieldConfig)) {
19
+ return null;
82
20
  }
83
21
 
84
- /*
85
- * ──────────────────────────────────────────────────────────────
86
- * CLASSIC PATH (2 sub‑fields, original behaviour)
87
- * ──────────────────────────────────────────────────────────────
88
- */
89
- const [firstCfg, secondCfg] = cfg.fields;
22
+ const [firstCfg, secondCfg] = fieldConfig.fields;
90
23
 
91
24
  const firstSelector = useMemo(() => (s: QSMRootState) => (s.qsm as any)[firstCfg.fieldKey] ?? '', [firstCfg.fieldKey]);
92
25
  const secondSelector = useMemo(() => (s: QSMRootState) => (s.qsm as any)[secondCfg.fieldKey] ?? '', [secondCfg.fieldKey]);
@@ -111,7 +44,7 @@ const DoubleSearchInputGroup: React.FC<Props> = ({ fieldKey, showReverse = false
111
44
  return (
112
45
  <div className="qsm__double-input qsm__double-input--splittable">
113
46
  <div className="qsm__input-wrapper qsm__input-wrapper--splittable">
114
- <SearchInputGroup fieldKey={firstCfg.fieldKey} isDoubleInput />
47
+ <SearchInputGroup fieldConfig={firstCfg} isDoubleInput />
115
48
  </div>
116
49
 
117
50
  {showReverse ? (
@@ -130,7 +63,13 @@ const DoubleSearchInputGroup: React.FC<Props> = ({ fieldKey, showReverse = false
130
63
  )}
131
64
 
132
65
  <div className="qsm__input-wrapper qsm__input-wrapper--splittable">
133
- <SearchInputGroup fieldKey={secondCfg.fieldKey} isSecondInput isDoubleInput />
66
+ <SearchInputGroup
67
+ fieldConfig={secondCfg}
68
+ isSecondInput
69
+ isDoubleInput
70
+ isDisabled={fieldConfig.disableReturnField}
71
+ readOnlyForced={fieldConfig.disableReturnField}
72
+ />
134
73
  </div>
135
74
  </div>
136
75
  );
@@ -15,14 +15,25 @@ import DateRangePicker from '../date-range-picker';
15
15
  import TravelInput from '../travel-input';
16
16
  import { format } from 'date-fns';
17
17
  import QSMConfigurationContext from '../../qsm-configuration-context';
18
- import { BaseFieldConfig, FieldConfig, TypeaheadOption } from '../../types';
18
+ import { BaseFieldConfig, TypeaheadOption } from '../../types';
19
19
  import { getTranslations } from '../../../shared/utils/localization-util';
20
20
 
21
21
  const MobileFilterModal: React.FC = () => {
22
- const { datesIcon, languageCode, searchFields } = useContext(QSMConfigurationContext);
22
+ const { datesIcon, languageCode } = useContext(QSMConfigurationContext);
23
23
  const translations = getTranslations(languageCode ?? 'en-GB');
24
24
  const dispatch = useDispatch();
25
- const { mobileFilterType, mobileDatePickerMode, activeSearchFieldProps, fromDate, toDate, searchResults } = useSelector((state: QSMRootState) => state.qsm);
25
+ const {
26
+ mobileFilterType,
27
+ mobileDatePickerMode,
28
+ activeSearchFieldProps,
29
+ fromDate,
30
+ toDate,
31
+ searchResults,
32
+ departureAirport,
33
+ destinationAirport,
34
+ returnAirport,
35
+ destination
36
+ } = useSelector((state: QSMRootState) => state.qsm);
26
37
 
27
38
  const [inputValue, setInputValue] = useState('');
28
39
 
@@ -45,24 +56,20 @@ const MobileFilterModal: React.FC = () => {
45
56
  dispatch(closeMobileFilter());
46
57
  };
47
58
 
48
- const findConfig = (all: FieldConfig[], key: string): BaseFieldConfig | undefined => {
59
+ const findConfig = (all: BaseFieldConfig[], key: string): BaseFieldConfig | undefined => {
49
60
  for (const config of all) {
50
- if (config.type === 'single' && config.fieldKey === key) {
61
+ if (config.fieldKey === key) {
51
62
  return config;
52
63
  }
53
- if (config.type === 'double') {
54
- const field = config.fields.find((x) => x.fieldKey === key);
55
- if (field) return field;
56
- }
57
64
  }
58
65
  return undefined;
59
66
  };
60
67
 
61
68
  const config = useMemo<BaseFieldConfig | undefined>(() => {
62
69
  if (!activeSearchFieldProps) return undefined;
63
-
70
+ const searchFields = [departureAirport, destinationAirport, returnAirport, destination].filter((field): field is BaseFieldConfig => field !== undefined);
64
71
  return findConfig(searchFields, activeSearchFieldProps.fieldKey);
65
- }, [searchFields, activeSearchFieldProps]);
72
+ }, [departureAirport, destinationAirport, returnAirport, destination, activeSearchFieldProps]);
66
73
 
67
74
  const match = useCallback(
68
75
  (input: string) => {
@@ -10,9 +10,13 @@ interface SearchInputProps {
10
10
  label: string;
11
11
  isSecondInput?: boolean;
12
12
  isDoubleInput?: boolean;
13
+ isDisabled?: boolean;
13
14
  }
14
15
 
15
- const SearchInput: React.FC<SearchInputProps> = ({ searchResults, onOptionSelect, highlightTarget, label, isSecondInput, isDoubleInput }) => {
16
+ const SearchInput: React.FC<SearchInputProps> = ({ searchResults, onOptionSelect, highlightTarget, label, isSecondInput, isDoubleInput, isDisabled }) => {
17
+ if (isDisabled) {
18
+ return null;
19
+ }
16
20
  const highlightMatch = (option: TypeaheadOption, highlight: string) => {
17
21
  if (!highlight) {
18
22
  return option.value;
@@ -47,7 +51,10 @@ const SearchInput: React.FC<SearchInputProps> = ({ searchResults, onOptionSelect
47
51
  <div
48
52
  key={index}
49
53
  className="qsm__double-input-option"
50
- onMouseDown={(e) => e.preventDefault()}
54
+ onMouseDown={(e) => {
55
+ e.preventDefault();
56
+ e.stopPropagation();
57
+ }}
51
58
  onClick={(e) => {
52
59
  e.stopPropagation();
53
60
  onOptionSelect(option);