@ticketboothapp/booking 1.2.24 → 1.2.25-rc.0

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 (158) hide show
  1. package/package.json +29 -2
  2. package/src/assets/icons/minus.svg +7 -0
  3. package/src/assets/icons/partner-logos/getyourguide.svg +8 -0
  4. package/src/assets/icons/plus.svg +3 -0
  5. package/src/colours.css +23 -0
  6. package/src/components/BookingDetails.module.css +1591 -0
  7. package/src/components/BookingDetails.tsx +2264 -0
  8. package/src/components/BookingWidget.tsx +302 -0
  9. package/src/components/ManageBookingView.tsx +437 -0
  10. package/src/components/PhoneInputWithCountry.module.css +131 -0
  11. package/src/components/PhoneInputWithCountry.tsx +44 -0
  12. package/src/components/PickupLocationDialog.module.css +360 -0
  13. package/src/components/PickupLocationDialog.tsx +357 -0
  14. package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
  15. package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
  16. package/src/components/booking/AddOnsSection.module.css +10 -0
  17. package/src/components/booking/AddOnsSection.tsx +184 -0
  18. package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
  19. package/src/components/booking/BookingDialog.module.css +643 -0
  20. package/src/components/booking/BookingDialog.tsx +356 -0
  21. package/src/components/booking/BookingFlow.tsx +4385 -0
  22. package/src/components/booking/BookingFlowCollage.module.css +148 -0
  23. package/src/components/booking/BookingFlowCollage.tsx +184 -0
  24. package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
  25. package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
  26. package/src/components/booking/BookingFlowPreview.tsx +51 -0
  27. package/src/components/booking/BookingProductGrid.module.css +359 -0
  28. package/src/components/booking/BookingProductGrid.tsx +497 -0
  29. package/src/components/booking/Calendar.module.css +616 -0
  30. package/src/components/booking/Calendar.tsx +1123 -0
  31. package/src/components/booking/CancellationPolicySelector.module.css +124 -0
  32. package/src/components/booking/CancellationPolicySelector.tsx +142 -0
  33. package/src/components/booking/ChangeBookingDialog.tsx +562 -0
  34. package/src/components/booking/CheckoutForm.module.css +244 -0
  35. package/src/components/booking/CheckoutForm.tsx +364 -0
  36. package/src/components/booking/CheckoutModal.tsx +451 -0
  37. package/src/components/booking/CurrencySwitcher.tsx +81 -0
  38. package/src/components/booking/DapFlowCollage.tsx +88 -0
  39. package/src/components/booking/DapTourDescription.tsx +35 -0
  40. package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
  41. package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
  42. package/src/components/booking/ErrorBoundary.tsx +63 -0
  43. package/src/components/booking/InfoTooltip.tsx +108 -0
  44. package/src/components/booking/ItineraryBox.module.css +258 -0
  45. package/src/components/booking/ItineraryBox.tsx +550 -0
  46. package/src/components/booking/ItineraryBuilder.tsx +82 -0
  47. package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
  48. package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
  49. package/src/components/booking/MealDrinkAddOnSelector.tsx +338 -0
  50. package/src/components/booking/PickupLocationSelector.module.css +124 -0
  51. package/src/components/booking/PickupLocationSelector.tsx +1566 -0
  52. package/src/components/booking/PickupTimeSelector.module.css +134 -0
  53. package/src/components/booking/PickupTimeSelector.tsx +112 -0
  54. package/src/components/booking/PriceBreakdown.tsx +154 -0
  55. package/src/components/booking/PriceSummary.tsx +234 -0
  56. package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
  57. package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
  58. package/src/components/booking/PromoCodeInput.module.css +166 -0
  59. package/src/components/booking/PromoCodeInput.tsx +99 -0
  60. package/src/components/booking/ReturnTimeSelector.module.css +173 -0
  61. package/src/components/booking/ReturnTimeSelector.tsx +145 -0
  62. package/src/components/booking/TermsAcceptance.tsx +111 -0
  63. package/src/components/booking/TicketSelector.module.css +164 -0
  64. package/src/components/booking/TicketSelector.tsx +199 -0
  65. package/src/components/booking/TourDescription.module.css +304 -0
  66. package/src/components/booking/TourDescription.tsx +273 -0
  67. package/src/components/booking/booking-flow-ui.ts +38 -0
  68. package/src/components/booking/booking-flow.css +944 -0
  69. package/src/components/button.css +245 -0
  70. package/src/components/button.tsx +152 -0
  71. package/src/components/colorable-svg.tsx +29 -0
  72. package/src/components/image.css +29 -0
  73. package/src/components/image.tsx +113 -0
  74. package/src/components/partner/PartnerBookingPage.module.css +130 -0
  75. package/src/components/partner/PartnerBookingPage.tsx +390 -0
  76. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +45 -0
  77. package/src/components/product-tag.module.css +30 -0
  78. package/src/components/product-tag.tsx +34 -0
  79. package/src/components/product-theme-pages/image-modal.tsx +248 -0
  80. package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
  81. package/src/components/terms/TermsContent.tsx +178 -0
  82. package/src/components/value-pill.module.css +59 -0
  83. package/src/components/value-pill.tsx +46 -0
  84. package/src/constants/images.ts +556 -0
  85. package/src/constants/pill-values.ts +210 -0
  86. package/src/constants/products.ts +155 -0
  87. package/src/contexts/AvailabilitiesCacheContext.tsx +125 -0
  88. package/src/contexts/BookingAppContext.tsx +134 -0
  89. package/src/contexts/CompanyContext.tsx +70 -0
  90. package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
  91. package/src/data/dap-descriptions/session-elopements.en.json +60 -0
  92. package/src/data/dap-descriptions/session-proposals.en.json +60 -0
  93. package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
  94. package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
  95. package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
  96. package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
  97. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
  98. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
  99. package/src/data/product-descriptions/private-tour.en.json +80 -0
  100. package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
  101. package/src/data/products-config.json +101 -0
  102. package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
  103. package/src/hooks/useIsBookingLaunchLive.ts +49 -0
  104. package/src/index.ts +79 -0
  105. package/src/lib/analytics.ts +197 -0
  106. package/src/lib/booking/booking-source.ts +51 -0
  107. package/src/lib/booking/checkout-breakdown.ts +69 -0
  108. package/src/lib/booking/correlation-id.ts +46 -0
  109. package/src/lib/booking/i18n/config.ts +21 -0
  110. package/src/lib/booking/i18n/index.tsx +144 -0
  111. package/src/lib/booking/i18n/messages/en.json +236 -0
  112. package/src/lib/booking/i18n/messages/fr.json +236 -0
  113. package/src/lib/booking/itinerary-display.ts +36 -0
  114. package/src/lib/booking/itinerary-labels.ts +70 -0
  115. package/src/lib/booking/location-calculations.ts +43 -0
  116. package/src/lib/booking/location-utils.ts +165 -0
  117. package/src/lib/booking/map-utils.ts +153 -0
  118. package/src/lib/booking/marker-icons.ts +113 -0
  119. package/src/lib/booking/normalize-booking-product-id.ts +21 -0
  120. package/src/lib/booking/pickup-location-types.ts +25 -0
  121. package/src/lib/booking/places-api.ts +154 -0
  122. package/src/lib/booking/pricing.ts +466 -0
  123. package/src/lib/booking/product-option-id.ts +35 -0
  124. package/src/lib/booking/source-metadata.ts +226 -0
  125. package/src/lib/booking/sunday-week.ts +14 -0
  126. package/src/lib/booking/theme.ts +83 -0
  127. package/src/lib/booking/trace-context.ts +62 -0
  128. package/src/lib/booking/utils.ts +9 -0
  129. package/src/lib/booking-api.ts +1793 -0
  130. package/src/lib/booking-constants.ts +23 -0
  131. package/src/lib/booking-ref.ts +13 -0
  132. package/src/lib/booking-types.ts +36 -0
  133. package/src/lib/currency.ts +81 -0
  134. package/src/lib/dap-descriptions.ts +50 -0
  135. package/src/lib/dap-itinerary-preview.ts +315 -0
  136. package/src/lib/dependent-add-on-api.ts +434 -0
  137. package/src/lib/env.ts +96 -0
  138. package/src/lib/firebase.ts +20 -0
  139. package/src/lib/job-application-api.ts +83 -0
  140. package/src/lib/manage-booking-embed-print.ts +16 -0
  141. package/src/lib/manage-booking-post-checkout.ts +68 -0
  142. package/src/lib/photo-dap-config.ts +228 -0
  143. package/src/lib/photo-packages.ts +75 -0
  144. package/src/lib/pickup/map-utils.ts +56 -0
  145. package/src/lib/pickup/marker-icons.ts +19 -0
  146. package/src/lib/product-descriptions.ts +66 -0
  147. package/src/lib/products-config.ts +73 -0
  148. package/src/providers/booking-dialog-provider.tsx +282 -0
  149. package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
  150. package/src/radius.css +5 -0
  151. package/src/spacing.css +7 -0
  152. package/src/strings/en.json +1774 -0
  153. package/src/strings/es.json +1573 -0
  154. package/src/strings/fr.json +1573 -0
  155. package/src/strings/index.js +23 -0
  156. package/src/text-style.css +56 -0
  157. package/src/utils/currency-converter.ts +101 -0
  158. package/tsconfig.json +8 -2
