@qite/tide-booking-component 1.4.93 → 1.4.95

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 (181) hide show
  1. package/.prettierrc +9 -9
  2. package/.vs/ProjectSettings.json +3 -3
  3. package/.vs/VSWorkspaceState.json +5 -5
  4. package/build/build-cjs/index.js +81 -27
  5. package/build/build-cjs/src/booking-wizard/features/booking/booking-slice.d.ts +2 -1
  6. package/build/build-cjs/src/booking-wizard/features/booking/selectors.d.ts +4 -3
  7. package/build/build-cjs/src/booking-wizard/features/price-details/price-details-slice.d.ts +1 -0
  8. package/build/build-cjs/src/booking-wizard/features/price-details/selectors.d.ts +1 -0
  9. package/build/build-cjs/src/booking-wizard/features/sidebar/sidebar.d.ts +1 -0
  10. package/build/build-cjs/src/booking-wizard/types.d.ts +1 -0
  11. package/build/build-cjs/src/shared/utils/localization-util.d.ts +1 -0
  12. package/build/build-esm/index.js +81 -27
  13. package/build/build-esm/src/booking-wizard/features/booking/booking-slice.d.ts +2 -1
  14. package/build/build-esm/src/booking-wizard/features/booking/selectors.d.ts +4 -3
  15. package/build/build-esm/src/booking-wizard/features/price-details/price-details-slice.d.ts +1 -0
  16. package/build/build-esm/src/booking-wizard/features/price-details/selectors.d.ts +1 -0
  17. package/build/build-esm/src/booking-wizard/features/sidebar/sidebar.d.ts +1 -0
  18. package/build/build-esm/src/booking-wizard/types.d.ts +1 -0
  19. package/build/build-esm/src/shared/utils/localization-util.d.ts +1 -0
  20. package/package.json +83 -83
  21. package/src/booking-product/components/age-select.tsx +35 -35
  22. package/src/booking-product/components/amount-input.tsx +51 -51
  23. package/src/booking-product/components/date-range-picker/calendar.tsx +155 -155
  24. package/src/booking-product/components/footer.tsx +54 -54
  25. package/src/booking-product/components/header.tsx +57 -57
  26. package/src/booking-product/components/icon.tsx +200 -200
  27. package/src/booking-product/components/list-view.tsx +54 -54
  28. package/src/booking-product/components/rating.tsx +21 -21
  29. package/src/booking-product/components/rooms.tsx +171 -171
  30. package/src/booking-product/constants.ts +1 -1
  31. package/src/booking-product/index.tsx +21 -21
  32. package/src/booking-product/settings-context.ts +16 -16
  33. package/src/booking-product/types.ts +30 -30
  34. package/src/booking-product/utils/api.ts +26 -26
  35. package/src/booking-product/utils/price.ts +28 -28
  36. package/src/booking-wizard/api-settings-slice.ts +24 -24
  37. package/src/booking-wizard/components/icon.tsx +398 -398
  38. package/src/booking-wizard/components/labeled-input.tsx +56 -56
  39. package/src/booking-wizard/components/labeled-select.tsx +54 -54
  40. package/src/booking-wizard/components/message.tsx +21 -21
  41. package/src/booking-wizard/components/multi-range-filter.tsx +99 -99
  42. package/src/booking-wizard/components/phone-input.tsx +146 -146
  43. package/src/booking-wizard/components/print-offer-button.tsx +53 -53
  44. package/src/booking-wizard/components/product-card.tsx +23 -23
  45. package/src/booking-wizard/declarations.d.ts +4 -4
  46. package/src/booking-wizard/features/booking/booking-self-contained.tsx +16 -1
  47. package/src/booking-wizard/features/booking/booking-slice.ts +9 -1
  48. package/src/booking-wizard/features/booking/booking.tsx +16 -1
  49. package/src/booking-wizard/features/booking/selectors.ts +5 -0
  50. package/src/booking-wizard/features/flight-options/flight-filter.tsx +371 -371
  51. package/src/booking-wizard/features/flight-options/flight-option-flight.tsx +354 -354
  52. package/src/booking-wizard/features/flight-options/flight-option-modal.tsx +211 -211
  53. package/src/booking-wizard/features/flight-options/flight-option.tsx +57 -57
  54. package/src/booking-wizard/features/flight-options/flight-utils.ts +423 -423
  55. package/src/booking-wizard/features/price-details/price-details-api.ts +20 -20
  56. package/src/booking-wizard/features/price-details/price-details-slice.ts +2 -0
  57. package/src/booking-wizard/features/price-details/selectors.ts +1 -0
  58. package/src/booking-wizard/features/price-details/util.ts +115 -115
  59. package/src/booking-wizard/features/product-options/no-options.tsx +18 -18
  60. package/src/booking-wizard/features/product-options/none-option.tsx +73 -73
  61. package/src/booking-wizard/features/product-options/option-booking-airline-group.tsx +53 -53
  62. package/src/booking-wizard/features/product-options/option-booking-group.tsx +152 -152
  63. package/src/booking-wizard/features/product-options/option-item.tsx +236 -236
  64. package/src/booking-wizard/features/product-options/option-pax-card.tsx +159 -159
  65. package/src/booking-wizard/features/product-options/option-pax-group.tsx +122 -122
  66. package/src/booking-wizard/features/product-options/option-room.tsx +226 -226
  67. package/src/booking-wizard/features/product-options/option-unit-group.tsx +138 -138
  68. package/src/booking-wizard/features/room-options/room-utils.ts +154 -154
  69. package/src/booking-wizard/features/room-options/room.tsx +123 -123
  70. package/src/booking-wizard/features/room-options/traveler-rooms.tsx +64 -64
  71. package/src/booking-wizard/features/sidebar/index.tsx +2 -0
  72. package/src/booking-wizard/features/sidebar/sidebar-flight.tsx +66 -66
  73. package/src/booking-wizard/features/sidebar/sidebar.tsx +17 -1
  74. package/src/booking-wizard/features/summary/summary-booking-option-pax.tsx +23 -23
  75. package/src/booking-wizard/features/summary/summary-booking-option-unit.tsx +23 -23
  76. package/src/booking-wizard/features/summary/summary-flight.tsx +36 -36
  77. package/src/booking-wizard/features/summary/summary-per-booking-option-group.tsx +60 -60
  78. package/src/booking-wizard/features/summary/summary-per-pax-option-group.tsx +56 -56
  79. package/src/booking-wizard/features/summary/summary-per-unit-option-group.tsx +58 -58
  80. package/src/booking-wizard/features/summary/summary-slice.ts +27 -27
  81. package/src/booking-wizard/features/travelers-form/travelers-form-slice.ts +157 -157
  82. package/src/booking-wizard/features/travelers-form/travelers-form-util.ts +10 -10
  83. package/src/booking-wizard/features/travelers-form/type-ahead-input.tsx +85 -85
  84. package/src/booking-wizard/features/travelers-form/validate-form.ts +178 -178
  85. package/src/booking-wizard/index.tsx +27 -27
  86. package/src/booking-wizard/store.ts +26 -26
  87. package/src/booking-wizard/types.ts +1 -0
  88. package/src/booking-wizard/use-offer-printer.ts +108 -108
  89. package/src/content/components/LanguageSwitcher.tsx +158 -158
  90. package/src/content/components/accordion.tsx +30 -30
  91. package/src/content/components/contact.tsx +211 -211
  92. package/src/content/components/personal-contact-form.tsx +809 -809
  93. package/src/content/header/index.tsx +43 -43
  94. package/src/content/header/types.ts +26 -26
  95. package/src/qsm/components/date-picker/index.tsx +152 -152
  96. package/src/qsm/components/date-range-picker/calendar-day.tsx +49 -49
  97. package/src/qsm/components/date-range-picker/calendar.tsx +211 -211
  98. package/src/qsm/components/date-range-picker/index.tsx +404 -404
  99. package/src/qsm/index.tsx +26 -26
  100. package/src/qsm/store/qsm-store.ts +13 -13
  101. package/src/search-results/components/flight/flight-card.tsx +38 -38
  102. package/src/search-results/components/flight/flight-leg.tsx +61 -61
  103. package/src/search-results/components/flight/flight-path.tsx +23 -23
  104. package/src/search-results/components/multi-range-filter.tsx +104 -104
  105. package/src/search-results/components/search-results-container/search-results-container.tsx +2 -2
  106. package/src/search-results/index.tsx +24 -24
  107. package/src/search-results/search-results-configuration-context.ts +6 -6
  108. package/src/search-results/store/search-results-store.ts +13 -13
  109. package/src/shared/components/loader.tsx +16 -16
  110. package/src/shared/translations/ar-SA.json +2 -1
  111. package/src/shared/translations/da-DK.json +2 -1
  112. package/src/shared/translations/de-DE.json +2 -1
  113. package/src/shared/translations/en-GB.json +2 -1
  114. package/src/shared/translations/es-ES.json +2 -1
  115. package/src/shared/translations/fr-BE.json +2 -1
  116. package/src/shared/translations/fr-FR.json +2 -1
  117. package/src/shared/translations/is-IS.json +2 -1
  118. package/src/shared/translations/it-IT.json +2 -1
  119. package/src/shared/translations/ja-JP.json +2 -1
  120. package/src/shared/translations/nl-BE.json +2 -1
  121. package/src/shared/translations/nl-NL.json +2 -1
  122. package/src/shared/translations/no-NO.json +2 -1
  123. package/src/shared/translations/pl-PL.json +2 -1
  124. package/src/shared/translations/pt-PT.json +2 -1
  125. package/src/shared/translations/sv-SE.json +2 -1
  126. package/src/shared/utils/class-util.ts +7 -7
  127. package/src/shared/utils/query-string-util.ts +91 -91
  128. package/src/shared/utils/tide-api-utils.ts +34 -34
  129. package/src/shared/utils/use-media-query-util.ts +19 -19
  130. package/styles/abstracts/_mixins.scss +74 -74
  131. package/styles/abstracts/_variables.scss +57 -57
  132. package/styles/base/_fonts.scss +2 -2
  133. package/styles/base/_normalize.scss +227 -227
  134. package/styles/base/_typography.scss +35 -35
  135. package/styles/booking-joker-variables.scss +596 -596
  136. package/styles/booking-product-variables.scss +330 -330
  137. package/styles/booking-product.scss +438 -438
  138. package/styles/booking-qsm-variables.scss +501 -501
  139. package/styles/booking-qsm.scss +52 -52
  140. package/styles/booking-wizard-variables.scss +603 -603
  141. package/styles/booking-wizard.scss +61 -61
  142. package/styles/components/_accordion.scss +67 -67
  143. package/styles/components/_animations.scss +39 -39
  144. package/styles/components/_base.scss +107 -107
  145. package/styles/components/_breadcrumb.scss +92 -92
  146. package/styles/components/_button.scss +238 -238
  147. package/styles/components/_checkbox.scss +230 -230
  148. package/styles/components/_contact.scss +239 -239
  149. package/styles/components/_cta.scss +238 -238
  150. package/styles/components/_date-list.scss +41 -41
  151. package/styles/components/_date-range-picker.scss +223 -223
  152. package/styles/components/_decrement-increment.scss +35 -35
  153. package/styles/components/_dropdown.scss +72 -72
  154. package/styles/components/_faq.scss +27 -27
  155. package/styles/components/_flight-option.scss +1419 -1419
  156. package/styles/components/_gallery.scss +314 -314
  157. package/styles/components/_header.scss +113 -113
  158. package/styles/components/_img-slider.scss +175 -175
  159. package/styles/components/_info-message.scss +75 -75
  160. package/styles/components/_input.scss +35 -35
  161. package/styles/components/_list.scss +185 -185
  162. package/styles/components/_loader.scss +70 -70
  163. package/styles/components/_mixins.scss +579 -579
  164. package/styles/components/_passenger-picker.scss +306 -306
  165. package/styles/components/_phone-input.scss +8 -8
  166. package/styles/components/_placeholders.scss +165 -165
  167. package/styles/components/_qsm.scss +17 -17
  168. package/styles/components/_radiobutton.scss +170 -170
  169. package/styles/components/_select-wrapper.scss +76 -76
  170. package/styles/components/_slider.scss +128 -128
  171. package/styles/components/_spinner.scss +29 -29
  172. package/styles/components/_step-indicators.scss +161 -161
  173. package/styles/components/_table.scss +81 -81
  174. package/styles/components/_typeahead.scss +275 -275
  175. package/styles/components/_variables.scss +89 -89
  176. package/styles/content-blocks-variables.scss +507 -507
  177. package/styles/font.scss +2 -2
  178. package/styles/qsm/_calendar.scss +274 -274
  179. package/styles/qsm/_qsm.scss +1094 -1094
  180. package/styles/search.scss +1200 -1200
  181. package/tsconfig.json +24 -24
