@qite/tide-components 1.0.1 → 1.0.2

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 (28) hide show
  1. package/README.md +392 -421
  2. package/build/build-cjs/index.js +1009 -133
  3. package/build/build-cjs/src/booking-wizard/features/booking/selectors.d.ts +5 -5
  4. package/build/build-cjs/src/booking-wizard/features/travelers-form/travelers-form-slice.d.ts +1 -0
  5. package/build/build-cjs/src/booking-wizard/features/travelers-form/validate-form.d.ts +1 -1
  6. package/build/build-cjs/src/booking-wizard/types.d.ts +15 -0
  7. package/build/build-cjs/src/search-results/components/traject/traject-node-card.d.ts +12 -0
  8. package/build/build-cjs/src/search-results/components/traject/traject-results.d.ts +6 -0
  9. package/build/build-cjs/src/search-results/hooks/use-traject-availability.d.ts +2 -0
  10. package/build/build-cjs/src/search-results/store/search-results-slice.d.ts +21 -2
  11. package/build/build-cjs/src/search-results/types.d.ts +2 -1
  12. package/build/build-cjs/src/search-results/utils/flight-booking-params.d.ts +18 -0
  13. package/build/build-cjs/src/search-results/utils/traject-utils.d.ts +31 -0
  14. package/build/build-cjs/src/shared/booking/travelers-form.d.ts +3 -2
  15. package/build/build-esm/index.js +1010 -134
  16. package/build/build-esm/src/booking-wizard/features/booking/selectors.d.ts +5 -5
  17. package/build/build-esm/src/booking-wizard/features/travelers-form/travelers-form-slice.d.ts +1 -0
  18. package/build/build-esm/src/booking-wizard/features/travelers-form/validate-form.d.ts +1 -1
  19. package/build/build-esm/src/booking-wizard/types.d.ts +15 -0
  20. package/build/build-esm/src/search-results/components/traject/traject-node-card.d.ts +12 -0
  21. package/build/build-esm/src/search-results/components/traject/traject-results.d.ts +6 -0
  22. package/build/build-esm/src/search-results/hooks/use-traject-availability.d.ts +2 -0
  23. package/build/build-esm/src/search-results/store/search-results-slice.d.ts +21 -2
  24. package/build/build-esm/src/search-results/types.d.ts +2 -1
  25. package/build/build-esm/src/search-results/utils/flight-booking-params.d.ts +18 -0
  26. package/build/build-esm/src/search-results/utils/traject-utils.d.ts +31 -0
  27. package/build/build-esm/src/shared/booking/travelers-form.d.ts +3 -2
  28. package/package.json +2 -2