@@ -0,0 +1,356 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { useBookingSourceMetadataFromLocation } from '@/hooks/useBookingSourceMetadataFromLocation';
5
+ import { useBookingDialog } from '@/providers/booking-dialog-provider';
6
+ import { getProductByIdOrSlug, buildMinimalProductFromConfig } from '@/lib/products-config';
7
+ import { getProduct, type Product } from '@/lib/booking-api';
8
+ import { ENV } from '@/lib/env';
9
+ import defaultStrings from '@/strings';
10
+ import './booking-flow.css';
11
+ import BookingProductGrid from './BookingProductGrid';
12
+ import { BookingFlow } from './BookingFlow';
13
+ import { PrivateShuttleBookingFlow } from './PrivateShuttleBookingFlow';
14
+ import { BookingFlowPreview } from './BookingFlowPreview';
15
+ import { useIsBookingLaunchLive } from '@/hooks/useIsBookingLaunchLive';
16
+ import styles from './BookingDialog.module.css';
17
+
18
+ function BookFlowScreen({
19
+ productId,
20
+ onBack,
21
+ onComplete,
22
+ onProductLoaded,
23
+ contentRef,
24
+ isPartialLaunch,
25
+ }: {
26
+ productId: string;
27
+ onBack: () => void;
28
+ onComplete: () => void;
29
+ onProductLoaded?: (productName: string) => void;
30
+ contentRef?: React.RefObject<HTMLDivElement | null>;
31
+ isPartialLaunch: boolean;
32
+ }) {
33
+ const bookingSourceAttribution = useBookingSourceMetadataFromLocation();
34
+ const [product, setProduct] = useState<Product | null>(null);
35
+ const [error, setError] = useState<string | null>(null);
36
+
37
+ const config = getProductByIdOrSlug(productId);
38
+ const apiProductId = config?.productId ?? productId;
39
+
40
+ // Build minimal product from config for immediate availability fetch (no /products wait)
41
+ const minimalProduct = config
42
+ ? buildMinimalProductFromConfig(config, ENV.COMPANY_ID)
43
+ : null;
44
+
45
+ useEffect(() => {
46
+ if (isPartialLaunch) return; // No API needed for partial launch
47
+ let cancelled = false;
48
+ setError(null);
49
+ getProduct(apiProductId, ENV.COMPANY_ID)
50
+ .then((p) => {
51
+ if (!cancelled && p) setProduct(p);
52
+ else if (!cancelled && !p && !config) setError('Product not found');
53
+ })
54
+ .catch((err) => {
55
+ if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to load product');
56
+ });
57
+ return () => { cancelled = true; };
58
+ }, [apiProductId, config, isPartialLaunch]);
59
+
60
+ useEffect(() => {
61
+ if (product) {
62
+ onProductLoaded?.(product.name);
63
+ }
64
+ }, [product?.name, onProductLoaded]);
65
+
66
+ // Partial launch: show collage + description immediately (no API)
67
+ if (isPartialLaunch) {
68
+ return (
69
+ <div className={`${styles.screen} booking-flow-preflight`}>
70
+ <BookingFlowPreview productId={productId} defaultExpanded />
71
+ </div>
72
+ );
73
+ }
74
+
75
+ // No config: must wait for API (product not in products-config)
76
+ if (!config) {
77
+ if (error) {
78
+ return (
79
+ <div className={`${styles.screen} booking-flow-preflight`}>
80
+ <div className="flex flex-col items-center justify-center py-16 gap-4">
81
+ <p className="text-red-600">{error}</p>
82
+ <button type="button" onClick={onBack} className="text-emerald-600 hover:underline">
83
+ Go back
84
+ </button>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
89
+ return (
90
+ <div className={`${styles.screen} booking-flow-preflight`}>
91
+ <div className="flex items-center justify-center py-16">
92
+ <div className="text-stone-600">{defaultStrings.common.chooseYourExperience} — Loading...</div>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ // Use full product when loaded, otherwise minimal from config (availabilities fetch starts immediately)
99
+ const displayProduct = product ?? minimalProduct!;
100
+
101
+ if (error && !product) {
102
+ return (
103
+ <div className={`${styles.screen} booking-flow-preflight`}>
104
+ <div className="flex flex-col items-center justify-center py-16 gap-4">
105
+ <p className="text-red-600">{error}</p>
106
+ <button type="button" onClick={onBack} className="text-emerald-600 hover:underline">
107
+ Go back
108
+ </button>
109
+ </div>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ return (
115
+ <div className={`${styles.screen} booking-flow-preflight`}>
116
+ {displayProduct.productType === 'PRIVATE_SHUTTLE' ? (
117
+ <PrivateShuttleBookingFlow
118
+ product={displayProduct}
119
+ productId={productId}
120
+ onBack={onBack}
121
+ currency="CAD"
122
+ contentRef={contentRef}
123
+ bookingSourceAttribution={bookingSourceAttribution}
124
+ onSuccess={() => {
125
+ onComplete();
126
+ }}
127
+ />
128
+ ) : (
129
+ <BookingFlow
130
+ product={displayProduct}
131
+ productId={productId}
132
+ onBack={onBack}
133
+ currency="CAD"
134
+ contentRef={contentRef}
135
+ bookingSourceAttribution={bookingSourceAttribution}
136
+ onSuccess={() => {
137
+ onComplete();
138
+ }}
139
+ isPartialLaunch={isPartialLaunch}
140
+ />
141
+ )}
142
+ </div>
143
+ );
144
+ }
145
+
146
+ function getFocusableElements(container: HTMLElement): HTMLElement[] {
147
+ const selector =
148
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
149
+ return Array.from(container.querySelectorAll<HTMLElement>(selector)).filter(
150
+ (el) =>
151
+ !el.hasAttribute('disabled') &&
152
+ el.offsetParent !== null &&
153
+ getComputedStyle(el).visibility !== 'hidden'
154
+ );
155
+ }
156
+
157
+ export default function BookingDialog() {
158
+ const {
159
+ isOpen,
160
+ close,
161
+ stack,
162
+ pop,
163
+ canGoBack,
164
+ productGridRestoreState,
165
+ clearProductGridRestoreState,
166
+ } = useBookingDialog();
167
+ const isLaunchLive = useIsBookingLaunchLive();
168
+ const isPartialLaunch = !isLaunchLive;
169
+ const currentScreen = stack[stack.length - 1];
170
+ const [productName, setProductName] = useState<string | null>(null);
171
+ const dialogRef = useRef<HTMLDivElement>(null);
172
+
173
+ // Use products-config for instant header title (no API wait)
174
+ const displayNameFromConfig =
175
+ currentScreen?.type === 'book-flow'
176
+ ? getProductByIdOrSlug(currentScreen.productId)?.display?.shortName ?? null
177
+ : null;
178
+ const contentRef = useRef<HTMLDivElement>(null);
179
+
180
+ useEffect(() => {
181
+ if (currentScreen.type === 'product-grid') {
182
+ setProductName(null);
183
+ }
184
+ }, [currentScreen.type]);
185
+
186
+ // Reset scroll when switching to book-flow so we don't appear partially scrolled
187
+ // (product-grid restores its own scroll when returning from a product)
188
+ useEffect(() => {
189
+ if (currentScreen.type === 'book-flow' && contentRef.current) {
190
+ contentRef.current.scrollTop = 0;
191
+ }
192
+ }, [currentScreen.type, currentScreen.type === 'book-flow' ? currentScreen.productId : null]);
193
+
194
+ const overlayRef = useRef<HTMLDivElement>(null);
195
+
196
+ useEffect(() => {
197
+ if (isOpen) {
198
+ document.body.style.overflow = 'hidden';
199
+ document.body.setAttribute('data-booking-dialog-open', '');
200
+ // Pause all background videos (exclude videos inside the dialog)
201
+ const overlay = overlayRef.current;
202
+ const videos = document.querySelectorAll('video');
203
+ const pausedVideos: HTMLVideoElement[] = [];
204
+ videos.forEach((video) => {
205
+ if (!overlay?.contains(video) && !video.paused) {
206
+ video.pause();
207
+ pausedVideos.push(video);
208
+ }
209
+ });
210
+ return () => {
211
+ document.body.style.overflow = 'unset';
212
+ document.body.removeAttribute('data-booking-dialog-open');
213
+ pausedVideos.forEach((v) => v.play().catch(() => {}));
214
+ };
215
+ }
216
+ document.body.style.overflow = 'unset';
217
+ document.body.removeAttribute('data-booking-dialog-open');
218
+ }, [isOpen]);
219
+
220
+ useEffect(() => {
221
+ if (!isOpen) return;
222
+ const handleKeyDown = (e: KeyboardEvent) => {
223
+ if (e.key === 'Escape') {
224
+ close();
225
+ return;
226
+ }
227
+ if (e.key !== 'Tab' || !dialogRef.current) return;
228
+ const focusable = getFocusableElements(dialogRef.current);
229
+ if (focusable.length === 0) return;
230
+ const first = focusable[0];
231
+ const last = focusable[focusable.length - 1];
232
+ const currentIndex = focusable.indexOf(
233
+ document.activeElement as HTMLElement
234
+ );
235
+ if (e.shiftKey) {
236
+ if (currentIndex <= 0 || currentIndex === -1) {
237
+ e.preventDefault();
238
+ last.focus();
239
+ }
240
+ } else {
241
+ if (currentIndex === focusable.length - 1 || currentIndex === -1) {
242
+ e.preventDefault();
243
+ first.focus();
244
+ }
245
+ }
246
+ };
247
+ window.addEventListener('keydown', handleKeyDown);
248
+ return () => window.removeEventListener('keydown', handleKeyDown);
249
+ }, [isOpen, close]);
250
+
251
+ useEffect(() => {
252
+ if (isOpen && dialogRef.current) {
253
+ dialogRef.current.focus();
254
+ }
255
+ }, [isOpen]);
256
+
257
+ if (!isOpen) return null;
258
+
259
+ return (
260
+ <div ref={overlayRef} className={`${styles.overlay} booking-dialog-overlay`} onClick={() => close()}>
261
+ <div
262
+ ref={dialogRef}
263
+ className={`booking-flow-root ${styles.dialog}`}
264
+ onClick={(e) => e.stopPropagation()}
265
+ role="dialog"
266
+ aria-modal="true"
267
+ aria-labelledby="booking-dialog-title"
268
+ tabIndex={-1}
269
+ >
270
+ <header className={styles.header}>
271
+ <div className={styles.headerLeft}>
272
+ {canGoBack ? (
273
+ <button
274
+ type="button"
275
+ className={styles.backButton}
276
+ onClick={pop}
277
+ aria-label="Go back"
278
+ >
279
+ <svg
280
+ width="24"
281
+ height="24"
282
+ viewBox="0 0 24 24"
283
+ fill="none"
284
+ stroke="currentColor"
285
+ strokeWidth="2"
286
+ strokeLinecap="round"
287
+ strokeLinejoin="round"
288
+ >
289
+ <path d="M19 12H5M12 19l-7-7 7-7" />
290
+ </svg>
291
+ </button>
292
+ ) : (
293
+ <span className={styles.headerSpacer} aria-hidden />
294
+ )}
295
+ </div>
296
+ <h2
297
+ id="booking-dialog-title"
298
+ className={
299
+ currentScreen.type === 'product-grid'
300
+ ? styles.titleProductGrid
301
+ : styles.title
302
+ }
303
+ >
304
+ {currentScreen.type === 'product-grid' && defaultStrings.common.chooseYourExperience}
305
+ {currentScreen.type === 'book-flow' && (displayNameFromConfig ?? productName ?? defaultStrings.common.book)}
306
+ </h2>
307
+ <div className={styles.headerRight}>
308
+ <button
309
+ type="button"
310
+ className={styles.closeButton}
311
+ onClick={() => close()}
312
+ aria-label="Close"
313
+ >
314
+ <svg
315
+ width="24"
316
+ height="24"
317
+ viewBox="0 0 24 24"
318
+ fill="none"
319
+ stroke="currentColor"
320
+ strokeWidth="2"
321
+ strokeLinecap="round"
322
+ strokeLinejoin="round"
323
+ >
324
+ <path d="M18 6L6 18M6 6l12 12" />
325
+ </svg>
326
+ </button>
327
+ </div>
328
+ </header>
329
+
330
+ <div ref={contentRef} className={styles.content}>
331
+ {currentScreen.type === 'product-grid' && (
332
+ <div className={styles.screen}>
333
+ <BookingProductGrid
334
+ contentRef={contentRef}
335
+ restoreState={productGridRestoreState}
336
+ onRestoreApplied={clearProductGridRestoreState}
337
+ initialFilterId={currentScreen.filterId}
338
+ isPartialLaunch={isPartialLaunch}
339
+ />
340
+ </div>
341
+ )}
342
+ {currentScreen.type === 'book-flow' && (
343
+ <BookFlowScreen
344
+ productId={currentScreen.productId}
345
+ onBack={pop}
346
+ onComplete={() => close({ reason: 'completed' })}
347
+ onProductLoaded={setProductName}
348
+ contentRef={contentRef}
349
+ isPartialLaunch={isPartialLaunch}
350
+ />
351
+ )}
352
+ </div>
353
+ </div>
354
+ </div>
355
+ );
356
+ }