@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.
- package/.prettierrc +9 -9
- package/.vs/ProjectSettings.json +3 -3
- package/.vs/VSWorkspaceState.json +5 -5
- package/build/build-cjs/index.js +81 -27
- package/build/build-cjs/src/booking-wizard/features/booking/booking-slice.d.ts +2 -1
- package/build/build-cjs/src/booking-wizard/features/booking/selectors.d.ts +4 -3
- package/build/build-cjs/src/booking-wizard/features/price-details/price-details-slice.d.ts +1 -0
- package/build/build-cjs/src/booking-wizard/features/price-details/selectors.d.ts +1 -0
- package/build/build-cjs/src/booking-wizard/features/sidebar/sidebar.d.ts +1 -0
- package/build/build-cjs/src/booking-wizard/types.d.ts +1 -0
- package/build/build-cjs/src/shared/utils/localization-util.d.ts +1 -0
- package/build/build-esm/index.js +81 -27
- package/build/build-esm/src/booking-wizard/features/booking/booking-slice.d.ts +2 -1
- package/build/build-esm/src/booking-wizard/features/booking/selectors.d.ts +4 -3
- package/build/build-esm/src/booking-wizard/features/price-details/price-details-slice.d.ts +1 -0
- package/build/build-esm/src/booking-wizard/features/price-details/selectors.d.ts +1 -0
- package/build/build-esm/src/booking-wizard/features/sidebar/sidebar.d.ts +1 -0
- package/build/build-esm/src/booking-wizard/types.d.ts +1 -0
- package/build/build-esm/src/shared/utils/localization-util.d.ts +1 -0
- package/package.json +83 -83
- package/src/booking-product/components/age-select.tsx +35 -35
- package/src/booking-product/components/amount-input.tsx +51 -51
- package/src/booking-product/components/date-range-picker/calendar.tsx +155 -155
- package/src/booking-product/components/footer.tsx +54 -54
- package/src/booking-product/components/header.tsx +57 -57
- package/src/booking-product/components/icon.tsx +200 -200
- package/src/booking-product/components/list-view.tsx +54 -54
- package/src/booking-product/components/rating.tsx +21 -21
- package/src/booking-product/components/rooms.tsx +171 -171
- package/src/booking-product/constants.ts +1 -1
- package/src/booking-product/index.tsx +21 -21
- package/src/booking-product/settings-context.ts +16 -16
- package/src/booking-product/types.ts +30 -30
- package/src/booking-product/utils/api.ts +26 -26
- package/src/booking-product/utils/price.ts +28 -28
- package/src/booking-wizard/api-settings-slice.ts +24 -24
- package/src/booking-wizard/components/icon.tsx +398 -398
- package/src/booking-wizard/components/labeled-input.tsx +56 -56
- package/src/booking-wizard/components/labeled-select.tsx +54 -54
- package/src/booking-wizard/components/message.tsx +21 -21
- package/src/booking-wizard/components/multi-range-filter.tsx +99 -99
- package/src/booking-wizard/components/phone-input.tsx +146 -146
- package/src/booking-wizard/components/print-offer-button.tsx +53 -53
- package/src/booking-wizard/components/product-card.tsx +23 -23
- package/src/booking-wizard/declarations.d.ts +4 -4
- package/src/booking-wizard/features/booking/booking-self-contained.tsx +16 -1
- package/src/booking-wizard/features/booking/booking-slice.ts +9 -1
- package/src/booking-wizard/features/booking/booking.tsx +16 -1
- package/src/booking-wizard/features/booking/selectors.ts +5 -0
- package/src/booking-wizard/features/flight-options/flight-filter.tsx +371 -371
- package/src/booking-wizard/features/flight-options/flight-option-flight.tsx +354 -354
- package/src/booking-wizard/features/flight-options/flight-option-modal.tsx +211 -211
- package/src/booking-wizard/features/flight-options/flight-option.tsx +57 -57
- package/src/booking-wizard/features/flight-options/flight-utils.ts +423 -423
- package/src/booking-wizard/features/price-details/price-details-api.ts +20 -20
- package/src/booking-wizard/features/price-details/price-details-slice.ts +2 -0
- package/src/booking-wizard/features/price-details/selectors.ts +1 -0
- package/src/booking-wizard/features/price-details/util.ts +115 -115
- package/src/booking-wizard/features/product-options/no-options.tsx +18 -18
- package/src/booking-wizard/features/product-options/none-option.tsx +73 -73
- package/src/booking-wizard/features/product-options/option-booking-airline-group.tsx +53 -53
- package/src/booking-wizard/features/product-options/option-booking-group.tsx +152 -152
- package/src/booking-wizard/features/product-options/option-item.tsx +236 -236
- package/src/booking-wizard/features/product-options/option-pax-card.tsx +159 -159
- package/src/booking-wizard/features/product-options/option-pax-group.tsx +122 -122
- package/src/booking-wizard/features/product-options/option-room.tsx +226 -226
- package/src/booking-wizard/features/product-options/option-unit-group.tsx +138 -138
- package/src/booking-wizard/features/room-options/room-utils.ts +154 -154
- package/src/booking-wizard/features/room-options/room.tsx +123 -123
- package/src/booking-wizard/features/room-options/traveler-rooms.tsx +64 -64
- package/src/booking-wizard/features/sidebar/index.tsx +2 -0
- package/src/booking-wizard/features/sidebar/sidebar-flight.tsx +66 -66
- package/src/booking-wizard/features/sidebar/sidebar.tsx +17 -1
- package/src/booking-wizard/features/summary/summary-booking-option-pax.tsx +23 -23
- package/src/booking-wizard/features/summary/summary-booking-option-unit.tsx +23 -23
- package/src/booking-wizard/features/summary/summary-flight.tsx +36 -36
- package/src/booking-wizard/features/summary/summary-per-booking-option-group.tsx +60 -60
- package/src/booking-wizard/features/summary/summary-per-pax-option-group.tsx +56 -56
- package/src/booking-wizard/features/summary/summary-per-unit-option-group.tsx +58 -58
- package/src/booking-wizard/features/summary/summary-slice.ts +27 -27
- package/src/booking-wizard/features/travelers-form/travelers-form-slice.ts +157 -157
- package/src/booking-wizard/features/travelers-form/travelers-form-util.ts +10 -10
- package/src/booking-wizard/features/travelers-form/type-ahead-input.tsx +85 -85
- package/src/booking-wizard/features/travelers-form/validate-form.ts +178 -178
- package/src/booking-wizard/index.tsx +27 -27
- package/src/booking-wizard/store.ts +26 -26
- package/src/booking-wizard/types.ts +1 -0
- package/src/booking-wizard/use-offer-printer.ts +108 -108
- package/src/content/components/LanguageSwitcher.tsx +158 -158
- package/src/content/components/accordion.tsx +30 -30
- package/src/content/components/contact.tsx +211 -211
- package/src/content/components/personal-contact-form.tsx +809 -809
- package/src/content/header/index.tsx +43 -43
- package/src/content/header/types.ts +26 -26
- package/src/qsm/components/date-picker/index.tsx +152 -152
- package/src/qsm/components/date-range-picker/calendar-day.tsx +49 -49
- package/src/qsm/components/date-range-picker/calendar.tsx +211 -211
- package/src/qsm/components/date-range-picker/index.tsx +404 -404
- package/src/qsm/index.tsx +26 -26
- package/src/qsm/store/qsm-store.ts +13 -13
- package/src/search-results/components/flight/flight-card.tsx +38 -38
- package/src/search-results/components/flight/flight-leg.tsx +61 -61
- package/src/search-results/components/flight/flight-path.tsx +23 -23
- package/src/search-results/components/multi-range-filter.tsx +104 -104
- package/src/search-results/components/search-results-container/search-results-container.tsx +2 -2
- package/src/search-results/index.tsx +24 -24
- package/src/search-results/search-results-configuration-context.ts +6 -6
- package/src/search-results/store/search-results-store.ts +13 -13
- package/src/shared/components/loader.tsx +16 -16
- package/src/shared/translations/ar-SA.json +2 -1
- package/src/shared/translations/da-DK.json +2 -1
- package/src/shared/translations/de-DE.json +2 -1
- package/src/shared/translations/en-GB.json +2 -1
- package/src/shared/translations/es-ES.json +2 -1
- package/src/shared/translations/fr-BE.json +2 -1
- package/src/shared/translations/fr-FR.json +2 -1
- package/src/shared/translations/is-IS.json +2 -1
- package/src/shared/translations/it-IT.json +2 -1
- package/src/shared/translations/ja-JP.json +2 -1
- package/src/shared/translations/nl-BE.json +2 -1
- package/src/shared/translations/nl-NL.json +2 -1
- package/src/shared/translations/no-NO.json +2 -1
- package/src/shared/translations/pl-PL.json +2 -1
- package/src/shared/translations/pt-PT.json +2 -1
- package/src/shared/translations/sv-SE.json +2 -1
- package/src/shared/utils/class-util.ts +7 -7
- package/src/shared/utils/query-string-util.ts +91 -91
- package/src/shared/utils/tide-api-utils.ts +34 -34
- package/src/shared/utils/use-media-query-util.ts +19 -19
- package/styles/abstracts/_mixins.scss +74 -74
- package/styles/abstracts/_variables.scss +57 -57
- package/styles/base/_fonts.scss +2 -2
- package/styles/base/_normalize.scss +227 -227
- package/styles/base/_typography.scss +35 -35
- package/styles/booking-joker-variables.scss +596 -596
- package/styles/booking-product-variables.scss +330 -330
- package/styles/booking-product.scss +438 -438
- package/styles/booking-qsm-variables.scss +501 -501
- package/styles/booking-qsm.scss +52 -52
- package/styles/booking-wizard-variables.scss +603 -603
- package/styles/booking-wizard.scss +61 -61
- package/styles/components/_accordion.scss +67 -67
- package/styles/components/_animations.scss +39 -39
- package/styles/components/_base.scss +107 -107
- package/styles/components/_breadcrumb.scss +92 -92
- package/styles/components/_button.scss +238 -238
- package/styles/components/_checkbox.scss +230 -230
- package/styles/components/_contact.scss +239 -239
- package/styles/components/_cta.scss +238 -238
- package/styles/components/_date-list.scss +41 -41
- package/styles/components/_date-range-picker.scss +223 -223
- package/styles/components/_decrement-increment.scss +35 -35
- package/styles/components/_dropdown.scss +72 -72
- package/styles/components/_faq.scss +27 -27
- package/styles/components/_flight-option.scss +1419 -1419
- package/styles/components/_gallery.scss +314 -314
- package/styles/components/_header.scss +113 -113
- package/styles/components/_img-slider.scss +175 -175
- package/styles/components/_info-message.scss +75 -75
- package/styles/components/_input.scss +35 -35
- package/styles/components/_list.scss +185 -185
- package/styles/components/_loader.scss +70 -70
- package/styles/components/_mixins.scss +579 -579
- package/styles/components/_passenger-picker.scss +306 -306
- package/styles/components/_phone-input.scss +8 -8
- package/styles/components/_placeholders.scss +165 -165
- package/styles/components/_qsm.scss +17 -17
- package/styles/components/_radiobutton.scss +170 -170
- package/styles/components/_select-wrapper.scss +76 -76
- package/styles/components/_slider.scss +128 -128
- package/styles/components/_spinner.scss +29 -29
- package/styles/components/_step-indicators.scss +161 -161
- package/styles/components/_table.scss +81 -81
- package/styles/components/_typeahead.scss +275 -275
- package/styles/components/_variables.scss +89 -89
- package/styles/content-blocks-variables.scss +507 -507
- package/styles/font.scss +2 -2
- package/styles/qsm/_calendar.scss +274 -274
- package/styles/qsm/_qsm.scss +1094 -1094
- package/styles/search.scss +1200 -1200
- 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;
|