@qite/tide-components
v1.0.15
Published
React components for Tide (booking, product availability, form, ...)
Readme
@qite/tide-components
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, ...).
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.
Table of contents
- Requirements
- Installation
- Dependencies
- Authentication (apiKey / apiUrl)
- Provider & router requirements at a glance
- Components
- Self-contained (non-React) bundles
- Framework-specific notes
Requirements
- Node 20 (an
.nvmrcis present; if you use fnm or nvm, your shell should pick it up automatically) - React 18 (
react,react-dom^18.2.0) - TypeScript 5.x recommended — the package ships its own
.d.tsdeclarations
Installation
npm install @qite/tide-componentsThen install the peer dependencies your package manager doesn't already resolve for you.
Dependencies
Peer dependencies (you must install these)
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.
| Package | Version | Notes |
|---|---|---|
| react | ^18.2.0 | |
| react-dom | ^18.2.0 | |
| react-redux | ^9.2.0 | Needed even if you never touch Redux directly — BookingWizard, QSM and SearchResults create and provide their own stores internally |
| react-router | ^6.30.3 | Only exercised by BookingWizard's standard (non-self-contained) flow — see Provider & router requirements |
| @reduxjs/toolkit | ^2.8.2 | |
| formik | ^2.2.9 | Used by TideForm and the booking traveler forms |
| immer | ^9.0.5 | |
| lodash | ^4.17.21 | |
| date-fns | ^4.1.0 | |
| uuid | ^11.1.0 | |
| @popperjs/core | ^2.10.2 | Positioning for date pickers / dropdowns |
| react-popper | ^2.2.5 | |
| flat | ^5.0.2 | |
| @jsonurl/jsonurl | ^1.1.4 | Used to serialize booking state into the URL |
Bundled dependencies (no separate install needed)
These ship inside @qite/tide-components's build output already, so you don't add them to your own package.json:
he,jwt-decode,react-html-comment,react-router-dom,signalr-no-jquery(used internally for live flight-price updates inSearchResults),yup- The Tide API client runtime code
A note on @qite/tide-client and TypeScript
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.
Authentication (apiKey / apiUrl)
Components that call Tide's API accept a Tide API key and host, under slightly different names depending on the component:
| Component | Where |
|---|---|
| BookingProduct, BookingWizard, TideForm | settings.apiKey / settings.apiUrl |
| SearchResults | configuration.tideConnection.apiKey / .host |
| Login | tideClientConfig.apiKey / .host |
As of @qite/[email protected], 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 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.
Provider & router requirements at a glance
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:
| Component | Needs <BrowserRouter> around it? | Sets up its own Redux store internally? |
|---|---|---|
| BookingWizard | Yes (standard flow — set settings.skipRouter = true to opt out, matching the self-contained bundle's behavior) | Yes |
| QSM | No | Yes |
| SearchResults | No | Yes |
| BookingProduct | No | No (context only) |
| TideForm | No | No (context only) |
| Login | No | No |
| All content/marketing components | No | No |
Components
Booking, search & forms
BookingProduct
A single bookable product (accommodation, excursion, ...) with its own date/room picker and pricing, typically shown in a list or product detail page.
<BookingProduct
productCode="PTFSGROUPTOUR"
productName="Soft Rock Hotel"
rating={3.5}
settings={{
apiKey, apiUrl,
officeId: 1,
catalogueId: 1,
language: 'en-GB',
basePath: '/boeken',
priceMode: 0,
includeFlights: true,
displayMode: 'calendar'
}}
/>Key settings fields: officeId, catalogueId, basePath, language, priceMode (required); apiKey/apiUrl, agentId, includeFlights, displayMode ('list' | 'calendar'), disableRooms, mainIcon, customTranslationsUrl/customTranslations, isOffer, alternativeActionText/alternativeAction (optional).
BookingWizard
The full multi-step booking flow (options → travelers → summary → confirmation) for a product, driven by routes.
<BrowserRouter>
<Routes>
<Route
path="/boeken/*"
element={
<BookingWizard
productCode="HTFSSOFTROCK"
productName="Soft Rock Hotel"
thumbnailUrl="https://example.com/thumb.jpg"
settings={{
apiKey, apiUrl,
officeId: 1,
language: 'en-GB',
basePath: '/boeken',
productPath: '/',
bookingOptions: { b2b: { entryStatus: 0 }, b2b2c: { entryStatus: 0 }, b2c: { entryStatus: 0 } },
roomOptions: { pathSuffix: '/kamers' },
flightOptions: { pathSuffix: '/vluchten' },
options: { pathSuffix: '/extra' },
travellers: { pathSuffix: '/reizigers' },
summary: { pathSuffix: '/samenvatting', checkboxes: [] },
confirmation: { pathSuffix: '/bevestiging' },
error: { pathSuffix: '/mislukt' },
companyContactEmail: '[email protected]',
companyContactPhone: '+32 000 00 00 00',
showProductCardRating: false,
showSidebarDeposit: true
}}
/>
}
/>
</Routes>
</BrowserRouter>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).
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 does).
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 date — so 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.
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".
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.
QSM (Quick Search Module)
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.
<QSM
configuration={{
searchConfigurations,
askTravelers: true,
askNationality: true,
nationalities,
allowOneWay: true,
allowRoundtrip: true,
departureAirport: { fieldKey: 'selectedDepartureAirport', label: 'Departure', options: originAirports, autoComplete: true },
destinationAirport: { fieldKey: 'selectedDestinationAirport', label: 'Destination', options: destinationAirports },
onSubmit: (data) => navigate(buildSearchResultsUrl(data)),
submitIcon: <SearchIcon />
}}
/>SearchResults
Renders search results (flights, hotels, round trips, packages) for a given search configuration and lets the user start a booking.
<SearchResults
configuration={{
tideConnection: { host: apiUrl, apiKey, catalogueIds: [1], officeId: 1 },
searchConfiguration,
showFilters: true,
showFlightResults: true,
showTabViews: true
}}
onBookingStarted={() => setIsBooking(true)}
/>Flight results use SignalR for live price updates (signalr-no-jquery, bundled — no setup required on your side).
TideForm
Renders a Tide-configured dynamic web form (e.g. a contact/quote request form tied to a product).
<TideForm
configuration={{
id: 1,
languageCode: 'en-GB',
apiKey, apiUrl,
contexts: [{ key: 'product', identifier: 'ETFSSTARGAZE' }],
initialValues: { travellers: { traveller_counts: { adult_count: 2, child_count: 0 } } }
}}
/>Login
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.
const [member, setMember] = useState<MemberInfo>();
<Login
tideClientConfig={{ host: apiUrl, apiKey, catalogueIds: [1] }}
portalId={0}
languageCode="en-GB"
isLoginPage={true}
isResetPassword={false}
isMemberConfirmation={false}
member={member}
setMember={setMember}
handleBackToHome={() => navigate('/')}
handleBackToLogin={() => navigate('/login')}
/>Once setMember receives a logged-in MemberInfo, pass that same object into Navbar's member prop to reflect the logged-in state in your header.
Content & marketing
These are presentational — plain props in, JSX out, no Tide API calls, no context or store required — unless noted otherwise.
Header
Hero/banner section with a video, image, or slider background.
<Header
media={{ type: 'image', src: '/hero.jpg', alt: 'Sunset over the coast' }}
title="Explore the world with us"
description="Discover breathtaking destinations, curated just for you."
showButton
buttonText="See offers"
onButtonClick={() => navigate('/offers')}
/>Navbar
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.
<Navbar
logo={<Logo />}
topLinks={topLinks}
items={navItems}
language={language}
languages={languages}
onLanguageChange={setLanguage}
onSearch={(query) => navigate(`/search?q=${query}`)}
member={member}
onLogin={() => navigate('/login')}
onLogout={() => setMember(undefined)}
/>Footer
Site footer with columns, social links and payment icons. Every prop is optional — <Footer /> renders sensible defaults.
<Footer />ImageCardGrid
A grid of image cards with an optional call-to-action per card.
<ImageCardGrid
title="Popular destinations"
columns={4}
cards={[{ image: '/santorini.jpg', title: 'Santorini', buttonText: 'Book now', onButtonClick: (card) => navigate(`/product/${card.title}`) }]}
/>ImageWithTextSection
Alternating image + two-column text sections, e.g. for describing activities.
<ImageWithTextSection
title="Activities"
cards={[{
imageSrc: '/activity.jpg', imageAlt: 'Hiking trail',
title: 'Discover Santorini', section1Title: 'The activity', section1Text: '...',
section2Title: 'Details', section2Text: '...', buttonText: 'Book now'
}]}
/>Slider
A simple image carousel; also used internally by Header for media.type: 'slider'.
<Slider images={['/1.jpg', '/2.jpg', '/3.jpg']} mode="auto" intervalMs={4000} />PhotoGallery
A titled image gallery with a lightbox.
<PhotoGallery
title="Project gallery"
images={[{ src: '/lake.jpg', alt: 'Mountain lake', caption: 'Morning hike' }]}
/>Breadcrumbs
<Breadcrumbs items={[{ href: '/', label: 'Home' }, { href: '/products', label: 'Products' }, { label: 'Santorini', isCurrent: true }]} />FAQ
Renders a list of question/answer pairs as an accordion.
<FAQ title="Frequently asked questions" items={[{ question: 'How do I cancel?', answer: <p>Contact us at least 48h in advance.</p> }]} />FeaturedTrips
A row of highlighted trip cards.
<FeaturedTrips
title="Featured trips"
cards={[{ imageSrc: '/santorini.jpg', imageAlt: 'Santorini', title: 'Santorini getaway', location: 'Greece', onButtonClick: () => navigate('/santorini') }]}
/>Spinner
Loading indicator, localized via the SearchResultsConfigurationContext when rendered inside SearchResults (falls back to English otherwise).
{isLoading ? <Spinner /> : <SearchResults configuration={configuration} />}ItineraryMapView
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.
<ItineraryMapView
searchResults={searchResults}
onSearchChange={(query) => setSearchResults(searchDestinations(query))}
onAddDestination={(name) => addDestination(name)}
onContinue={() => navigate('/booking')}
/>Self-contained (non-React) bundles
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.
Three of these are published to Tide's CDN, each under its own per-version path (plus a latest alias):
| Bundle | Mounts into | CDN component | CDN path |
|---|---|---|---|
| form | #tide-form | form | https://cdn.tidesoftware.be/components/form/{version}/ (or /latest/) |
| booking-wizard | #tide-booking | booking | https://cdn.tidesoftware.be/components/booking/{version}/ (or /latest/) |
| booking-product | #tide-product | product | https://cdn.tidesoftware.be/components/product/{version}/ (or /latest/) |
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.
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.
Framework-specific notes
Gatsby (or any SSR framework): when using BookingWizard on a Gatsby v5+ site:
- Use React 18 and
react-router-dom@6. - Import only from
@qite/tide-componentsat the top level and keep the actual render client-side (e.g. behindtypeof window !== 'undefined', or in a client-only wrapper) —signalr-no-jquerytoucheswindowand will error if evaluated during SSR/build. - Surround
<BookingWizard>with<BrowserRouter basename={basePath}>to handle its internal routing. - Set
skipBasePathInRouting: trueinsettingsto avoid duplicate base paths in the wizard's internal navigation.
Working on this library itself, rather than consuming it? See DEVELOPMENT.md for local setup (not included in the published package).