@@ -1,809 +1,809 @@
1
- import React, { useMemo, useState } from 'react';
2
- import Icon from './icon';
3
-
4
- type ContactValues = {
5
- firstName: string;
6
- lastName: string;
7
- dateOfBirth: string;
8
- nationality: string;
9
- email: string;
10
- phone: string;
11
- message: string;
12
- street: string;
13
- houseNumber: string;
14
- box: string;
15
- postalCode: string;
16
- city: string;
17
- country: string;
18
- // traveler 1
19
- bookingType1: 'leisure' | 'business' | '';
20
- gender1: 'female' | 'male' | 'other' | '';
21
-
22
- // traveler 2
23
- bookingType2: 'leisure' | 'business' | '';
24
- gender2: 'female' | 'male' | 'other' | '';
25
- };
26
-
27
- type ContactErrors = Partial<Record<keyof ContactValues, string>>;
28
- // const [touched, setTouched] = useState<Partial<Record<keyof ContactValues, boolean>>>({});
29
-
30
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
31
- const phoneRegex = /^[+()\-.\s0-9]{8,20}$/;
32
-
33
- // Small helper: checks YYYY-MM-DD parses to a real date
34
- const isValidISODate = (value: string) => {
35
- if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
36
- const d = new Date(value);
37
- return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === value;
38
- };
39
-
40
- // You can expand this list later (or load from API)
41
- const nationalityOptions = [
42
- { value: '', label: 'Selecteer nationaliteit' },
43
- { value: 'BE', label: 'Belgisch' },
44
- { value: 'NL', label: 'Nederlands' },
45
- { value: 'FR', label: 'Frans' },
46
- { value: 'DE', label: 'Duits' },
47
- { value: 'UK', label: 'Brits' },
48
- { value: 'US', label: 'Amerikaans' }
49
- ];
50
-
51
- const countryOptions = [
52
- { value: '', label: 'Selecteer land' },
53
- { value: 'BE', label: 'België' },
54
- { value: 'NL', label: 'Nederland' },
55
- { value: 'FR', label: 'Frankrijk' },
56
- { value: 'DE', label: 'Duitsland' },
57
- { value: 'LU', label: 'Luxemburg' },
58
- { value: 'UK', label: 'Verenigd Koninkrijk' }
59
- ];
60
-
61
- const PersonalContactForm: React.FC = () => {
62
- const [values, setValues] = useState<ContactValues>({
63
- firstName: '',
64
- lastName: '',
65
- dateOfBirth: '',
66
- nationality: '',
67
- email: '',
68
- phone: '',
69
- message: '',
70
- street: '',
71
- houseNumber: '',
72
- box: '',
73
- postalCode: '',
74
- city: '',
75
- country: '',
76
- bookingType1: '',
77
- gender1: '',
78
- bookingType2: '',
79
- gender2: ''
80
- });
81
-
82
- const [touched, setTouched] = useState<Partial<Record<keyof ContactValues, boolean>>>({});
83
- const [errors, setErrors] = useState<ContactErrors>({});
84
- const [submitted, setSubmitted] = useState(false);
85
- const [isNationalityOpen, setIsNationalityOpen] = useState(false);
86
- const [isCountryOpen, setIsCountryOpen] = useState(false);
87
-
88
- const postalCodeRegex = /^[0-9A-Za-z\s-]{3,10}$/;
89
-
90
- const validate = (v: ContactValues): ContactErrors => {
91
- const e: ContactErrors = {};
92
-
93
- if (!v.firstName.trim()) e.firstName = 'Voornaam is verplicht.';
94
- if (!v.lastName.trim()) e.lastName = 'Achternaam is verplicht.';
95
-
96
- if (!v.dateOfBirth.trim()) e.dateOfBirth = 'Geboortedatum is verplicht.';
97
- else if (!isValidISODate(v.dateOfBirth.trim())) e.dateOfBirth = 'Vul een geldige geboortedatum in.';
98
-
99
- if (!v.nationality.trim()) e.nationality = 'Nationaliteit is verplicht.';
100
-
101
- if (!v.email.trim()) e.email = 'Email is verplicht.';
102
- else if (!emailRegex.test(v.email.trim())) e.email = 'Vul een geldig e-mailadres in.';
103
-
104
- if (v.phone.trim() && !phoneRegex.test(v.phone.trim())) {
105
- e.phone = 'Vul een geldig telefoonnummer in.';
106
- }
107
-
108
- if (!v.message.trim()) e.message = 'Bericht is verplicht.';
109
- else if (v.message.trim().length < 10) e.message = 'Bericht moet minstens 10 tekens zijn.';
110
-
111
- if (!v.street.trim()) e.street = 'Straat is verplicht.';
112
- if (!v.houseNumber.trim()) e.houseNumber = 'Nummer is verplicht.';
113
-
114
- if (!v.postalCode.trim()) e.postalCode = 'Postcode is verplicht.';
115
- else if (!postalCodeRegex.test(v.postalCode.trim())) e.postalCode = 'Vul een geldige postcode in.';
116
-
117
- if (!v.city.trim()) e.city = 'Woonplaats is verplicht.';
118
- if (!v.country.trim()) e.country = 'Land is verplicht.';
119
-
120
- if (!v.gender1) e.gender1 = 'Geslacht is verplicht.';
121
- if (!v.gender2) e.gender2 = 'Geslacht is verplicht.';
122
-
123
- if (!v.bookingType1) e.bookingType1 = 'Type boeking is verplicht.';
124
- if (!v.bookingType2) e.bookingType2 = 'Type boeking is verplicht.';
125
-
126
- return e;
127
- };
128
-
129
- const currentErrors = useMemo(() => validate(values), [values]);
130
-
131
- const setField = <K extends keyof ContactValues>(key: K, val: ContactValues[K]) => {
132
- setValues((p) => ({ ...p, [key]: val }));
133
- if (submitted || touched[key]) {
134
- setErrors(validate({ ...values, [key]: val } as ContactValues));
135
- }
136
- };
137
-
138
- const onBlur = (key: keyof ContactValues) => {
139
- setTouched((p) => ({ ...p, [key]: true }));
140
- setErrors(validate(values));
141
- };
142
-
143
- const onSubmit = (e: React.FormEvent) => {
144
- e.preventDefault();
145
- setSubmitted(true);
146
-
147
- const eMap = validate(values);
148
- setErrors(eMap);
149
-
150
- if (Object.keys(eMap).length > 0) return;
151
-
152
- console.log('Contact form submit:', values);
153
-
154
- setValues({
155
- firstName: '',
156
- lastName: '',
157
- dateOfBirth: '',
158
- nationality: '',
159
- email: '',
160
- phone: '',
161
- message: '',
162
- street: '',
163
- houseNumber: '',
164
- box: '',
165
- postalCode: '',
166
- city: '',
167
- country: '',
168
- bookingType1: '',
169
- gender1: '',
170
- bookingType2: '',
171
- gender2: ''
172
- });
173
- setTouched({});
174
- setErrors({});
175
- setSubmitted(false);
176
- };
177
-
178
- const showError = (key: keyof ContactValues) => {
179
- const shouldShow = submitted || touched[key];
180
- return shouldShow ? errors[key] : undefined;
181
- };
182
-
183
- return (
184
- <div className="content content--background">
185
- <div className="content__container content__container--medium">
186
- <div className="content__title__row">
187
- <h2 className="content__title">Persoonlijke informatie</h2>
188
- </div>
189
-
190
- <form className="contact" noValidate onSubmit={onSubmit}>
191
- <div className="contact__card contact__card--headbooker">
192
- <div className="contact__title">
193
- <Icon name="ui-info-circle" width={24} height={24} />
194
- <h4>Gaat de hoofdboeker mee op reis?</h4>
195
- </div>
196
-
197
- <div className="contact__form">
198
- <div className="radiobutton-group">
199
- <div className="radiobutton">
200
- <label className="radiobutton__label">
201
- <input type="radio" name="headBooker" value="yes" className="radiobutton__input" defaultChecked={true} />
202
- <span>Ja</span>
203
- </label>
204
- </div>
205
-
206
- <div className="radiobutton">
207
- <label className="radiobutton__label">
208
- <input type="radio" name="headBooker" value="no" className="radiobutton__input" />
209
- <span>Nee</span>
210
- </label>
211
- </div>
212
- </div>
213
- </div>
214
- </div>
215
- <div className="contact__card">
216
- <div className="contact__title">
217
- <Icon name="ui-user" width={24} height={24} />
218
- <h4>Reiziger 1</h4>
219
- <span>volwassenen, hoofdboeker</span>
220
- </div>
221
-
222
- <div
223
- className="contact__radio"
224
- role="group"
225
- aria-labelledby="bookingType-label"
226
- aria-invalid={!!showError('bookingType1')}
227
- aria-describedby="bookingType-error">
228
- <span id="bookingType-label">Type boeking{/** add * if required */}</span>
229
-
230
- <div className="radiobutton-group">
231
- <div className="radiobutton">
232
- <label className="radiobutton__label">
233
- <input
234
- type="radio"
235
- name="bookingType1"
236
- value="leisure"
237
- className="radiobutton__input"
238
- checked={values.bookingType1 === 'leisure'}
239
- onChange={() => setField('bookingType1', 'leisure')}
240
- onBlur={() => onBlur('bookingType1')}
241
- />
242
- <span>Plezier</span>
243
- </label>
244
- </div>
245
-
246
- <div className="radiobutton">
247
- <label className="radiobutton__label">
248
- <input
249
- type="radio"
250
- name="bookingType1"
251
- value="business"
252
- className="radiobutton__input"
253
- checked={values.bookingType1 === 'business'}
254
- onChange={() => setField('bookingType1', 'business')}
255
- onBlur={() => onBlur('bookingType1')}
256
- />
257
- <span>Zakelijk</span>
258
- </label>
259
- </div>
260
- </div>
261
-
262
- {showError('bookingType1') && (
263
- <span className="contact__radio--error" id="bookingType-error">
264
- {showError('bookingType1')}
265
- </span>
266
- )}
267
- </div>
268
-
269
- <div className="contact__radio" role="group" aria-labelledby="gender-label" aria-invalid={!!showError('gender1')} aria-describedby="gender-error">
270
- <span id="gender-label">Geslacht*</span>
271
-
272
- <div className="radiobutton-group">
273
- <div className="radiobutton">
274
- <label className="radiobutton__label">
275
- <input
276
- type="radio"
277
- name="gender1"
278
- value="female"
279
- className="radiobutton__input"
280
- checked={values.gender1 === 'female'}
281
- onChange={() => setField('gender1', 'female')}
282
- onBlur={() => onBlur('gender1')}
283
- />
284
- <span>Vrouw</span>
285
- </label>
286
- </div>
287
-
288
- <div className="radiobutton">
289
- <label className="radiobutton__label">
290
- <input
291
- type="radio"
292
- name="gender1"
293
- value="male"
294
- className="radiobutton__input"
295
- checked={values.gender1 === 'male'}
296
- onChange={() => setField('gender1', 'male')}
297
- onBlur={() => onBlur('gender1')}
298
- />
299
- <span>Man</span>
300
- </label>
301
- </div>
302
-
303
- <div className="radiobutton">
304
- <label className="radiobutton__label">
305
- <input
306
- type="radio"
307
- name="gender1"
308
- value="other"
309
- className="radiobutton__input"
310
- checked={values.gender1 === 'other'}
311
- onChange={() => setField('gender1', 'other')}
312
- onBlur={() => onBlur('gender1')}
313
- />
314
- <span>Anders</span>
315
- </label>
316
- </div>
317
- </div>
318
-
319
- {showError('gender1') && (
320
- <span className="contact__radio--error" id="gender-error">
321
- {showError('gender1')}
322
- </span>
323
- )}
324
- </div>
325
-
326
- <div className="contact__form">
327
- <label className="contact__form__group contact__form__group--3">
328
- <span className="contact__form__label">Voornaam*</span>
329
- <input
330
- type="text"
331
- className="contact__input"
332
- placeholder="Enter your name"
333
- aria-label="Enter your name"
334
- value={values.firstName}
335
- onChange={(e) => setField('firstName', e.target.value)}
336
- onBlur={() => onBlur('firstName')}
337
- aria-invalid={!!showError('firstName')}
338
- aria-describedby="firstName-error"
339
- />
340
- {showError('firstName') && (
341
- <span className="contact__error" id="firstName-error">
342
- {showError('firstName')}
343
- </span>
344
- )}
345
- </label>
346
-
347
- <label className="contact__form__group contact__form__group--3">
348
- <span className="contact__form__label">Achternaam*</span>
349
- <input
350
- type="text"
351
- className="contact__input"
352
- placeholder="Enter your last name"
353
- aria-label="Enter your last name"
354
- value={values.lastName}
355
- onChange={(e) => setField('lastName', e.target.value)}
356
- onBlur={() => onBlur('lastName')}
357
- aria-invalid={!!showError('lastName')}
358
- aria-describedby="lastName-error"
359
- />
360
- {showError('lastName') && (
361
- <span className="contact__error" id="lastName-error">
362
- {showError('lastName')}
363
- </span>
364
- )}
365
- </label>
366
-
367
- <label className="contact__form__group contact__form__group--3">
368
- <span className="contact__form__label">Geboortedatum*</span>
369
- <input
370
- type="date"
371
- className="contact__input"
372
- aria-label="Enter your date of birth"
373
- value={values.dateOfBirth}
374
- onChange={(e) => setField('dateOfBirth', e.target.value)}
375
- onBlur={() => onBlur('dateOfBirth')}
376
- aria-invalid={!!showError('dateOfBirth')}
377
- aria-describedby="dateOfBirth-error"
378
- />
379
- {showError('dateOfBirth') && (
380
- <span className="contact__error" id="dateOfBirth-error">
381
- {showError('dateOfBirth')}
382
- </span>
383
- )}
384
- </label>
385
-
386
- <label className="contact__form__group contact__form__group--3">
387
- <span className="contact__form__label">Nationaliteit*</span>
388
-
389
- <div className={`contact__dropdown ${showError('nationality') ? 'contact__dropdown--error' : ''}`}>
390
- <button
391
- type="button"
392
- className="contact__dropdown__trigger"
393
- aria-haspopup="listbox"
394
- aria-expanded={isNationalityOpen}
395
- onClick={() => setIsNationalityOpen((o) => !o)}
396
- onBlur={() => {
397
- setIsNationalityOpen(false);
398
- onBlur('nationality');
399
- }}>
400
- <span>{nationalityOptions.find((o) => o.value === values.nationality)?.label ?? 'Selecteer nationaliteit'}</span>
401
-
402
- <Icon name="ui-chevron" width={16} height={16} />
403
- </button>
404
-
405
- {isNationalityOpen && (
406
- <ul className="contact__dropdown__menu" role="listbox">
407
- {nationalityOptions.map((option) => (
408
- <li key={option.value}>
409
- <button
410
- type="button"
411
- className={`contact__dropdown__option ${values.nationality === option.value ? 'contact__dropdown__option--active' : ''}`}
412
- role="option"
413
- aria-selected={values.nationality === option.value}
414
- onMouseDown={() => {
415
- // onMouseDown prevents blur-before-click
416
- setField('nationality', option.value);
417
- setIsNationalityOpen(false);
418
- }}>
419
- {option.label}
420
- </button>
421
- </li>
422
- ))}
423
- </ul>
424
- )}
425
- </div>
426
-
427
- {showError('nationality') && (
428
- <span className="contact__error" id="nationality-error">
429
- {showError('nationality')}
430
- </span>
431
- )}
432
- </label>
433
-
434
- <label className="contact__form__group contact__form__group--3">
435
- <span className="contact__form__label">Email*</span>
436
- <input
437
- type="email"
438
- className="contact__input"
439
- placeholder="Enter your email"
440
- aria-label="Enter your email"
441
- value={values.email}
442
- onChange={(e) => setField('email', e.target.value)}
443
- onBlur={() => onBlur('email')}
444
- aria-invalid={!!showError('email')}
445
- aria-describedby="email-error"
446
- />
447
- {showError('email') && (
448
- <span className="contact__error" id="email-error">
449
- {showError('email')}
450
- </span>
451
- )}
452
- </label>
453
-
454
- <label className="contact__form__group contact__form__group--3">
455
- <span className="contact__form__label">Telefoonnummer</span>
456
- <input
457
- type="tel"
458
- className="contact__input"
459
- placeholder="Enter your phone number"
460
- aria-label="Enter your phone number"
461
- value={values.phone}
462
- onChange={(e) => setField('phone', e.target.value)}
463
- onBlur={() => onBlur('phone')}
464
- aria-invalid={!!showError('phone')}
465
- aria-describedby="phone-error"
466
- />
467
- {showError('phone') && (
468
- <span className="contact__error" id="phone-error">
469
- {showError('phone')}
470
- </span>
471
- )}
472
- </label>
473
-
474
- <label className="contact__form__group contact__form__group--2">
475
- <span className="contact__form__label">Straat*</span>
476
- <input
477
- type="text"
478
- className="contact__input"
479
- placeholder="Straatnaam"
480
- aria-label="Straat"
481
- value={values.street}
482
- onChange={(e) => setField('street', e.target.value)}
483
- onBlur={() => onBlur('street')}
484
- aria-invalid={!!showError('street')}
485
- aria-describedby="street-error"
486
- />
487
- {showError('street') && (
488
- <span className="contact__error" id="street-error">
489
- {showError('street')}
490
- </span>
491
- )}
492
- </label>
493
-
494
- <label className="contact__form__group contact__form__group--4">
495
- <span className="contact__form__label">Nummer*</span>
496
- <input
497
- type="text"
498
- className="contact__input"
499
- placeholder="Nr"
500
- aria-label="Huisnummer"
501
- value={values.houseNumber}
502
- onChange={(e) => setField('houseNumber', e.target.value)}
503
- onBlur={() => onBlur('houseNumber')}
504
- aria-invalid={!!showError('houseNumber')}
505
- aria-describedby="houseNumber-error"
506
- />
507
- {showError('houseNumber') && (
508
- <span className="contact__error" id="houseNumber-error">
509
- {showError('houseNumber')}
510
- </span>
511
- )}
512
- </label>
513
-
514
- <label className="contact__form__group contact__form__group--4">
515
- <span className="contact__form__label">Bus</span>
516
- <input
517
- type="text"
518
- className="contact__input"
519
- placeholder="Bus"
520
- aria-label="Bus"
521
- value={values.box}
522
- onChange={(e) => setField('box', e.target.value)}
523
- onBlur={() => onBlur('box')}
524
- aria-invalid={!!showError('box')}
525
- aria-describedby="box-error"
526
- />
527
- {showError('box') && (
528
- <span className="contact__error" id="box-error">
529
- {showError('box')}
530
- </span>
531
- )}
532
- </label>
533
-
534
- <label className="contact__form__group contact__form__group--3">
535
- <span className="contact__form__label">Postcode*</span>
536
- <input
537
- type="text"
538
- className="contact__input"
539
- placeholder="Postcode"
540
- aria-label="Postcode"
541
- value={values.postalCode}
542
- onChange={(e) => setField('postalCode', e.target.value)}
543
- onBlur={() => onBlur('postalCode')}
544
- aria-invalid={!!showError('postalCode')}
545
- aria-describedby="postalCode-error"
546
- />
547
- {showError('postalCode') && (
548
- <span className="contact__error" id="postalCode-error">
549
- {showError('postalCode')}
550
- </span>
551
- )}
552
- </label>
553
-
554
- <label className="contact__form__group contact__form__group--3">
555
- <span className="contact__form__label">Woonplaats*</span>
556
- <input
557
- type="text"
558
- className="contact__input"
559
- placeholder="Woonplaats"
560
- aria-label="Woonplaats"
561
- value={values.city}
562
- onChange={(e) => setField('city', e.target.value)}
563
- onBlur={() => onBlur('city')}
564
- aria-invalid={!!showError('city')}
565
- aria-describedby="city-error"
566
- />
567
- {showError('city') && (
568
- <span className="contact__error" id="city-error">
569
- {showError('city')}
570
- </span>
571
- )}
572
- </label>
573
-
574
- <label className="contact__form__group contact__form__group--3">
575
- <span className="contact__form__label">Land*</span>
576
-
577
- <div className={`contact__dropdown ${showError('country') ? 'contact__dropdown--error' : ''}`}>
578
- <button
579
- type="button"
580
- className="contact__dropdown__trigger"
581
- aria-haspopup="listbox"
582
- aria-expanded={isCountryOpen}
583
- aria-describedby="country-error"
584
- onClick={() => setIsCountryOpen((o) => !o)}
585
- onBlur={() => {
586
- setIsCountryOpen(false);
587
- onBlur('country');
588
- }}>
589
- <span>{countryOptions.find((o) => o.value === values.country)?.label ?? 'Selecteer land'}</span>
590
- <Icon name="ui-chevron" width={16} height={16} />
591
- </button>
592
-
593
- {isCountryOpen && (
594
- <ul className="contact__dropdown__menu" role="listbox">
595
- {countryOptions.map((option) => (
596
- <li key={option.value}>
597
- <button
598
- type="button"
599
- className={`contact__dropdown__option ${values.country === option.value ? 'contact__dropdown__option--active' : ''}`}
600
- role="option"
601
- aria-selected={values.country === option.value}
602
- onMouseDown={() => {
603
- // Prevent blur-before-click
604
- setField('country', option.value);
605
- setIsCountryOpen(false);
606
- }}>
607
- {option.label}
608
- </button>
609
- </li>
610
- ))}
611
- </ul>
612
- )}
613
- </div>
614
-
615
- {showError('country') && (
616
- <span className="contact__error" id="country-error">
617
- {showError('country')}
618
- </span>
619
- )}
620
- </label>
621
- </div>
622
- </div>
623
-
624
- <div className="contact__card">
625
- <div className="contact__title">
626
- <Icon name="ui-user" width={24} height={24} />
627
- <h4>Reiziger 2</h4>
628
- <span>volwassenen</span>
629
- </div>
630
-
631
- <div className="contact__radio" role="group" aria-labelledby="gender-label" aria-invalid={!!showError('gender1')} aria-describedby="gender-error">
632
- <span id="gender-label">Geslacht*</span>
633
-
634
- <div className="radiobutton-group">
635
- <div className="radiobutton">
636
- <label className="radiobutton__label">
637
- <input
638
- type="radio"
639
- name="gender2"
640
- value="female"
641
- className="radiobutton__input"
642
- checked={values.gender2 === 'female'}
643
- onChange={() => setField('gender2', 'female')}
644
- onBlur={() => onBlur('gender2')}
645
- />
646
- <span>Vrouw</span>
647
- </label>
648
- </div>
649
-
650
- <div className="radiobutton">
651
- <label className="radiobutton__label">
652
- <input
653
- type="radio"
654
- name="gender2"
655
- value="male"
656
- className="radiobutton__input"
657
- checked={values.gender2 === 'male'}
658
- onChange={() => setField('gender2', 'male')}
659
- onBlur={() => onBlur('gender2')}
660
- />
661
- <span>Man</span>
662
- </label>
663
- </div>
664
-
665
- <div className="radiobutton">
666
- <label className="radiobutton__label">
667
- <input
668
- type="radio"
669
- name="gender2"
670
- value="other"
671
- className="radiobutton__input"
672
- checked={values.gender2 === 'other'}
673
- onChange={() => setField('gender2', 'other')}
674
- onBlur={() => onBlur('gender2')}
675
- />
676
- <span>Anders</span>
677
- </label>
678
- </div>
679
- </div>
680
-
681
- {showError('gender2') && (
682
- <span className="contact__radio--error" id="gender-error">
683
- {showError('gender2')}
684
- </span>
685
- )}
686
- </div>
687
-
688
- <div className="contact__form">
689
- <label className="contact__form__group contact__form__group--2">
690
- <span className="contact__form__label">Voornaam*</span>
691
- <input
692
- type="text"
693
- className="contact__input"
694
- placeholder="Enter your name"
695
- aria-label="Enter your name"
696
- value={values.firstName}
697
- onChange={(e) => setField('firstName', e.target.value)}
698
- onBlur={() => onBlur('firstName')}
699
- aria-invalid={!!showError('firstName')}
700
- aria-describedby="firstName-error"
701
- />
702
- {showError('firstName') && (
703
- <span className="contact__error" id="firstName-error">
704
- {showError('firstName')}
705
- </span>
706
- )}
707
- </label>
708
-
709
- <label className="contact__form__group contact__form__group--2">
710
- <span className="contact__form__label">Achternaam*</span>
711
- <input
712
- type="text"
713
- className="contact__input"
714
- placeholder="Enter your last name"
715
- aria-label="Enter your last name"
716
- value={values.lastName}
717
- onChange={(e) => setField('lastName', e.target.value)}
718
- onBlur={() => onBlur('lastName')}
719
- aria-invalid={!!showError('lastName')}
720
- aria-describedby="lastName-error"
721
- />
722
- {showError('lastName') && (
723
- <span className="contact__error" id="lastName-error">
724
- {showError('lastName')}
725
- </span>
726
- )}
727
- </label>
728
-
729
- <label className="contact__form__group contact__form__group--2">
730
- <span className="contact__form__label">Geboortedatum*</span>
731
- <input
732
- type="date"
733
- className="contact__input"
734
- aria-label="Enter your date of birth"
735
- value={values.dateOfBirth}
736
- onChange={(e) => setField('dateOfBirth', e.target.value)}
737
- onBlur={() => onBlur('dateOfBirth')}
738
- aria-invalid={!!showError('dateOfBirth')}
739
- aria-describedby="dateOfBirth-error"
740
- />
741
- {showError('dateOfBirth') && (
742
- <span className="contact__error" id="dateOfBirth-error">
743
- {showError('dateOfBirth')}
744
- </span>
745
- )}
746
- </label>
747
-
748
- <label className="contact__form__group contact__form__group--2">
749
- <span className="contact__form__label">Nationaliteit*</span>
750
-
751
- <div className={`contact__dropdown ${showError('nationality') ? 'contact__dropdown--error' : ''}`}>
752
- <button
753
- type="button"
754
- className="contact__dropdown__trigger"
755
- aria-haspopup="listbox"
756
- aria-expanded={isNationalityOpen}
757
- onClick={() => setIsNationalityOpen((o) => !o)}
758
- onBlur={() => {
759
- setIsNationalityOpen(false);
760
- onBlur('nationality');
761
- }}>
762
- <span>{nationalityOptions.find((o) => o.value === values.nationality)?.label ?? 'Selecteer nationaliteit'}</span>
763
-
764
- <Icon name="ui-chevron" width={16} height={16} />
765
- </button>
766
-
767
- {isNationalityOpen && (
768
- <ul className="contact__dropdown__menu" role="listbox">
769
- {nationalityOptions.map((option) => (
770
- <li key={option.value}>
771
- <button
772
- type="button"
773
- className={`contact__dropdown__option ${values.nationality === option.value ? 'contact__dropdown__option--active' : ''}`}
774
- role="option"
775
- aria-selected={values.nationality === option.value}
776
- onMouseDown={() => {
777
- // onMouseDown prevents blur-before-click
778
- setField('nationality', option.value);
779
- setIsNationalityOpen(false);
780
- }}>
781
- {option.label}
782
- </button>
783
- </li>
784
- ))}
785
- </ul>
786
- )}
787
- </div>
788
-
789
- {showError('nationality') && (
790
- <span className="contact__error" id="nationality-error">
791
- {showError('nationality')}
792
- </span>
793
- )}
794
- </label>
795
- </div>
796
- </div>
797
-
798
- <div className="contact__form__actions">
799
- <button type="submit" className="cta cta--primary">
800
- Send message
801
- </button>
802
- </div>
803
- </form>
804
- </div>
805
- </div>
806
- );
807
- };
808
-
809
- export default PersonalContactForm;
1
+ import React, { useMemo, useState } from 'react';
2
+ import Icon from './icon';
3
+
4
+ type ContactValues = {
5
+ firstName: string;
6
+ lastName: string;
7
+ dateOfBirth: string;
8
+ nationality: string;
9
+ email: string;
10
+ phone: string;
11
+ message: string;
12
+ street: string;
13
+ houseNumber: string;
14
+ box: string;
15
+ postalCode: string;
16
+ city: string;
17
+ country: string;
18
+ // traveler 1
19
+ bookingType1: 'leisure' | 'business' | '';
20
+ gender1: 'female' | 'male' | 'other' | '';
21
+
22
+ // traveler 2
23
+ bookingType2: 'leisure' | 'business' | '';
24
+ gender2: 'female' | 'male' | 'other' | '';
25
+ };
26
+
27
+ type ContactErrors = Partial<Record<keyof ContactValues, string>>;
28
+ // const [touched, setTouched] = useState<Partial<Record<keyof ContactValues, boolean>>>({});
29
+
30
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
31
+ const phoneRegex = /^[+()\-.\s0-9]{8,20}$/;
32
+
33
+ // Small helper: checks YYYY-MM-DD parses to a real date
34
+ const isValidISODate = (value: string) => {
35
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
36
+ const d = new Date(value);
37
+ return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === value;
38
+ };
39
+
40
+ // You can expand this list later (or load from API)
41
+ const nationalityOptions = [
42
+ { value: '', label: 'Selecteer nationaliteit' },
43
+ { value: 'BE', label: 'Belgisch' },
44
+ { value: 'NL', label: 'Nederlands' },
45
+ { value: 'FR', label: 'Frans' },
46
+ { value: 'DE', label: 'Duits' },
47
+ { value: 'UK', label: 'Brits' },
48
+ { value: 'US', label: 'Amerikaans' }
49
+ ];
50
+
51
+ const countryOptions = [
52
+ { value: '', label: 'Selecteer land' },
53
+ { value: 'BE', label: 'België' },
54
+ { value: 'NL', label: 'Nederland' },
55
+ { value: 'FR', label: 'Frankrijk' },
56
+ { value: 'DE', label: 'Duitsland' },
57
+ { value: 'LU', label: 'Luxemburg' },
58
+ { value: 'UK', label: 'Verenigd Koninkrijk' }
59
+ ];
60
+
61
+ const PersonalContactForm: React.FC = () => {
62
+ const [values, setValues] = useState<ContactValues>({
63
+ firstName: '',
64
+ lastName: '',
65
+ dateOfBirth: '',
66
+ nationality: '',
67
+ email: '',
68
+ phone: '',
69
+ message: '',
70
+ street: '',
71
+ houseNumber: '',
72
+ box: '',
73
+ postalCode: '',
74
+ city: '',
75
+ country: '',
76
+ bookingType1: '',
77
+ gender1: '',
78
+ bookingType2: '',
79
+ gender2: ''
80
+ });
81
+
82
+ const [touched, setTouched] = useState<Partial<Record<keyof ContactValues, boolean>>>({});
83
+ const [errors, setErrors] = useState<ContactErrors>({});
84
+ const [submitted, setSubmitted] = useState(false);
85
+ const [isNationalityOpen, setIsNationalityOpen] = useState(false);
86
+ const [isCountryOpen, setIsCountryOpen] = useState(false);
87
+
88
+ const postalCodeRegex = /^[0-9A-Za-z\s-]{3,10}$/;
89
+
90
+ const validate = (v: ContactValues): ContactErrors => {
91
+ const e: ContactErrors = {};
92
+
93
+ if (!v.firstName.trim()) e.firstName = 'Voornaam is verplicht.';
94
+ if (!v.lastName.trim()) e.lastName = 'Achternaam is verplicht.';
95
+
96
+ if (!v.dateOfBirth.trim()) e.dateOfBirth = 'Geboortedatum is verplicht.';
97
+ else if (!isValidISODate(v.dateOfBirth.trim())) e.dateOfBirth = 'Vul een geldige geboortedatum in.';
98
+
99
+ if (!v.nationality.trim()) e.nationality = 'Nationaliteit is verplicht.';
100
+
101
+ if (!v.email.trim()) e.email = 'Email is verplicht.';
102
+ else if (!emailRegex.test(v.email.trim())) e.email = 'Vul een geldig e-mailadres in.';
103
+
104
+ if (v.phone.trim() && !phoneRegex.test(v.phone.trim())) {
105
+ e.phone = 'Vul een geldig telefoonnummer in.';
106
+ }
107
+
108
+ if (!v.message.trim()) e.message = 'Bericht is verplicht.';
109
+ else if (v.message.trim().length < 10) e.message = 'Bericht moet minstens 10 tekens zijn.';
110
+
111
+ if (!v.street.trim()) e.street = 'Straat is verplicht.';
112
+ if (!v.houseNumber.trim()) e.houseNumber = 'Nummer is verplicht.';
113
+
114
+ if (!v.postalCode.trim()) e.postalCode = 'Postcode is verplicht.';
115
+ else if (!postalCodeRegex.test(v.postalCode.trim())) e.postalCode = 'Vul een geldige postcode in.';
116
+
117
+ if (!v.city.trim()) e.city = 'Woonplaats is verplicht.';
118
+ if (!v.country.trim()) e.country = 'Land is verplicht.';
119
+
120
+ if (!v.gender1) e.gender1 = 'Geslacht is verplicht.';
121
+ if (!v.gender2) e.gender2 = 'Geslacht is verplicht.';
122
+
123
+ if (!v.bookingType1) e.bookingType1 = 'Type boeking is verplicht.';
124
+ if (!v.bookingType2) e.bookingType2 = 'Type boeking is verplicht.';
125
+
126
+ return e;
127
+ };
128
+
129
+ const currentErrors = useMemo(() => validate(values), [values]);
130
+
131
+ const setField = <K extends keyof ContactValues>(key: K, val: ContactValues[K]) => {
132
+ setValues((p) => ({ ...p, [key]: val }));
133
+ if (submitted || touched[key]) {
134
+ setErrors(validate({ ...values, [key]: val } as ContactValues));
135
+ }
136
+ };
137
+
138
+ const onBlur = (key: keyof ContactValues) => {
139
+ setTouched((p) => ({ ...p, [key]: true }));
140
+ setErrors(validate(values));
141
+ };
142
+
143
+ const onSubmit = (e: React.FormEvent) => {
144
+ e.preventDefault();
145
+ setSubmitted(true);
146
+
147
+ const eMap = validate(values);
148
+ setErrors(eMap);
149
+
150
+ if (Object.keys(eMap).length > 0) return;
151
+
152
+ console.log('Contact form submit:', values);
153
+
154
+ setValues({
155
+ firstName: '',
156
+ lastName: '',
157
+ dateOfBirth: '',
158
+ nationality: '',
159
+ email: '',
160
+ phone: '',
161
+ message: '',
162
+ street: '',
163
+ houseNumber: '',
164
+ box: '',
165
+ postalCode: '',
166
+ city: '',
167
+ country: '',
168
+ bookingType1: '',
169
+ gender1: '',
170
+ bookingType2: '',
171
+ gender2: ''
172
+ });
173
+ setTouched({});
174
+ setErrors({});
175
+ setSubmitted(false);
176
+ };
177
+
178
+ const showError = (key: keyof ContactValues) => {
179
+ const shouldShow = submitted || touched[key];
180
+ return shouldShow ? errors[key] : undefined;
181
+ };
182
+
183
+ return (
184
+ <div className="content content--background">
185
+ <div className="content__container content__container--medium">
186
+ <div className="content__title__row">
187
+ <h2 className="content__title">Persoonlijke informatie</h2>
188
+ </div>
189
+
190
+ <form className="contact" noValidate onSubmit={onSubmit}>
191
+ <div className="contact__card contact__card--headbooker">
192
+ <div className="contact__title">
193
+ <Icon name="ui-info-circle" width={24} height={24} />
194
+ <h4>Gaat de hoofdboeker mee op reis?</h4>
195
+ </div>
196
+
197
+ <div className="contact__form">
198
+ <div className="radiobutton-group">
199
+ <div className="radiobutton">
200
+ <label className="radiobutton__label">
201
+ <input type="radio" name="headBooker" value="yes" className="radiobutton__input" defaultChecked={true} />
202
+ <span>Ja</span>
203
+ </label>
204
+ </div>
205
+
206
+ <div className="radiobutton">
207
+ <label className="radiobutton__label">
208
+ <input type="radio" name="headBooker" value="no" className="radiobutton__input" />
209
+ <span>Nee</span>
210
+ </label>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ <div className="contact__card">
216
+ <div className="contact__title">
217
+ <Icon name="ui-user" width={24} height={24} />
218
+ <h4>Reiziger 1</h4>
219
+ <span>volwassenen, hoofdboeker</span>
220
+ </div>
221
+
222
+ <div
223
+ className="contact__radio"
224
+ role="group"
225
+ aria-labelledby="bookingType-label"
226
+ aria-invalid={!!showError('bookingType1')}
227
+ aria-describedby="bookingType-error">
228
+ <span id="bookingType-label">Type boeking{/** add * if required */}</span>
229
+
230
+ <div className="radiobutton-group">
231
+ <div className="radiobutton">
232
+ <label className="radiobutton__label">
233
+ <input
234
+ type="radio"
235
+ name="bookingType1"
236
+ value="leisure"
237
+ className="radiobutton__input"
238
+ checked={values.bookingType1 === 'leisure'}
239
+ onChange={() => setField('bookingType1', 'leisure')}
240
+ onBlur={() => onBlur('bookingType1')}
241
+ />
242
+ <span>Plezier</span>
243
+ </label>
244
+ </div>
245
+
246
+ <div className="radiobutton">
247
+ <label className="radiobutton__label">
248
+ <input
249
+ type="radio"
250
+ name="bookingType1"
251
+ value="business"
252
+ className="radiobutton__input"
253
+ checked={values.bookingType1 === 'business'}
254
+ onChange={() => setField('bookingType1', 'business')}
255
+ onBlur={() => onBlur('bookingType1')}
256
+ />
257
+ <span>Zakelijk</span>
258
+ </label>
259
+ </div>
260
+ </div>
261
+
262
+ {showError('bookingType1') && (
263
+ <span className="contact__radio--error" id="bookingType-error">
264
+ {showError('bookingType1')}
265
+ </span>
266
+ )}
267
+ </div>
268
+
269
+ <div className="contact__radio" role="group" aria-labelledby="gender-label" aria-invalid={!!showError('gender1')} aria-describedby="gender-error">
270
+ <span id="gender-label">Geslacht*</span>
271
+
272
+ <div className="radiobutton-group">
273
+ <div className="radiobutton">
274
+ <label className="radiobutton__label">
275
+ <input
276
+ type="radio"
277
+ name="gender1"
278
+ value="female"
279
+ className="radiobutton__input"
280
+ checked={values.gender1 === 'female'}
281
+ onChange={() => setField('gender1', 'female')}
282
+ onBlur={() => onBlur('gender1')}
283
+ />
284
+ <span>Vrouw</span>
285
+ </label>
286
+ </div>
287
+
288
+ <div className="radiobutton">
289
+ <label className="radiobutton__label">
290
+ <input
291
+ type="radio"
292
+ name="gender1"
293
+ value="male"
294
+ className="radiobutton__input"
295
+ checked={values.gender1 === 'male'}
296
+ onChange={() => setField('gender1', 'male')}
297
+ onBlur={() => onBlur('gender1')}
298
+ />
299
+ <span>Man</span>
300
+ </label>
301
+ </div>
302
+
303
+ <div className="radiobutton">
304
+ <label className="radiobutton__label">
305
+ <input
306
+ type="radio"
307
+ name="gender1"
308
+ value="other"
309
+ className="radiobutton__input"
310
+ checked={values.gender1 === 'other'}
311
+ onChange={() => setField('gender1', 'other')}
312
+ onBlur={() => onBlur('gender1')}
313
+ />
314
+ <span>Anders</span>
315
+ </label>
316
+ </div>
317
+ </div>
318
+
319
+ {showError('gender1') && (
320
+ <span className="contact__radio--error" id="gender-error">
321
+ {showError('gender1')}
322
+ </span>
323
+ )}
324
+ </div>
325
+
326
+ <div className="contact__form">
327
+ <label className="contact__form__group contact__form__group--3">
328
+ <span className="contact__form__label">Voornaam*</span>
329
+ <input
330
+ type="text"
331
+ className="contact__input"
332
+ placeholder="Enter your name"
333
+ aria-label="Enter your name"
334
+ value={values.firstName}
335
+ onChange={(e) => setField('firstName', e.target.value)}
336
+ onBlur={() => onBlur('firstName')}
337
+ aria-invalid={!!showError('firstName')}
338
+ aria-describedby="firstName-error"
339
+ />
340
+ {showError('firstName') && (
341
+ <span className="contact__error" id="firstName-error">
342
+ {showError('firstName')}
343
+ </span>
344
+ )}
345
+ </label>
346
+
347
+ <label className="contact__form__group contact__form__group--3">
348
+ <span className="contact__form__label">Achternaam*</span>
349
+ <input
350
+ type="text"
351
+ className="contact__input"
352
+ placeholder="Enter your last name"
353
+ aria-label="Enter your last name"
354
+ value={values.lastName}
355
+ onChange={(e) => setField('lastName', e.target.value)}
356
+ onBlur={() => onBlur('lastName')}
357
+ aria-invalid={!!showError('lastName')}
358
+ aria-describedby="lastName-error"
359
+ />
360
+ {showError('lastName') && (
361
+ <span className="contact__error" id="lastName-error">
362
+ {showError('lastName')}
363
+ </span>
364
+ )}
365
+ </label>
366
+
367
+ <label className="contact__form__group contact__form__group--3">
368
+ <span className="contact__form__label">Geboortedatum*</span>
369
+ <input
370
+ type="date"
371
+ className="contact__input"
372
+ aria-label="Enter your date of birth"
373
+ value={values.dateOfBirth}
374
+ onChange={(e) => setField('dateOfBirth', e.target.value)}
375
+ onBlur={() => onBlur('dateOfBirth')}
376
+ aria-invalid={!!showError('dateOfBirth')}
377
+ aria-describedby="dateOfBirth-error"
378
+ />
379
+ {showError('dateOfBirth') && (
380
+ <span className="contact__error" id="dateOfBirth-error">
381
+ {showError('dateOfBirth')}
382
+ </span>
383
+ )}
384
+ </label>
385
+
386
+ <label className="contact__form__group contact__form__group--3">
387
+ <span className="contact__form__label">Nationaliteit*</span>
388
+
389
+ <div className={`contact__dropdown ${showError('nationality') ? 'contact__dropdown--error' : ''}`}>
390
+ <button
391
+ type="button"
392
+ className="contact__dropdown__trigger"
393
+ aria-haspopup="listbox"
394
+ aria-expanded={isNationalityOpen}
395
+ onClick={() => setIsNationalityOpen((o) => !o)}
396
+ onBlur={() => {
397
+ setIsNationalityOpen(false);
398
+ onBlur('nationality');
399
+ }}>
400
+ <span>{nationalityOptions.find((o) => o.value === values.nationality)?.label ?? 'Selecteer nationaliteit'}</span>
401
+
402
+ <Icon name="ui-chevron" width={16} height={16} />
403
+ </button>
404
+
405
+ {isNationalityOpen && (
406
+ <ul className="contact__dropdown__menu" role="listbox">
407
+ {nationalityOptions.map((option) => (
408
+ <li key={option.value}>
409
+ <button
410
+ type="button"
411
+ className={`contact__dropdown__option ${values.nationality === option.value ? 'contact__dropdown__option--active' : ''}`}
412
+ role="option"
413
+ aria-selected={values.nationality === option.value}
414
+ onMouseDown={() => {
415
+ // onMouseDown prevents blur-before-click
416
+ setField('nationality', option.value);
417
+ setIsNationalityOpen(false);
418
+ }}>
419
+ {option.label}
420
+ </button>
421
+ </li>
422
+ ))}
423
+ </ul>
424
+ )}
425
+ </div>
426
+
427
+ {showError('nationality') && (
428
+ <span className="contact__error" id="nationality-error">
429
+ {showError('nationality')}
430
+ </span>
431
+ )}
432
+ </label>
433
+
434
+ <label className="contact__form__group contact__form__group--3">
435
+ <span className="contact__form__label">Email*</span>
436
+ <input
437
+ type="email"
438
+ className="contact__input"
439
+ placeholder="Enter your email"
440
+ aria-label="Enter your email"
441
+ value={values.email}
442
+ onChange={(e) => setField('email', e.target.value)}
443
+ onBlur={() => onBlur('email')}
444
+ aria-invalid={!!showError('email')}
445
+ aria-describedby="email-error"
446
+ />
447
+ {showError('email') && (
448
+ <span className="contact__error" id="email-error">
449
+ {showError('email')}
450
+ </span>
451
+ )}
452
+ </label>
453
+
454
+ <label className="contact__form__group contact__form__group--3">
455
+ <span className="contact__form__label">Telefoonnummer</span>
456
+ <input
457
+ type="tel"
458
+ className="contact__input"
459
+ placeholder="Enter your phone number"
460
+ aria-label="Enter your phone number"
461
+ value={values.phone}
462
+ onChange={(e) => setField('phone', e.target.value)}
463
+ onBlur={() => onBlur('phone')}
464
+ aria-invalid={!!showError('phone')}
465
+ aria-describedby="phone-error"
466
+ />
467
+ {showError('phone') && (
468
+ <span className="contact__error" id="phone-error">
469
+ {showError('phone')}
470
+ </span>
471
+ )}
472
+ </label>
473
+
474
+ <label className="contact__form__group contact__form__group--2">
475
+ <span className="contact__form__label">Straat*</span>
476
+ <input
477
+ type="text"
478
+ className="contact__input"
479
+ placeholder="Straatnaam"
480
+ aria-label="Straat"
481
+ value={values.street}
482
+ onChange={(e) => setField('street', e.target.value)}
483
+ onBlur={() => onBlur('street')}
484
+ aria-invalid={!!showError('street')}
485
+ aria-describedby="street-error"
486
+ />
487
+ {showError('street') && (
488
+ <span className="contact__error" id="street-error">
489
+ {showError('street')}
490
+ </span>
491
+ )}
492
+ </label>
493
+
494
+ <label className="contact__form__group contact__form__group--4">
495
+ <span className="contact__form__label">Nummer*</span>
496
+ <input
497
+ type="text"
498
+ className="contact__input"
499
+ placeholder="Nr"
500
+ aria-label="Huisnummer"
501
+ value={values.houseNumber}
502
+ onChange={(e) => setField('houseNumber', e.target.value)}
503
+ onBlur={() => onBlur('houseNumber')}
504
+ aria-invalid={!!showError('houseNumber')}
505
+ aria-describedby="houseNumber-error"
506
+ />
507
+ {showError('houseNumber') && (
508
+ <span className="contact__error" id="houseNumber-error">
509
+ {showError('houseNumber')}
510
+ </span>
511
+ )}
512
+ </label>
513
+
514
+ <label className="contact__form__group contact__form__group--4">
515
+ <span className="contact__form__label">Bus</span>
516
+ <input
517
+ type="text"
518
+ className="contact__input"
519
+ placeholder="Bus"
520
+ aria-label="Bus"
521
+ value={values.box}
522
+ onChange={(e) => setField('box', e.target.value)}
523
+ onBlur={() => onBlur('box')}
524
+ aria-invalid={!!showError('box')}
525
+ aria-describedby="box-error"
526
+ />
527
+ {showError('box') && (
528
+ <span className="contact__error" id="box-error">
529
+ {showError('box')}
530
+ </span>
531
+ )}
532
+ </label>
533
+
534
+ <label className="contact__form__group contact__form__group--3">
535
+ <span className="contact__form__label">Postcode*</span>
536
+ <input
537
+ type="text"
538
+ className="contact__input"
539
+ placeholder="Postcode"
540
+ aria-label="Postcode"
541
+ value={values.postalCode}
542
+ onChange={(e) => setField('postalCode', e.target.value)}
543
+ onBlur={() => onBlur('postalCode')}
544
+ aria-invalid={!!showError('postalCode')}
545
+ aria-describedby="postalCode-error"
546
+ />
547
+ {showError('postalCode') && (
548
+ <span className="contact__error" id="postalCode-error">
549
+ {showError('postalCode')}
550
+ </span>
551
+ )}
552
+ </label>
553
+
554
+ <label className="contact__form__group contact__form__group--3">
555
+ <span className="contact__form__label">Woonplaats*</span>
556
+ <input
557
+ type="text"
558
+ className="contact__input"
559
+ placeholder="Woonplaats"
560
+ aria-label="Woonplaats"
561
+ value={values.city}
562
+ onChange={(e) => setField('city', e.target.value)}
563
+ onBlur={() => onBlur('city')}
564
+ aria-invalid={!!showError('city')}
565
+ aria-describedby="city-error"
566
+ />
567
+ {showError('city') && (
568
+ <span className="contact__error" id="city-error">
569
+ {showError('city')}
570
+ </span>
571
+ )}
572
+ </label>
573
+
574
+ <label className="contact__form__group contact__form__group--3">
575
+ <span className="contact__form__label">Land*</span>
576
+
577
+ <div className={`contact__dropdown ${showError('country') ? 'contact__dropdown--error' : ''}`}>
578
+ <button
579
+ type="button"
580
+ className="contact__dropdown__trigger"
581
+ aria-haspopup="listbox"
582
+ aria-expanded={isCountryOpen}
583
+ aria-describedby="country-error"
584
+ onClick={() => setIsCountryOpen((o) => !o)}
585
+ onBlur={() => {
586
+ setIsCountryOpen(false);
587
+ onBlur('country');
588
+ }}>
589
+ <span>{countryOptions.find((o) => o.value === values.country)?.label ?? 'Selecteer land'}</span>
590
+ <Icon name="ui-chevron" width={16} height={16} />
591
+ </button>
592
+
593
+ {isCountryOpen && (
594
+ <ul className="contact__dropdown__menu" role="listbox">
595
+ {countryOptions.map((option) => (
596
+ <li key={option.value}>
597
+ <button
598
+ type="button"
599
+ className={`contact__dropdown__option ${values.country === option.value ? 'contact__dropdown__option--active' : ''}`}
600
+ role="option"
601
+ aria-selected={values.country === option.value}
602
+ onMouseDown={() => {
603
+ // Prevent blur-before-click
604
+ setField('country', option.value);
605
+ setIsCountryOpen(false);
606
+ }}>
607
+ {option.label}
608
+ </button>
609
+ </li>
610
+ ))}
611
+ </ul>
612
+ )}
613
+ </div>
614
+
615
+ {showError('country') && (
616
+ <span className="contact__error" id="country-error">
617
+ {showError('country')}
618
+ </span>
619
+ )}
620
+ </label>
621
+ </div>
622
+ </div>
623
+
624
+ <div className="contact__card">
625
+ <div className="contact__title">
626
+ <Icon name="ui-user" width={24} height={24} />
627
+ <h4>Reiziger 2</h4>
628
+ <span>volwassenen</span>
629
+ </div>
630
+
631
+ <div className="contact__radio" role="group" aria-labelledby="gender-label" aria-invalid={!!showError('gender1')} aria-describedby="gender-error">
632
+ <span id="gender-label">Geslacht*</span>
633
+
634
+ <div className="radiobutton-group">
635
+ <div className="radiobutton">
636
+ <label className="radiobutton__label">
637
+ <input
638
+ type="radio"
639
+ name="gender2"
640
+ value="female"
641
+ className="radiobutton__input"
642
+ checked={values.gender2 === 'female'}
643
+ onChange={() => setField('gender2', 'female')}
644
+ onBlur={() => onBlur('gender2')}
645
+ />
646
+ <span>Vrouw</span>
647
+ </label>
648
+ </div>
649
+
650
+ <div className="radiobutton">
651
+ <label className="radiobutton__label">
652
+ <input
653
+ type="radio"
654
+ name="gender2"
655
+ value="male"
656
+ className="radiobutton__input"
657
+ checked={values.gender2 === 'male'}
658
+ onChange={() => setField('gender2', 'male')}
659
+ onBlur={() => onBlur('gender2')}
660
+ />
661
+ <span>Man</span>
662
+ </label>
663
+ </div>
664
+
665
+ <div className="radiobutton">
666
+ <label className="radiobutton__label">
667
+ <input
668
+ type="radio"
669
+ name="gender2"
670
+ value="other"
671
+ className="radiobutton__input"
672
+ checked={values.gender2 === 'other'}
673
+ onChange={() => setField('gender2', 'other')}
674
+ onBlur={() => onBlur('gender2')}
675
+ />
676
+ <span>Anders</span>
677
+ </label>
678
+ </div>
679
+ </div>
680
+
681
+ {showError('gender2') && (
682
+ <span className="contact__radio--error" id="gender-error">
683
+ {showError('gender2')}
684
+ </span>
685
+ )}
686
+ </div>
687
+
688
+ <div className="contact__form">
689
+ <label className="contact__form__group contact__form__group--2">
690
+ <span className="contact__form__label">Voornaam*</span>
691
+ <input
692
+ type="text"
693
+ className="contact__input"
694
+ placeholder="Enter your name"
695
+ aria-label="Enter your name"
696
+ value={values.firstName}
697
+ onChange={(e) => setField('firstName', e.target.value)}
698
+ onBlur={() => onBlur('firstName')}
699
+ aria-invalid={!!showError('firstName')}
700
+ aria-describedby="firstName-error"
701
+ />
702
+ {showError('firstName') && (
703
+ <span className="contact__error" id="firstName-error">
704
+ {showError('firstName')}
705
+ </span>
706
+ )}
707
+ </label>
708
+
709
+ <label className="contact__form__group contact__form__group--2">
710
+ <span className="contact__form__label">Achternaam*</span>
711
+ <input
712
+ type="text"
713
+ className="contact__input"
714
+ placeholder="Enter your last name"
715
+ aria-label="Enter your last name"
716
+ value={values.lastName}
717
+ onChange={(e) => setField('lastName', e.target.value)}
718
+ onBlur={() => onBlur('lastName')}
719
+ aria-invalid={!!showError('lastName')}
720
+ aria-describedby="lastName-error"
721
+ />
722
+ {showError('lastName') && (
723
+ <span className="contact__error" id="lastName-error">
724
+ {showError('lastName')}
725
+ </span>
726
+ )}
727
+ </label>
728
+
729
+ <label className="contact__form__group contact__form__group--2">
730
+ <span className="contact__form__label">Geboortedatum*</span>
731
+ <input
732
+ type="date"
733
+ className="contact__input"
734
+ aria-label="Enter your date of birth"
735
+ value={values.dateOfBirth}
736
+ onChange={(e) => setField('dateOfBirth', e.target.value)}
737
+ onBlur={() => onBlur('dateOfBirth')}
738
+ aria-invalid={!!showError('dateOfBirth')}
739
+ aria-describedby="dateOfBirth-error"
740
+ />
741
+ {showError('dateOfBirth') && (
742
+ <span className="contact__error" id="dateOfBirth-error">
743
+ {showError('dateOfBirth')}
744
+ </span>
745
+ )}
746
+ </label>
747
+
748
+ <label className="contact__form__group contact__form__group--2">
749
+ <span className="contact__form__label">Nationaliteit*</span>
750
+
751
+ <div className={`contact__dropdown ${showError('nationality') ? 'contact__dropdown--error' : ''}`}>
752
+ <button
753
+ type="button"
754
+ className="contact__dropdown__trigger"
755
+ aria-haspopup="listbox"
756
+ aria-expanded={isNationalityOpen}
757
+ onClick={() => setIsNationalityOpen((o) => !o)}
758
+ onBlur={() => {
759
+ setIsNationalityOpen(false);
760
+ onBlur('nationality');
761
+ }}>
762
+ <span>{nationalityOptions.find((o) => o.value === values.nationality)?.label ?? 'Selecteer nationaliteit'}</span>
763
+
764
+ <Icon name="ui-chevron" width={16} height={16} />
765
+ </button>
766
+
767
+ {isNationalityOpen && (
768
+ <ul className="contact__dropdown__menu" role="listbox">
769
+ {nationalityOptions.map((option) => (
770
+ <li key={option.value}>
771
+ <button
772
+ type="button"
773
+ className={`contact__dropdown__option ${values.nationality === option.value ? 'contact__dropdown__option--active' : ''}`}
774
+ role="option"
775
+ aria-selected={values.nationality === option.value}
776
+ onMouseDown={() => {
777
+ // onMouseDown prevents blur-before-click
778
+ setField('nationality', option.value);
779
+ setIsNationalityOpen(false);
780
+ }}>
781
+ {option.label}
782
+ </button>
783
+ </li>
784
+ ))}
785
+ </ul>
786
+ )}
787
+ </div>
788
+
789
+ {showError('nationality') && (
790
+ <span className="contact__error" id="nationality-error">
791
+ {showError('nationality')}
792
+ </span>
793
+ )}
794
+ </label>
795
+ </div>
796
+ </div>
797
+
798
+ <div className="contact__form__actions">
799
+ <button type="submit" className="cta cta--primary">
800
+ Send message
801
+ </button>
802
+ </div>
803
+ </form>
804
+ </div>
805
+ </div>
806
+ );
807
+ };
808
+
809
+ export default PersonalContactForm;