package/README.md CHANGED
@@ -1,421 +1,392 @@
1
- # @qite/tide-components
2
-
3
- React component library for Tide. What started as a single booking wizard component has grown into a full set of building blocks used across Tide-integrated sites: booking flows, search results, quick search modules (QSM), dynamic forms, member login, and marketing/content blocks (navbar, header, footer, image grids, sliders, FAQ, ...).
4
-
5
- Every component that talks to Tide's API is a **plain React component** — you own the page/routing, this library owns the Tide-specific UI and API calls.
6
-
7
- ## Table of contents
8
-
9
- - [Requirements](#requirements)
10
- - [Installation](#installation)
11
- - [Dependencies](#dependencies)
12
- - [Authentication (apiKey / apiUrl)](#authentication-apikey--apiurl)
13
- - [Provider & router requirements at a glance](#provider--router-requirements-at-a-glance)
14
- - [Components](#components)
15
- - [Booking, search & forms](#booking-search--forms)
16
- - [Content & marketing](#content--marketing)
17
- - [Self-contained (non-React) bundles](#self-contained-non-react-bundles)
18
- - [Framework-specific notes](#framework-specific-notes)
19
-
20
- ## Requirements
21
-
22
- - Node 20 (an `.nvmrc` is present; if you use fnm or nvm, your shell should pick it up automatically)
23
- - React 18 (`react`, `react-dom` ^18.2.0)
24
- - TypeScript 5.x recommended — the package ships its own `.d.ts` declarations
25
-
26
- ## Installation
27
-
28
- ```bash
29
- npm install @qite/tide-components
30
- ```
31
-
32
- Then install the [peer dependencies](#dependencies) your package manager doesn't already resolve for you.
33
-
34
- ## Dependencies
35
-
36
- ### Peer dependencies (you must install these)
37
-
38
- These are **not** bundled into the build output (they're externalized via `rollup-plugin-peer-deps-external`) — your app supplies its own copy, which also avoids duplicate-React-instance issues.
39
-
40
- | Package | Version | Notes |
41
- | ------------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
- | `react` | `^18.2.0` | |
43
- | `react-dom` | `^18.2.0` | |
44
- | `react-redux` | `^9.2.0` | Needed even if you never touch Redux directly — `BookingWizard`, `QSM` and `SearchResults` create and provide their own stores internally |
45
- | `react-router` | `^6.30.3` | Only exercised by `BookingWizard`'s standard (non-self-contained) flow — see [Provider & router requirements](#provider--router-requirements-at-a-glance) |
46
- | `@reduxjs/toolkit` | `^2.8.2` | |
47
- | `formik` | `^2.2.9` | Used by `TideForm` and the booking traveler forms |
48
- | `immer` | `^9.0.5` | |
49
- | `lodash` | `^4.17.21` | |
50
- | `date-fns` | `^4.1.0` | |
51
- | `uuid` | `^11.1.0` | |
52
- | `@popperjs/core` | `^2.10.2` | Positioning for date pickers / dropdowns |
53
- | `react-popper` | `^2.2.5` | |
54
- | `flat` | `^5.0.2` | |
55
- | `@jsonurl/jsonurl` | `^1.1.4` | Used to serialize booking state into the URL |
56
-
57
- ### Bundled dependencies (no separate install needed)
58
-
59
- These ship inside `@qite/tide-components`'s build output already, so you don't add them to your own `package.json`:
60
-
61
- - `he`, `jwt-decode`, `react-html-comment`, `react-router-dom`, `signalr-no-jquery` (used internally for live flight-price updates in `SearchResults`), `yup`
62
- - The [Tide API client](#a-note-on-qitetide-client-and-typescript) runtime code
63
-
64
- ### A note on `@qite/tide-client` and TypeScript
65
-
66
- The Tide API client, `@qite/tide-client`, is bundled into the build at runtime — you do not need to install it for the components to work. However, several public prop/settings types reference its types directly (`Login`'s `tideClientConfig: TideClientConfig`, `Navbar`'s `member?: MemberInfo`, `QSM`'s `searchConfigurations`, `TideForm`'s `contexts`, `SearchResults`'s `tideConnection`, ...). **If you use TypeScript, add `@qite/tide-client` as a dev dependency too**, matching the version this package was built against, so those types resolve cleanly in your build. It isn't needed at runtime — only at compile time.
67
-
68
- ## Authentication (apiKey / apiUrl)
69
-
70
- Components that call Tide's API accept a Tide API key and host, under slightly different names depending on the component:
71
-
72
- | Component | Where |
73
- | --------------------------------------------- | ----------------------------------------------- |
74
- | `BookingProduct`, `BookingWizard`, `TideForm` | `settings.apiKey` / `settings.apiUrl` |
75
- | `SearchResults` | `configuration.tideConnection.apiKey` / `.host` |
76
- | `Login` | `tideClientConfig.apiKey` / `.host` |
77
-
78
- As of `@qite/tide-client@2.0.0`, that API key is exchanged transparently for a short-lived access token behind the scenes (token exchange, caching and refresh all happen inside the client) — you don't need to write any auth code yourself, just provide a valid key and host. See [API_KEY_MIGRATION.md](API_KEY_MIGRATION.md) for the full background on that model, including how a member/customer's own login token (from `Login`) takes over from the service API key once someone is logged in.
79
-
80
- ## Provider & router requirements at a glance
81
-
82
- Every component below is self-contained regarding state — none of them expect you to pass in a Redux store or context. The only two things you may need to add yourself:
83
-
84
- | Component | Needs `<BrowserRouter>` around it? | Sets up its own Redux store internally? |
85
- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
86
- | `BookingWizard` | **Yes** (standard flow — set `settings.skipRouter = true` to opt out, matching the self-contained bundle's behavior) | Yes |
87
- | `QSM` | No | Yes |
88
- | `SearchResults` | No | Yes |
89
- | `BookingProduct` | No | No (context only) |
90
- | `TideForm` | No | No (context only) |
91
- | `Login` | No | No |
92
- | All content/marketing components | No | No |
93
-
94
- ## Components
95
-
96
- ### Booking, search & forms
97
-
98
- #### `BookingProduct`
99
-
100
- A single bookable product (accommodation, excursion, ...) with its own date/room picker and pricing, typically shown in a list or product detail page.
101
-
102
- ```tsx
103
- <BookingProduct
104
- productCode="PTFSGROUPTOUR"
105
- productName="Soft Rock Hotel"
106
- rating={3.5}
107
- settings={{
108
- apiKey,
109
- apiUrl,
110
- officeId: 1,
111
- catalogueId: 1,
112
- language: 'en-GB',
113
- basePath: '/boeken',
114
- priceMode: 0,
115
- includeFlights: true,
116
- displayMode: 'calendar'
117
- }}
118
- />
119
- ```
120
-
121
- Key `settings` fields: `officeId`, `catalogueId`, `basePath`, `language`, `priceMode` (required); `apiKey`/`apiUrl`, `agentId`, `includeFlights`, `displayMode` (`'list' | 'calendar'`), `disableRooms`, `mainIcon`, `customTranslationsUrl`/`customTranslations`, `isOffer`, `alternativeActionText`/`alternativeAction` (optional).
122
-
123
- #### `BookingWizard`
124
-
125
- The full multi-step booking flow (options → travelers → summary → confirmation) for a product, driven by routes.
126
-
127
- ```tsx
128
- <BrowserRouter>
129
- <Routes>
130
- <Route
131
- path="/boeken/*"
132
- element={
133
- <BookingWizard
134
- productCode="HTFSSOFTROCK"
135
- productName="Soft Rock Hotel"
136
- thumbnailUrl="https://example.com/thumb.jpg"
137
- settings={{
138
- apiKey,
139
- apiUrl,
140
- officeId: 1,
141
- language: 'en-GB',
142
- basePath: '/boeken',
143
- productPath: '/',
144
- bookingOptions: { b2b: { entryStatus: 0 }, b2b2c: { entryStatus: 0 }, b2c: { entryStatus: 0 } },
145
- roomOptions: { pathSuffix: '/kamers' },
146
- flightOptions: { pathSuffix: '/vluchten' },
147
- options: { pathSuffix: '/extra' },
148
- travellers: { pathSuffix: '/reizigers' },
149
- summary: { pathSuffix: '/samenvatting', checkboxes: [] },
150
- confirmation: { pathSuffix: '/bevestiging' },
151
- error: { pathSuffix: '/mislukt' },
152
- companyContactEmail: 'info@example.com',
153
- companyContactPhone: '+32 000 00 00 00',
154
- showProductCardRating: false,
155
- showSidebarDeposit: true
156
- }}
157
- />
158
- }
159
- />
160
- </Routes>
161
- </BrowserRouter>
162
- ```
163
-
164
- Every step (`roomOptions`, `flightOptions`, `options`, `travellers`, `summary`, `confirmation`, `error`) is a required config block with at least a `pathSuffix`. Other notable `settings` fields: `skipBasePathInRouting` (omit `basePath` when building internal nav URLs — needed when the host already scopes routing itself, e.g. under a Gatsby `basePath`), `skipRouter` (bypass react-router entirely, see below), `translationFiles`/`customTranslationsUrl`, `hideAgentSelection`/`agentRequired`/`agentAdressId`, `enableVoucher`, `maxChildAge` (defaults to 17).
165
-
166
- Set `skipRouter: true` to run `BookingWizard` without a `<BrowserRouter>` ancestorstep transitions are then driven by Redux state instead of routes (this is exactly what the [self-contained bundle](#self-contained-non-react-bundles) does).
167
-
168
- #### `QSM` (Quick Search Module)
169
-
170
- A search form (flights, hotels, round trips, group tours) that hands its result off via `onSubmit` QSM itself never calls the booking/search-results API, it just collects and validates search criteria.
171
-
172
- ```tsx
173
- <QSM
174
- configuration={{
175
- searchConfigurations,
176
- askTravelers: true,
177
- askNationality: true,
178
- nationalities,
179
- allowOneWay: true,
180
- allowRoundtrip: true,
181
- departureAirport: { fieldKey: 'selectedDepartureAirport', label: 'Departure', options: originAirports, autoComplete: true },
182
- destinationAirport: { fieldKey: 'selectedDestinationAirport', label: 'Destination', options: destinationAirports },
183
- onSubmit: (data) => navigate(buildSearchResultsUrl(data)),
184
- submitIcon: <SearchIcon />
185
- }}
186
- />
187
- ```
188
-
189
- #### `SearchResults`
190
-
191
- Renders search results (flights, hotels, round trips, packages) for a given search configuration and lets the user start a booking.
192
-
193
- ```tsx
194
- <SearchResults
195
- configuration={{
196
- tideConnection: { host: apiUrl, apiKey, catalogueIds: [1], officeId: 1 },
197
- searchConfiguration,
198
- showFilters: true,
199
- showFlightResults: true,
200
- showTabViews: true
201
- }}
202
- onBookingStarted={() => setIsBooking(true)}
203
- />
204
- ```
205
-
206
- Flight results use SignalR for live price updates (`signalr-no-jquery`, bundled — no setup required on your side).
207
-
208
- #### `TideForm`
209
-
210
- Renders a Tide-configured dynamic web form (e.g. a contact/quote request form tied to a product).
211
-
212
- ```tsx
213
- <TideForm
214
- configuration={{
215
- id: 1,
216
- languageCode: 'en-GB',
217
- apiKey,
218
- apiUrl,
219
- contexts: [{ key: 'product', identifier: 'ETFSSTARGAZE' }],
220
- initialValues: { travellers: { traveller_counts: { adult_count: 2, child_count: 0 } } }
221
- }}
222
- />
223
- ```
224
-
225
- #### `Login`
226
-
227
- Member login / password reset / account confirmation. `Login` doesn't manage navigation between those three modes itself — you flip between them with the boolean props based on your own routing.
228
-
229
- ```tsx
230
- const [member, setMember] = useState<MemberInfo>();
231
-
232
- <Login
233
- tideClientConfig={{ host: apiUrl, apiKey, catalogueIds: [1] }}
234
- portalId={0}
235
- languageCode="en-GB"
236
- isLoginPage={true}
237
- isResetPassword={false}
238
- isMemberConfirmation={false}
239
- member={member}
240
- setMember={setMember}
241
- handleBackToHome={() => navigate('/')}
242
- handleBackToLogin={() => navigate('/login')}
243
- />;
244
- ```
245
-
246
- Once `setMember` receives a logged-in `MemberInfo`, pass that same object into [`Navbar`](#navbar)'s `member` prop to reflect the logged-in state in your header.
247
-
248
- ### Content & marketing
249
-
250
- These are presentational — plain props in, JSX out, no Tide API calls, no context or store required — unless noted otherwise.
251
-
252
- #### `Header`
253
-
254
- Hero/banner section with a video, image, or slider background.
255
-
256
- ```tsx
257
- <Header
258
- media={{ type: 'image', src: '/hero.jpg', alt: 'Sunset over the coast' }}
259
- title="Explore the world with us"
260
- description="Discover breathtaking destinations, curated just for you."
261
- showButton
262
- buttonText="See offers"
263
- onButtonClick={() => navigate('/offers')}
264
- />
265
- ```
266
-
267
- #### `Navbar`
268
-
269
- Site navigation, including language switcher and login/logout state. Navbar is purely presentational — you own fetching/holding `MemberInfo` (e.g. from `Login`) and pass it in.
270
-
271
- ```tsx
272
- <Navbar
273
- logo={<Logo />}
274
- topLinks={topLinks}
275
- items={navItems}
276
- language={language}
277
- languages={languages}
278
- onLanguageChange={setLanguage}
279
- onSearch={(query) => navigate(`/search?q=${query}`)}
280
- member={member}
281
- onLogin={() => navigate('/login')}
282
- onLogout={() => setMember(undefined)}
283
- />
284
- ```
285
-
286
- #### `Footer`
287
-
288
- Site footer with columns, social links and payment icons. Every prop is optional — `<Footer />` renders sensible defaults.
289
-
290
- ```tsx
291
- <Footer />
292
- ```
293
-
294
- #### `ImageCardGrid`
295
-
296
- A grid of image cards with an optional call-to-action per card.
297
-
298
- ```tsx
299
- <ImageCardGrid
300
- title="Popular destinations"
301
- columns={4}
302
- cards={[{ image: '/santorini.jpg', title: 'Santorini', buttonText: 'Book now', onButtonClick: (card) => navigate(`/product/${card.title}`) }]}
303
- />
304
- ```
305
-
306
- #### `ImageWithTextSection`
307
-
308
- Alternating image + two-column text sections, e.g. for describing activities.
309
-
310
- ```tsx
311
- <ImageWithTextSection
312
- title="Activities"
313
- cards={[
314
- {
315
- imageSrc: '/activity.jpg',
316
- imageAlt: 'Hiking trail',
317
- title: 'Discover Santorini',
318
- section1Title: 'The activity',
319
- section1Text: '...',
320
- section2Title: 'Details',
321
- section2Text: '...',
322
- buttonText: 'Book now'
323
- }
324
- ]}
325
- />
326
- ```
327
-
328
- #### `Slider`
329
-
330
- A simple image carousel; also used internally by `Header` for `media.type: 'slider'`.
331
-
332
- ```tsx
333
- <Slider images={['/1.jpg', '/2.jpg', '/3.jpg']} mode="auto" intervalMs={4000} />
334
- ```
335
-
336
- #### `PhotoGallery`
337
-
338
- A titled image gallery with a lightbox.
339
-
340
- ```tsx
341
- <PhotoGallery title="Project gallery" images={[{ src: '/lake.jpg', alt: 'Mountain lake', caption: 'Morning hike' }]} />
342
- ```
343
-
344
- #### `Breadcrumbs`
345
-
346
- ```tsx
347
- <Breadcrumbs
348
- items={[
349
- { href: '/', label: 'Home' },
350
- { href: '/products', label: 'Products' },
351
- { label: 'Santorini', isCurrent: true }
352
- ]}
353
- />
354
- ```
355
-
356
- #### `FAQ`
357
-
358
- Renders a list of question/answer pairs as an accordion.
359
-
360
- ```tsx
361
- <FAQ title="Frequently asked questions" items={[{ question: 'How do I cancel?', answer: <p>Contact us at least 48h in advance.</p> }]} />
362
- ```
363
-
364
- #### `FeaturedTrips`
365
-
366
- A row of highlighted trip cards.
367
-
368
- ```tsx
369
- <FeaturedTrips
370
- title="Featured trips"
371
- cards={[{ imageSrc: '/santorini.jpg', imageAlt: 'Santorini', title: 'Santorini getaway', location: 'Greece', onButtonClick: () => navigate('/santorini') }]}
372
- />
373
- ```
374
-
375
- #### `Spinner`
376
-
377
- Loading indicator, localized via the `SearchResultsConfigurationContext` when rendered inside `SearchResults` (falls back to English otherwise).
378
-
379
- ```tsx
380
- {
381
- isLoading ? <Spinner /> : <SearchResults configuration={configuration} />;
382
- }
383
- ```
384
-
385
- #### `ItineraryMapView`
386
-
387
- An interactive multi-destination itinerary builder with a map. Loads Leaflet's CSS/JS from a CDN at runtime, so it needs outbound network access to `unpkg.com` and map tile servers — all itinerary state is local to the component; callbacks are notifications, your app decides what to do with them.
388
-
389
- ```tsx
390
- <ItineraryMapView
391
- searchResults={searchResults}
392
- onSearchChange={(query) => setSearchResults(searchDestinations(query))}
393
- onAddDestination={(name) => addDestination(name)}
394
- onContinue={() => navigate('/booking')}
395
- />
396
- ```
397
-
398
- ## Self-contained (non-React) bundles
399
-
400
- For host pages that aren't React apps, `selfcontained/` ships pre-bundled scripts you drop in with a `<script>` tag; each looks for a specific container element, reads its configuration from that element's attributes (or the URL query string), and mounts itself — no React setup needed on the host page.
401
-
402
- | Bundle | Mounts into | Configuration source |
403
- | ---------------------------------------------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
404
- | `booking-product` | `#tide-product` | Attributes on the settings element + query string (`productCode`/`productName`) |
405
- | `booking-wizard` | `#tide-booking` | Attributes on the settings element + query string; runs with `skipRouter: true` (no `<BrowserRouter>` needed) |
406
- | `form` | `#tide-form` | Attributes on the settings element (`initialValues`/`contexts` as JSON strings) |
407
- | `flight-search-results`, `hotel-search-results`, `hotel-flight-search-results`, `roundtrip-search-results` | `#tide-*-search-results` | **Currently hardcoded demo configuration** in-file, not attribute-driven yet — treat these as reference implementations to copy/adapt rather than drop-in embeds |
408
- | `content-blocks` | `#tide-content-blocks` | Fixed demo content; only `Navbar`, `QSM`, `Slider` and a contact form are real library components — the rest of that page is hand-written markup that visually mimics `Header`/`Footer`/`ImageCardGrid`/`ImageWithTextSection` |
409
-
410
- ## Framework-specific notes
411
-
412
- **Gatsby (or any SSR framework):** when using `BookingWizard` on a Gatsby v5+ site:
413
-
414
- - Use React 18 and `react-router-dom@6`.
415
- - Import only from `@qite/tide-components` at the top level and keep the actual render client-side (e.g. behind `typeof window !== 'undefined'`, or in a client-only wrapper) — `signalr-no-jquery` touches `window` and will error if evaluated during SSR/build.
416
- - Surround `<BookingWizard>` with `<BrowserRouter basename={basePath}>` to handle its internal routing.
417
- - Set `skipBasePathInRouting: true` in `settings` to avoid duplicate base paths in the wizard's internal navigation.
418
-
419
- ---
420
-
421
- Working on this library itself, rather than consuming it? See [DEVELOPMENT.md](DEVELOPMENT.md) for local setup (not included in the published package).
1
+ # @qite/tide-components
2
+
3
+ React component library for Tide. What started as a single booking wizard component has grown into a full set of building blocks used across Tide-integrated sites: booking flows, search results, quick search modules (QSM), dynamic forms, member login, and marketing/content blocks (navbar, header, footer, image grids, sliders, FAQ, ...).
4
+
5
+ Every component that talks to Tide's API is a **plain React component** — you own the page/routing, this library owns the Tide-specific UI and API calls.
6
+
7
+ ## Table of contents
8
+
9
+ - [Requirements](#requirements)
10
+ - [Installation](#installation)
11
+ - [Dependencies](#dependencies)
12
+ - [Authentication (apiKey / apiUrl)](#authentication-apikey--apiurl)
13
+ - [Provider & router requirements at a glance](#provider--router-requirements-at-a-glance)
14
+ - [Components](#components)
15
+ - [Booking, search & forms](#booking-search--forms)
16
+ - [Content & marketing](#content--marketing)
17
+ - [Self-contained (non-React) bundles](#self-contained-non-react-bundles)
18
+ - [Framework-specific notes](#framework-specific-notes)
19
+
20
+ ## Requirements
21
+
22
+ - Node 20 (an `.nvmrc` is present; if you use fnm or nvm, your shell should pick it up automatically)
23
+ - React 18 (`react`, `react-dom` ^18.2.0)
24
+ - TypeScript 5.x recommended — the package ships its own `.d.ts` declarations
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ npm install @qite/tide-components
30
+ ```
31
+
32
+ Then install the [peer dependencies](#dependencies) your package manager doesn't already resolve for you.
33
+
34
+ ## Dependencies
35
+
36
+ ### Peer dependencies (you must install these)
37
+
38
+ These are **not** bundled into the build output (they're externalized via `rollup-plugin-peer-deps-external`) — your app supplies its own copy, which also avoids duplicate-React-instance issues.
39
+
40
+ | Package | Version | Notes |
41
+ |---|---|---|
42
+ | `react` | `^18.2.0` | |
43
+ | `react-dom` | `^18.2.0` | |
44
+ | `react-redux` | `^9.2.0` | Needed even if you never touch Redux directly — `BookingWizard`, `QSM` and `SearchResults` create and provide their own stores internally |
45
+ | `react-router` | `^6.30.3` | Only exercised by `BookingWizard`'s standard (non-self-contained) flow — see [Provider & router requirements](#provider--router-requirements-at-a-glance) |
46
+ | `@reduxjs/toolkit` | `^2.8.2` | |
47
+ | `formik` | `^2.2.9` | Used by `TideForm` and the booking traveler forms |
48
+ | `immer` | `^9.0.5` | |
49
+ | `lodash` | `^4.17.21` | |
50
+ | `date-fns` | `^4.1.0` | |
51
+ | `uuid` | `^11.1.0` | |
52
+ | `@popperjs/core` | `^2.10.2` | Positioning for date pickers / dropdowns |
53
+ | `react-popper` | `^2.2.5` | |
54
+ | `flat` | `^5.0.2` | |
55
+ | `@jsonurl/jsonurl` | `^1.1.4` | Used to serialize booking state into the URL |
56
+
57
+ ### Bundled dependencies (no separate install needed)
58
+
59
+ These ship inside `@qite/tide-components`'s build output already, so you don't add them to your own `package.json`:
60
+
61
+ - `he`, `jwt-decode`, `react-html-comment`, `react-router-dom`, `signalr-no-jquery` (used internally for live flight-price updates in `SearchResults`), `yup`
62
+ - The [Tide API client](#a-note-on-qitetide-client-and-typescript) runtime code
63
+
64
+ ### A note on `@qite/tide-client` and TypeScript
65
+
66
+ The Tide API client, `@qite/tide-client`, is bundled into the build at runtime — you do not need to install it for the components to work. However, several public prop/settings types reference its types directly (`Login`'s `tideClientConfig: TideClientConfig`, `Navbar`'s `member?: MemberInfo`, `QSM`'s `searchConfigurations`, `TideForm`'s `contexts`, `SearchResults`'s `tideConnection`, ...). **If you use TypeScript, add `@qite/tide-client` as a dev dependency too**, matching the version this package was built against, so those types resolve cleanly in your build. It isn't needed at runtime — only at compile time.
67
+
68
+ ## Authentication (apiKey / apiUrl)
69
+
70
+ Components that call Tide's API accept a Tide API key and host, under slightly different names depending on the component:
71
+
72
+ | Component | Where |
73
+ |---|---|
74
+ | `BookingProduct`, `BookingWizard`, `TideForm` | `settings.apiKey` / `settings.apiUrl` |
75
+ | `SearchResults` | `configuration.tideConnection.apiKey` / `.host` |
76
+ | `Login` | `tideClientConfig.apiKey` / `.host` |
77
+
78
+ As of `@qite/tide-client@2.0.0`, that API key is exchanged transparently for a short-lived access token behind the scenes (token exchange, caching and refresh all happen inside the client) — you don't need to write any auth code yourself, just provide a valid key and host. See [API_KEY_MIGRATION.md](API_KEY_MIGRATION.md) for the full background on that model, including how a member/customer's own login token (from `Login`) takes over from the service API key once someone is logged in.
79
+
80
+ ## Provider & router requirements at a glance
81
+
82
+ Every component below is self-contained regarding state — none of them expect you to pass in a Redux store or context. The only two things you may need to add yourself:
83
+
84
+ | Component | Needs `<BrowserRouter>` around it? | Sets up its own Redux store internally? |
85
+ |---|---|---|
86
+ | `BookingWizard` | **Yes** (standard flow — set `settings.skipRouter = true` to opt out, matching the self-contained bundle's behavior) | Yes |
87
+ | `QSM` | No | Yes |
88
+ | `SearchResults` | No | Yes |
89
+ | `BookingProduct` | No | No (context only) |
90
+ | `TideForm` | No | No (context only) |
91
+ | `Login` | No | No |
92
+ | All content/marketing components | No | No |
93
+
94
+ ## Components
95
+
96
+ ### Booking, search & forms
97
+
98
+ #### `BookingProduct`
99
+
100
+ A single bookable product (accommodation, excursion, ...) with its own date/room picker and pricing, typically shown in a list or product detail page.
101
+
102
+ ```tsx
103
+ <BookingProduct
104
+ productCode="PTFSGROUPTOUR"
105
+ productName="Soft Rock Hotel"
106
+ rating={3.5}
107
+ settings={{
108
+ apiKey, apiUrl,
109
+ officeId: 1,
110
+ catalogueId: 1,
111
+ language: 'en-GB',
112
+ basePath: '/boeken',
113
+ priceMode: 0,
114
+ includeFlights: true,
115
+ displayMode: 'calendar'
116
+ }}
117
+ />
118
+ ```
119
+
120
+ Key `settings` fields: `officeId`, `catalogueId`, `basePath`, `language`, `priceMode` (required); `apiKey`/`apiUrl`, `agentId`, `includeFlights`, `displayMode` (`'list' | 'calendar'`), `disableRooms`, `mainIcon`, `customTranslationsUrl`/`customTranslations`, `isOffer`, `alternativeActionText`/`alternativeAction` (optional).
121
+
122
+ #### `BookingWizard`
123
+
124
+ The full multi-step booking flow (options → travelers → summary → confirmation) for a product, driven by routes.
125
+
126
+ ```tsx
127
+ <BrowserRouter>
128
+ <Routes>
129
+ <Route
130
+ path="/boeken/*"
131
+ element={
132
+ <BookingWizard
133
+ productCode="HTFSSOFTROCK"
134
+ productName="Soft Rock Hotel"
135
+ thumbnailUrl="https://example.com/thumb.jpg"
136
+ settings={{
137
+ apiKey, apiUrl,
138
+ officeId: 1,
139
+ language: 'en-GB',
140
+ basePath: '/boeken',
141
+ productPath: '/',
142
+ bookingOptions: { b2b: { entryStatus: 0 }, b2b2c: { entryStatus: 0 }, b2c: { entryStatus: 0 } },
143
+ roomOptions: { pathSuffix: '/kamers' },
144
+ flightOptions: { pathSuffix: '/vluchten' },
145
+ options: { pathSuffix: '/extra' },
146
+ travellers: { pathSuffix: '/reizigers' },
147
+ summary: { pathSuffix: '/samenvatting', checkboxes: [] },
148
+ confirmation: { pathSuffix: '/bevestiging' },
149
+ error: { pathSuffix: '/mislukt' },
150
+ companyContactEmail: 'info@example.com',
151
+ companyContactPhone: '+32 000 00 00 00',
152
+ showProductCardRating: false,
153
+ showSidebarDeposit: true
154
+ }}
155
+ />
156
+ }
157
+ />
158
+ </Routes>
159
+ </BrowserRouter>
160
+ ```
161
+
162
+ Every step (`roomOptions`, `flightOptions`, `options`, `travellers`, `summary`, `confirmation`, `error`) is a required config block with at least a `pathSuffix`. Other notable `settings` fields: `skipBasePathInRouting` (omit `basePath` when building internal nav URLs — needed when the host already scopes routing itself, e.g. under a Gatsby `basePath`), `skipRouter` (bypass react-router entirely, see below), `translationFiles`/`customTranslationsUrl`, `hideAgentSelection`/`agentRequired`/`agentAdressId`, `enableVoucher`, `maxChildAge` (defaults to 17), `maxBabyAge` (defaults to 1).
163
+
164
+ Set `skipRouter: true` to run `BookingWizard` without a `<BrowserRouter>` ancestor step transitions are then driven by Redux state instead of routes (this is exactly what the [self-contained bundle](#self-contained-non-react-bundles) does).
165
+
166
+ **Traveler age thresholds.** `maxChildAge` and `maxBabyAge` decide how each booked traveler is presented and validated in the travelers step: above `maxChildAge` is an adult, at or below it a child, and at or below `maxBabyAge` a baby. A baby gets its own label instead of the child one, and its date of birth has to be under `maxBabyAge + 1` on the **departure** dateso with the default of 1, "younger than 2 when the trip starts". Children are checked against the return date instead, as before. Both settings are also available as attributes (`maxChildAge`, `maxBabyAge`) on the self-contained booking-wizard bundle.
167
+
168
+ These thresholds are compared against the age of the pax in the booking request — the `childAges` your search sent — not against what the traveler types in. A site that books babies as age 2 therefore needs `maxBabyAge: 2` for them to be recognized, which also loosens the date-of-birth rule to "younger than 3 on departure".
169
+
170
+ **Translation overrides** (`translationFiles`, `customTranslationsUrl`) are deep-merged over the built-in translations, so you only supply the keys you want to replace, including inside nested groups such as `TRAVELERS_FORM.VALIDATION`. Before `1.4.131` the merge was one level deep per section: overriding a nested group meant every built-in key you left out of it went missing, so if you worked around that by copying a whole group into your override file, you can trim it back to just your own keys.
171
+
172
+ #### `QSM` (Quick Search Module)
173
+
174
+ A search form (flights, hotels, round trips, group tours) that hands its result off via `onSubmit` — QSM itself never calls the booking/search-results API, it just collects and validates search criteria.
175
+
176
+ ```tsx
177
+ <QSM
178
+ configuration={{
179
+ searchConfigurations,
180
+ askTravelers: true,
181
+ askNationality: true,
182
+ nationalities,
183
+ allowOneWay: true,
184
+ allowRoundtrip: true,
185
+ departureAirport: { fieldKey: 'selectedDepartureAirport', label: 'Departure', options: originAirports, autoComplete: true },
186
+ destinationAirport: { fieldKey: 'selectedDestinationAirport', label: 'Destination', options: destinationAirports },
187
+ onSubmit: (data) => navigate(buildSearchResultsUrl(data)),
188
+ submitIcon: <SearchIcon />
189
+ }}
190
+ />
191
+ ```
192
+
193
+ #### `SearchResults`
194
+
195
+ Renders search results (flights, hotels, round trips, packages) for a given search configuration and lets the user start a booking.
196
+
197
+ ```tsx
198
+ <SearchResults
199
+ configuration={{
200
+ tideConnection: { host: apiUrl, apiKey, catalogueIds: [1], officeId: 1 },
201
+ searchConfiguration,
202
+ showFilters: true,
203
+ showFlightResults: true,
204
+ showTabViews: true
205
+ }}
206
+ onBookingStarted={() => setIsBooking(true)}
207
+ />
208
+ ```
209
+
210
+ Flight results use SignalR for live price updates (`signalr-no-jquery`, bundled no setup required on your side).
211
+
212
+ #### `TideForm`
213
+
214
+ Renders a Tide-configured dynamic web form (e.g. a contact/quote request form tied to a product).
215
+
216
+ ```tsx
217
+ <TideForm
218
+ configuration={{
219
+ id: 1,
220
+ languageCode: 'en-GB',
221
+ apiKey, apiUrl,
222
+ contexts: [{ key: 'product', identifier: 'ETFSSTARGAZE' }],
223
+ initialValues: { travellers: { traveller_counts: { adult_count: 2, child_count: 0 } } }
224
+ }}
225
+ />
226
+ ```
227
+
228
+ #### `Login`
229
+
230
+ Member login / password reset / account confirmation. `Login` doesn't manage navigation between those three modes itself — you flip between them with the boolean props based on your own routing.
231
+
232
+ ```tsx
233
+ const [member, setMember] = useState<MemberInfo>();
234
+
235
+ <Login
236
+ tideClientConfig={{ host: apiUrl, apiKey, catalogueIds: [1] }}
237
+ portalId={0}
238
+ languageCode="en-GB"
239
+ isLoginPage={true}
240
+ isResetPassword={false}
241
+ isMemberConfirmation={false}
242
+ member={member}
243
+ setMember={setMember}
244
+ handleBackToHome={() => navigate('/')}
245
+ handleBackToLogin={() => navigate('/login')}
246
+ />
247
+ ```
248
+
249
+ Once `setMember` receives a logged-in `MemberInfo`, pass that same object into [`Navbar`](#navbar)'s `member` prop to reflect the logged-in state in your header.
250
+
251
+ ### Content & marketing
252
+
253
+ These are presentational — plain props in, JSX out, no Tide API calls, no context or store required — unless noted otherwise.
254
+
255
+ #### `Header`
256
+ Hero/banner section with a video, image, or slider background.
257
+ ```tsx
258
+ <Header
259
+ media={{ type: 'image', src: '/hero.jpg', alt: 'Sunset over the coast' }}
260
+ title="Explore the world with us"
261
+ description="Discover breathtaking destinations, curated just for you."
262
+ showButton
263
+ buttonText="See offers"
264
+ onButtonClick={() => navigate('/offers')}
265
+ />
266
+ ```
267
+
268
+ #### `Navbar`
269
+ Site navigation, including language switcher and login/logout state. Navbar is purely presentational — you own fetching/holding `MemberInfo` (e.g. from `Login`) and pass it in.
270
+ ```tsx
271
+ <Navbar
272
+ logo={<Logo />}
273
+ topLinks={topLinks}
274
+ items={navItems}
275
+ language={language}
276
+ languages={languages}
277
+ onLanguageChange={setLanguage}
278
+ onSearch={(query) => navigate(`/search?q=${query}`)}
279
+ member={member}
280
+ onLogin={() => navigate('/login')}
281
+ onLogout={() => setMember(undefined)}
282
+ />
283
+ ```
284
+
285
+ #### `Footer`
286
+ Site footer with columns, social links and payment icons. Every prop is optional — `<Footer />` renders sensible defaults.
287
+ ```tsx
288
+ <Footer />
289
+ ```
290
+
291
+ #### `ImageCardGrid`
292
+ A grid of image cards with an optional call-to-action per card.
293
+ ```tsx
294
+ <ImageCardGrid
295
+ title="Popular destinations"
296
+ columns={4}
297
+ cards={[{ image: '/santorini.jpg', title: 'Santorini', buttonText: 'Book now', onButtonClick: (card) => navigate(`/product/${card.title}`) }]}
298
+ />
299
+ ```
300
+
301
+ #### `ImageWithTextSection`
302
+ Alternating image + two-column text sections, e.g. for describing activities.
303
+ ```tsx
304
+ <ImageWithTextSection
305
+ title="Activities"
306
+ cards={[{
307
+ imageSrc: '/activity.jpg', imageAlt: 'Hiking trail',
308
+ title: 'Discover Santorini', section1Title: 'The activity', section1Text: '...',
309
+ section2Title: 'Details', section2Text: '...', buttonText: 'Book now'
310
+ }]}
311
+ />
312
+ ```
313
+
314
+ #### `Slider`
315
+ A simple image carousel; also used internally by `Header` for `media.type: 'slider'`.
316
+ ```tsx
317
+ <Slider images={['/1.jpg', '/2.jpg', '/3.jpg']} mode="auto" intervalMs={4000} />
318
+ ```
319
+
320
+ #### `PhotoGallery`
321
+ A titled image gallery with a lightbox.
322
+ ```tsx
323
+ <PhotoGallery
324
+ title="Project gallery"
325
+ images={[{ src: '/lake.jpg', alt: 'Mountain lake', caption: 'Morning hike' }]}
326
+ />
327
+ ```
328
+
329
+ #### `Breadcrumbs`
330
+ ```tsx
331
+ <Breadcrumbs items={[{ href: '/', label: 'Home' }, { href: '/products', label: 'Products' }, { label: 'Santorini', isCurrent: true }]} />
332
+ ```
333
+
334
+ #### `FAQ`
335
+ Renders a list of question/answer pairs as an accordion.
336
+ ```tsx
337
+ <FAQ title="Frequently asked questions" items={[{ question: 'How do I cancel?', answer: <p>Contact us at least 48h in advance.</p> }]} />
338
+ ```
339
+
340
+ #### `FeaturedTrips`
341
+ A row of highlighted trip cards.
342
+ ```tsx
343
+ <FeaturedTrips
344
+ title="Featured trips"
345
+ cards={[{ imageSrc: '/santorini.jpg', imageAlt: 'Santorini', title: 'Santorini getaway', location: 'Greece', onButtonClick: () => navigate('/santorini') }]}
346
+ />
347
+ ```
348
+
349
+ #### `Spinner`
350
+ Loading indicator, localized via the `SearchResultsConfigurationContext` when rendered inside `SearchResults` (falls back to English otherwise).
351
+ ```tsx
352
+ {isLoading ? <Spinner /> : <SearchResults configuration={configuration} />}
353
+ ```
354
+
355
+ #### `ItineraryMapView`
356
+ An interactive multi-destination itinerary builder with a map. Loads Leaflet's CSS/JS from a CDN at runtime, so it needs outbound network access to `unpkg.com` and map tile servers — all itinerary state is local to the component; callbacks are notifications, your app decides what to do with them.
357
+ ```tsx
358
+ <ItineraryMapView
359
+ searchResults={searchResults}
360
+ onSearchChange={(query) => setSearchResults(searchDestinations(query))}
361
+ onAddDestination={(name) => addDestination(name)}
362
+ onContinue={() => navigate('/booking')}
363
+ />
364
+ ```
365
+
366
+ ## Self-contained (non-React) bundles
367
+
368
+ For host pages that aren't React apps, `selfcontained/` ships pre-bundled scripts you drop in with a `<script>` tag; each looks for a specific container element, reads its configuration from that element's attributes (or the URL query string), and mounts itself — no React setup needed on the host page.
369
+
370
+ Three of these are published to Tide's CDN, each under its own per-version path (plus a `latest` alias):
371
+
372
+ | Bundle | Mounts into | CDN component | CDN path |
373
+ |---|---|---|---|
374
+ | `form` | `#tide-form` | `form` | `https://cdn.tidesoftware.be/components/form/{version}/` (or `/latest/`) |
375
+ | `booking-wizard` | `#tide-booking` | `booking` | `https://cdn.tidesoftware.be/components/booking/{version}/` (or `/latest/`) |
376
+ | `booking-product` | `#tide-product` | `product` | `https://cdn.tidesoftware.be/components/product/{version}/` (or `/latest/`) |
377
+
378
+ Each CDN version directory contains `bundle.js`, `bundle.css`, `bundle.js.LICENSE.txt`, `variables.css`, and a `changelog.txt` listing what changed in that version — sourced from the matching `changelog.txt` in each bundle's folder under `selfcontained/` in this repo.
379
+
380
+ Configuration is read from attributes on the settings element (plus the URL query string for a couple of fields). `booking-wizard` additionally runs with `skipRouter: true` in this mode, so it needs no `<BrowserRouter>`; `form` and `booking-product` don't use routing at all.
381
+
382
+ ## Framework-specific notes
383
+
384
+ **Gatsby (or any SSR framework):** when using `BookingWizard` on a Gatsby v5+ site:
385
+ - Use React 18 and `react-router-dom@6`.
386
+ - Import only from `@qite/tide-components` at the top level and keep the actual render client-side (e.g. behind `typeof window !== 'undefined'`, or in a client-only wrapper) — `signalr-no-jquery` touches `window` and will error if evaluated during SSR/build.
387
+ - Surround `<BookingWizard>` with `<BrowserRouter basename={basePath}>` to handle its internal routing.
388
+ - Set `skipBasePathInRouting: true` in `settings` to avoid duplicate base paths in the wizard's internal navigation.
389
+
390
+ ---
391
+
392
+ Working on this library itself, rather than consuming it? See [DEVELOPMENT.md](DEVELOPMENT.md) for local setup (not included in the published package).