@ticketboothapp/booking 0.1.11 → 0.1.12

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 (252) hide show
  1. package/package.json +1 -1
  2. package/src/colours.css +23 -0
  3. package/src/components/BookingDetails.module.css +1591 -0
  4. package/src/components/BookingDetails.tsx +2072 -354
  5. package/src/components/BookingWidget.tsx +28 -248
  6. package/src/components/JobApplicationDialog.module.css +440 -0
  7. package/src/components/JobApplicationDialog.tsx +620 -0
  8. package/src/components/ManageBookingView.tsx +28 -36
  9. package/src/components/PhoneInputWithCountry.module.css +131 -0
  10. package/src/components/PhoneInputWithCountry.tsx +44 -0
  11. package/src/components/PickupLocationDialog.module.css +360 -0
  12. package/src/components/PickupLocationDialog.tsx +357 -0
  13. package/src/components/PickupLocationMap.tsx +110 -0
  14. package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
  15. package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
  16. package/src/components/accordion.css +27 -0
  17. package/src/components/accordion.tsx +29 -0
  18. package/src/components/analytics/AnalyticsConsentRestore.tsx +19 -0
  19. package/src/components/analytics/AnalyticsScripts.tsx +106 -0
  20. package/src/components/analytics/CookieConsentBanner.css +86 -0
  21. package/src/components/analytics/CookieConsentBanner.tsx +102 -0
  22. package/src/components/booking/AddOnsSection.module.css +10 -0
  23. package/src/components/booking/AddOnsSection.tsx +184 -0
  24. package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
  25. package/src/components/booking/BookingDialog.module.css +643 -0
  26. package/src/components/booking/BookingDialog.tsx +356 -0
  27. package/src/components/booking/BookingFlow.tsx +4385 -0
  28. package/src/components/booking/BookingFlowCollage.module.css +148 -0
  29. package/src/components/booking/BookingFlowCollage.tsx +184 -0
  30. package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
  31. package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
  32. package/src/components/booking/BookingFlowPreview.tsx +51 -0
  33. package/src/components/booking/BookingProductGrid.module.css +359 -0
  34. package/src/components/booking/BookingProductGrid.tsx +497 -0
  35. package/src/components/booking/Calendar.module.css +616 -0
  36. package/src/components/{Calendar.tsx → booking/Calendar.tsx} +464 -247
  37. package/src/components/booking/CancellationPolicySelector.module.css +124 -0
  38. package/src/components/booking/CancellationPolicySelector.tsx +142 -0
  39. package/src/components/booking/ChangeBookingDialog.tsx +562 -0
  40. package/src/components/booking/CheckoutForm.module.css +244 -0
  41. package/src/components/booking/CheckoutForm.tsx +364 -0
  42. package/src/components/{CheckoutModal.tsx → booking/CheckoutModal.tsx} +176 -19
  43. package/src/components/booking/DapFlowCollage.tsx +88 -0
  44. package/src/components/booking/DapTourDescription.tsx +35 -0
  45. package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
  46. package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
  47. package/src/components/booking/InfoTooltip.tsx +108 -0
  48. package/src/components/booking/ItineraryBox.module.css +258 -0
  49. package/src/components/booking/ItineraryBox.tsx +550 -0
  50. package/src/components/{ItineraryBuilder.tsx → booking/ItineraryBuilder.tsx} +1 -2
  51. package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
  52. package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
  53. package/src/components/{MealDrinkAddOnSelector.tsx → booking/MealDrinkAddOnSelector.tsx} +21 -13
  54. package/src/components/booking/PickupLocationSelector.module.css +124 -0
  55. package/src/components/{PickupLocationSelector.tsx → booking/PickupLocationSelector.tsx} +315 -290
  56. package/src/components/booking/PickupTimeSelector.module.css +134 -0
  57. package/src/components/booking/PickupTimeSelector.tsx +112 -0
  58. package/src/components/{PriceBreakdown.tsx → booking/PriceBreakdown.tsx} +3 -3
  59. package/src/components/{PriceSummary.tsx → booking/PriceSummary.tsx} +51 -28
  60. package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
  61. package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
  62. package/src/components/booking/PromoCodeInput.module.css +166 -0
  63. package/src/components/booking/PromoCodeInput.tsx +99 -0
  64. package/src/components/booking/ReturnTimeSelector.module.css +173 -0
  65. package/src/components/booking/ReturnTimeSelector.tsx +145 -0
  66. package/src/components/{TermsAcceptance.tsx → booking/TermsAcceptance.tsx} +9 -8
  67. package/src/components/booking/TicketSelector.module.css +164 -0
  68. package/src/components/booking/TicketSelector.tsx +199 -0
  69. package/src/components/booking/TourDescription.module.css +304 -0
  70. package/src/components/booking/TourDescription.tsx +273 -0
  71. package/src/components/booking/booking-flow-ui.ts +15 -1
  72. package/src/components/booking/booking-flow.css +944 -0
  73. package/src/components/bottom-sheet.module.css +78 -0
  74. package/src/components/bottom-sheet.tsx +60 -0
  75. package/src/components/breadcrumb.module.css +40 -0
  76. package/src/components/breadcrumb.tsx +36 -0
  77. package/src/components/button.css +245 -0
  78. package/src/components/button.tsx +152 -0
  79. package/src/components/client-bottom-sheet.tsx +14 -0
  80. package/src/components/colorable-svg.tsx +29 -0
  81. package/src/components/conditional-footer.tsx +27 -0
  82. package/src/components/contact-us.module.css +147 -0
  83. package/src/components/contact-us.tsx +49 -0
  84. package/src/components/email-signup.css +151 -0
  85. package/src/components/email-signup.tsx +63 -0
  86. package/src/components/faq-wrapper.module.css +47 -0
  87. package/src/components/faq-wrapper.tsx +15 -0
  88. package/src/components/footer.css +187 -0
  89. package/src/components/footer.tsx +143 -0
  90. package/src/components/global-simple-modal.tsx +33 -0
  91. package/src/components/google-review-summary.module.css +77 -0
  92. package/src/components/google-review-summary.tsx +50 -0
  93. package/src/components/hero-image.css +13 -0
  94. package/src/components/hero-image.tsx +44 -0
  95. package/src/components/image.css +29 -0
  96. package/src/components/image.tsx +113 -0
  97. package/src/components/language-aware-link.tsx +72 -0
  98. package/src/components/language-switcher.module.css +124 -0
  99. package/src/components/language-switcher.tsx +75 -0
  100. package/src/components/map-section.css +59 -0
  101. package/src/components/map-section.tsx +63 -0
  102. package/src/components/navbar.module.css +152 -0
  103. package/src/components/navbar.tsx +125 -0
  104. package/src/components/parallax-provider.tsx +11 -0
  105. package/src/components/partner/PartnerBookingPage.module.css +130 -0
  106. package/src/components/partner/PartnerBookingPage.tsx +390 -0
  107. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +19 -35
  108. package/src/components/product-tag.module.css +30 -0
  109. package/src/components/product-tag.tsx +34 -0
  110. package/src/components/product-theme-pages/best-option.module.css +70 -0
  111. package/src/components/product-theme-pages/best-option.tsx +35 -0
  112. package/src/components/product-theme-pages/extended-tour-options.module.css +22 -0
  113. package/src/components/product-theme-pages/extended-tour-options.tsx +11 -0
  114. package/src/components/product-theme-pages/image-modal.tsx +248 -0
  115. package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
  116. package/src/components/product-theme-pages/photo-gallery.tsx +90 -0
  117. package/src/components/product-theme-pages/product-theme-page-layout.module.css +13 -0
  118. package/src/components/product-theme-pages/product-theme-page-layout.tsx +67 -0
  119. package/src/components/product-theme-pages/top-of-fold.module.css +179 -0
  120. package/src/components/product-theme-pages/top-of-fold.tsx +80 -0
  121. package/src/components/product-tile/image-only-product-tile-desktop.module.css +106 -0
  122. package/src/components/product-tile/image-only-product-tile-desktop.tsx +56 -0
  123. package/src/components/product-tile/image-only-product-tile-mobile.module.css +122 -0
  124. package/src/components/product-tile/image-only-product-tile-mobile.tsx +89 -0
  125. package/src/components/product-tile/image-only-product-tile.tsx +44 -0
  126. package/src/components/product-tile/product-tile-card.module.css +84 -0
  127. package/src/components/product-tile/product-tile-card.tsx +61 -0
  128. package/src/components/review-highlights-section.css +85 -0
  129. package/src/components/review-highlights-section.tsx +127 -0
  130. package/src/components/season-closure-overlay.module.css +99 -0
  131. package/src/components/season-closure-overlay.tsx +98 -0
  132. package/src/components/simple-modal.tsx +69 -0
  133. package/src/components/simple-top-of-fold.module.css +76 -0
  134. package/src/components/simple-top-of-fold.tsx +34 -0
  135. package/src/components/spacer.css +41 -0
  136. package/src/components/spacer.tsx +23 -0
  137. package/src/components/star-rating.module.css +74 -0
  138. package/src/components/star-rating.tsx +48 -0
  139. package/src/components/terms/TermsContent.tsx +178 -0
  140. package/src/components/title-subtitle.module.css +10 -0
  141. package/src/components/title-subtitle.tsx +30 -0
  142. package/src/components/translatable-reviews.tsx +75 -0
  143. package/src/components/value-pill.module.css +59 -0
  144. package/src/components/value-pill.tsx +46 -0
  145. package/src/components/value-props.css +185 -0
  146. package/src/components/value-props.tsx +88 -0
  147. package/src/constants/booking-guide-quiz.ts +64 -0
  148. package/src/constants/contact-info.ts +2 -0
  149. package/src/constants/faq.ts +44 -0
  150. package/src/constants/images.ts +556 -0
  151. package/src/constants/json-ld/faq-json-ld.tsx +170 -0
  152. package/src/constants/json-ld/homepage-json-ld.tsx +138 -0
  153. package/src/constants/json-ld/job-posting-json-ld.tsx +92 -0
  154. package/src/constants/json-ld/organization-json-ld.tsx +62 -0
  155. package/src/constants/json-ld/page-json-ld.tsx +6 -0
  156. package/src/constants/json-ld/product-json-ld.tsx +154 -0
  157. package/src/constants/json-ld/review-json-ld.tsx +377 -0
  158. package/src/constants/navigation-links/footer-links.ts +48 -0
  159. package/src/constants/navigation-links/nav-bar-links.ts +41 -0
  160. package/src/constants/navigation-links/navigation-link.ts +6 -0
  161. package/src/constants/pill-values.ts +210 -0
  162. package/src/constants/products.ts +155 -0
  163. package/src/constants/quiz-recommendations.ts +506 -0
  164. package/src/constants/reviews.ts +75 -0
  165. package/src/constants/staff.ts +197 -0
  166. package/src/constants/value-props.ts +58 -0
  167. package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
  168. package/src/data/dap-descriptions/session-elopements.en.json +60 -0
  169. package/src/data/dap-descriptions/session-proposals.en.json +60 -0
  170. package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
  171. package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
  172. package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
  173. package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
  174. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
  175. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
  176. package/src/data/product-descriptions/private-tour.en.json +80 -0
  177. package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
  178. package/src/data/products-config.json +101 -0
  179. package/src/hooks/use-bottom-sheet.tsx +15 -0
  180. package/src/hooks/use-simple-modal.tsx +27 -0
  181. package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
  182. package/src/hooks/useEmailSubscription.tsx +103 -0
  183. package/src/hooks/useEmbeddedInIframe.ts +16 -0
  184. package/src/hooks/useIsBookingLaunchLive.ts +49 -0
  185. package/src/hooks/useQuiz.tsx +210 -0
  186. package/src/index.ts +27 -2
  187. package/src/lib/analytics.ts +197 -0
  188. package/src/lib/booking/booking-source.ts +20 -2
  189. package/src/lib/{checkout-breakdown.ts → booking/checkout-breakdown.ts} +1 -1
  190. package/src/lib/booking/correlation-id.ts +46 -0
  191. package/src/lib/{i18n → booking/i18n}/messages/en.json +48 -4
  192. package/src/lib/{i18n → booking/i18n}/messages/fr.json +48 -4
  193. package/src/lib/booking/itinerary-display.ts +36 -0
  194. package/src/lib/{itinerary-labels.ts → booking/itinerary-labels.ts} +1 -1
  195. package/src/lib/{location-calculations.ts → booking/location-calculations.ts} +4 -4
  196. package/src/lib/{location-utils.ts → booking/location-utils.ts} +26 -0
  197. package/src/lib/{map-utils.ts → booking/map-utils.ts} +3 -3
  198. package/src/lib/booking/normalize-booking-product-id.ts +7 -0
  199. package/src/lib/{pickup-location-types.ts → booking/pickup-location-types.ts} +2 -2
  200. package/src/lib/{pricing.ts → booking/pricing.ts} +2 -2
  201. package/src/lib/booking/product-option-id.ts +35 -0
  202. package/src/lib/booking/source-metadata.ts +72 -7
  203. package/src/lib/booking/sunday-week.ts +14 -0
  204. package/src/lib/booking/trace-context.ts +62 -0
  205. package/src/lib/booking-api.ts +1793 -0
  206. package/src/lib/{constants.ts → booking-constants.ts} +11 -5
  207. package/src/lib/booking-types.ts +36 -0
  208. package/src/lib/currency.ts +38 -45
  209. package/src/lib/dap-descriptions.ts +50 -0
  210. package/src/lib/dap-itinerary-preview.ts +315 -0
  211. package/src/lib/dependent-add-on-api.ts +434 -0
  212. package/src/lib/env.ts +89 -5
  213. package/src/lib/firebase.ts +20 -0
  214. package/src/lib/job-application-api.ts +83 -0
  215. package/src/lib/manage-booking-embed-print.ts +16 -0
  216. package/src/lib/manage-booking-post-checkout.ts +68 -0
  217. package/src/lib/photo-dap-config.ts +228 -0
  218. package/src/lib/pickup/map-utils.ts +56 -0
  219. package/src/lib/pickup/marker-icons.ts +19 -0
  220. package/src/lib/product-descriptions.ts +66 -0
  221. package/src/lib/products-config.ts +73 -0
  222. package/src/providers/booking-dialog-provider.tsx +107 -38
  223. package/src/providers/bottom-sheet-provider.tsx +40 -0
  224. package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
  225. package/src/radius.css +5 -0
  226. package/src/spacing.css +7 -0
  227. package/src/strings/en.json +1774 -0
  228. package/src/strings/es.json +1573 -0
  229. package/src/strings/fr.json +1573 -0
  230. package/src/strings/index.js +23 -0
  231. package/src/text-style.css +97 -0
  232. package/src/types/fareharbor.d.ts +12 -0
  233. package/src/types/quiz.ts +59 -0
  234. package/src/utils/currency-converter.ts +101 -0
  235. package/src/components/BookingFlow.tsx +0 -2952
  236. package/src/components/LanguageSwitcher.tsx +0 -30
  237. package/src/components/PrivateShuttleBookingFlow.tsx +0 -2290
  238. package/src/components/ProductList.tsx +0 -78
  239. package/src/components/WhatsAppPhoneInput.tsx +0 -224
  240. package/src/components/index.ts +0 -31
  241. package/src/lib/api.ts +0 -801
  242. package/src/lib/booking-api-auth.ts +0 -9
  243. package/src/lib/checkout-breakdown.test.ts +0 -70
  244. package/src/types/google-maps.d.ts +0 -2
  245. /package/src/components/{CurrencySwitcher.tsx → booking/CurrencySwitcher.tsx} +0 -0
  246. /package/src/components/{ErrorBoundary.tsx → booking/ErrorBoundary.tsx} +0 -0
  247. /package/src/lib/{i18n → booking/i18n}/config.ts +0 -0
  248. /package/src/lib/{i18n → booking/i18n}/index.tsx +0 -0
  249. /package/src/lib/{marker-icons.ts → booking/marker-icons.ts} +0 -0
  250. /package/src/lib/{places-api.ts → booking/places-api.ts} +0 -0
  251. /package/src/lib/{theme.ts → booking/theme.ts} +0 -0
  252. /package/src/lib/{utils.ts → booking/utils.ts} +0 -0
@@ -10,14 +10,15 @@ import {
10
10
  useRef,
11
11
  type ReactNode,
12
12
  } from 'react';
13
- import { getOrCreateBookingCorrelationId } from '../correlation-id';
14
- import { withBookingOutboundHeaders } from '../trace-context';
13
+ import { getOrCreateBookingCorrelationId } from '@/lib/booking/correlation-id';
14
+ import { withBookingOutboundHeaders } from '@/lib/booking/trace-context';
15
15
  import {
16
16
  isSuspiciousBookingProductId,
17
17
  normalizeBookingProductId,
18
- } from '../lib/booking/normalize-booking-product-id';
19
- import { ENV } from '../lib/env';
18
+ } from '@/lib/booking/normalize-booking-product-id';
19
+ import { ENV } from '@/lib/env';
20
20
 
21
+ /** Filter IDs for the product grid. Must match BookingProductGrid FILTER_IDS. */
21
22
  export type ProductGridFilterId =
22
23
  | 'all'
23
24
  | 'sunrise'
@@ -60,7 +61,9 @@ function reportSuspiciousBookingProductId(original: string, sanitized: string):
60
61
  headers: withBookingOutboundHeaders({ 'Content-Type': 'application/json' }),
61
62
  body: JSON.stringify(event),
62
63
  keepalive: true,
63
- }).catch(() => {});
64
+ }).catch(() => {
65
+ // Never throw from telemetry reporting.
66
+ });
64
67
  }
65
68
 
66
69
  interface BookingDialogContextValue {
@@ -69,29 +72,45 @@ interface BookingDialogContextValue {
69
72
  openForProduct: (productId: string) => void;
70
73
  close: (options?: { reason?: 'user' | 'completed' }) => void;
71
74
  stack: BookingScreen[];
72
- push: (screen: BookingScreen, options?: { productGridState?: ProductGridRestoreState }) => void;
75
+ push: (
76
+ screen: BookingScreen,
77
+ options?: { productGridState?: ProductGridRestoreState }
78
+ ) => void;
73
79
  pop: () => void;
74
80
  canGoBack: boolean;
75
81
  productGridRestoreState: ProductGridRestoreState | null;
76
82
  clearProductGridRestoreState: () => void;
77
83
  }
78
84
 
79
- const BookingDialogContext = createContext<BookingDialogContextValue | null>(null);
85
+ const BookingDialogContext = createContext<BookingDialogContextValue | null>(
86
+ null
87
+ );
80
88
 
81
89
  export function useBookingDialog() {
82
90
  const ctx = useContext(BookingDialogContext);
83
- if (!ctx) throw new Error('useBookingDialog must be used within BookingDialogProvider');
91
+ if (!ctx) {
92
+ throw new Error('useBookingDialog must be used within BookingDialogProvider');
93
+ }
84
94
  return ctx;
85
95
  }
86
96
 
87
- export function BookingDialogProvider({ children }: { children: ReactNode }) {
97
+ interface BookingDialogProviderProps {
98
+ children: ReactNode;
99
+ }
100
+
101
+ export function BookingDialogProvider({ children }: BookingDialogProviderProps) {
88
102
  const [isOpen, setIsOpen] = useState(false);
89
103
  const [stack, setStack] = useState<BookingScreen[]>([{ type: 'product-grid' }]);
90
- const [productGridRestoreState, setProductGridRestoreState] = useState<ProductGridRestoreState | null>(null);
104
+ const [productGridRestoreState, setProductGridRestoreState] = useState<
105
+ ProductGridRestoreState | null
106
+ >(null);
91
107
  const previouslyFocusedRef = useRef<HTMLElement | null>(null);
92
108
 
93
109
  const open = useCallback((options?: { filterId?: ProductGridFilterId }) => {
94
- previouslyFocusedRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
110
+ previouslyFocusedRef.current =
111
+ document.activeElement instanceof HTMLElement
112
+ ? document.activeElement
113
+ : null;
95
114
  setStack([{ type: 'product-grid', filterId: options?.filterId }]);
96
115
  setProductGridRestoreState(null);
97
116
  setIsOpen(true);
@@ -101,9 +120,16 @@ export function BookingDialogProvider({ children }: { children: ReactNode }) {
101
120
  const sanitizedProductId = normalizeBookingProductId(productId);
102
121
  if (!sanitizedProductId) return;
103
122
  if (isSuspiciousBookingProductId(productId)) {
123
+ console.warn('[booking-dialog] Suspicious productId input detected', {
124
+ original: productId,
125
+ sanitized: sanitizedProductId,
126
+ });
104
127
  reportSuspiciousBookingProductId(productId, sanitizedProductId);
105
128
  }
106
- previouslyFocusedRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
129
+ previouslyFocusedRef.current =
130
+ document.activeElement instanceof HTMLElement
131
+ ? document.activeElement
132
+ : null;
107
133
  setStack([{ type: 'book-flow', productId: sanitizedProductId }]);
108
134
  setProductGridRestoreState(null);
109
135
  setIsOpen(true);
@@ -116,14 +142,24 @@ export function BookingDialogProvider({ children }: { children: ReactNode }) {
116
142
  const prev = previouslyFocusedRef.current;
117
143
  setIsOpen(false);
118
144
  requestAnimationFrame(() => {
119
- if (prev && typeof prev.focus === 'function') prev.focus();
145
+ if (prev && typeof prev.focus === 'function') {
146
+ prev.focus();
147
+ }
120
148
  });
121
149
  }, []);
122
150
 
123
- const push = useCallback((screen: BookingScreen, options?: { productGridState?: ProductGridRestoreState }) => {
124
- if (options?.productGridState) setProductGridRestoreState(options.productGridState);
125
- setStack((prev) => [...prev, screen]);
126
- }, []);
151
+ const push = useCallback(
152
+ (
153
+ screen: BookingScreen,
154
+ options?: { productGridState?: ProductGridRestoreState }
155
+ ) => {
156
+ if (options?.productGridState) {
157
+ setProductGridRestoreState(options.productGridState);
158
+ }
159
+ setStack((prev) => [...prev, screen]);
160
+ },
161
+ []
162
+ );
127
163
 
128
164
  const pop = useCallback(() => {
129
165
  setStack((prev) => (prev.length > 1 ? prev.slice(0, -1) : prev));
@@ -143,26 +179,37 @@ export function BookingDialogProvider({ children }: { children: ReactNode }) {
143
179
  const handleOpenWithFilter = (e: Event) => {
144
180
  const customEvent = e as CustomEvent<{ filterId: ProductGridFilterId }>;
145
181
  const filterId = customEvent.detail?.filterId;
146
- if (filterId) open({ filterId });
147
- else open();
182
+ if (filterId) {
183
+ open({ filterId });
184
+ } else {
185
+ open();
186
+ }
148
187
  };
149
188
  window.addEventListener(OPEN_BOOKING_WITH_FILTER, handleOpenWithFilter);
150
- return () => window.removeEventListener(OPEN_BOOKING_WITH_FILTER, handleOpenWithFilter);
189
+ return () =>
190
+ window.removeEventListener(OPEN_BOOKING_WITH_FILTER, handleOpenWithFilter);
151
191
  }, [open]);
152
192
 
153
193
  useEffect(() => {
154
194
  const handleOpenForProduct = (e: Event) => {
155
195
  const customEvent = e as CustomEvent<{ productId: string }>;
156
196
  const productId = customEvent.detail?.productId;
157
- if (productId) openForProduct(productId);
197
+ if (productId) {
198
+ openForProduct(productId);
199
+ }
158
200
  };
159
201
  window.addEventListener(OPEN_BOOKING_FOR_PRODUCT, handleOpenForProduct);
160
- return () => window.removeEventListener(OPEN_BOOKING_FOR_PRODUCT, handleOpenForProduct);
202
+ return () =>
203
+ window.removeEventListener(OPEN_BOOKING_FOR_PRODUCT, handleOpenForProduct);
161
204
  }, [openForProduct]);
162
205
 
206
+ // When user clicks back after Stripe redirect to /manage-booking, the page may be
207
+ // restored from bfcache with the modal still open. Close it on restore.
163
208
  useEffect(() => {
164
209
  const handlePageShow = (e: PageTransitionEvent) => {
165
- if (e.persisted && isOpen) close();
210
+ if (e.persisted && isOpen) {
211
+ close();
212
+ }
166
213
  };
167
214
  window.addEventListener('pageshow', handlePageShow);
168
215
  return () => window.removeEventListener('pageshow', handlePageShow);
@@ -172,27 +219,34 @@ export function BookingDialogProvider({ children }: { children: ReactNode }) {
172
219
  const handleBookNowClick = (e: Event) => {
173
220
  const target = e.target as HTMLElement;
174
221
  const link = target.closest('a[href^="#book-now"]') as HTMLAnchorElement | null;
175
- if (!link) return;
176
- e.preventDefault();
177
- const href = link.getAttribute('href') ?? '';
178
- const filterMatch = href.match(/^#book-now\/filter\/([a-z0-9-]+)$/i);
179
- if (filterMatch) {
180
- const filterId = filterMatch[1] as ProductGridFilterId;
181
- if (['sunrise', 'moraine-lake', 'lake-louise', 'emerald-lake', 'private'].includes(filterId)) {
182
- open({ filterId });
183
- return;
222
+ if (link) {
223
+ e.preventDefault();
224
+ const href = link.getAttribute('href') ?? '';
225
+ // #book-now/filter/moraine-lake -> open with filter
226
+ const filterMatch = href.match(/^#book-now\/filter\/([a-z0-9-]+)$/i);
227
+ if (filterMatch) {
228
+ const filterId = filterMatch[1] as ProductGridFilterId;
229
+ if (['sunrise', 'moraine-lake', 'lake-louise', 'emerald-lake', 'private'].includes(filterId)) {
230
+ open({ filterId });
231
+ return;
232
+ }
233
+ }
234
+ // #book-now/product-slug -> open that product
235
+ const productMatch = href.match(/^#book-now\/([a-z0-9-]+)$/i);
236
+ const productSlug = productMatch?.[1];
237
+ if (productSlug) {
238
+ openForProduct(productSlug);
239
+ } else {
240
+ open();
184
241
  }
185
242
  }
186
- const productMatch = href.match(/^#book-now\/([a-z0-9-]+)$/i);
187
- const productSlug = productMatch?.[1];
188
- if (productSlug) openForProduct(productSlug);
189
- else open();
190
243
  };
191
244
  document.addEventListener('click', handleBookNowClick);
192
245
  return () => document.removeEventListener('click', handleBookNowClick);
193
246
  }, [open, openForProduct]);
194
247
 
195
248
  const canGoBack = stack.length > 1;
249
+
196
250
  const value = useMemo(
197
251
  () => ({
198
252
  isOpen,
@@ -206,8 +260,23 @@ export function BookingDialogProvider({ children }: { children: ReactNode }) {
206
260
  productGridRestoreState,
207
261
  clearProductGridRestoreState,
208
262
  }),
209
- [isOpen, open, openForProduct, close, stack, push, pop, canGoBack, productGridRestoreState, clearProductGridRestoreState]
263
+ [
264
+ isOpen,
265
+ open,
266
+ openForProduct,
267
+ close,
268
+ stack,
269
+ push,
270
+ pop,
271
+ canGoBack,
272
+ productGridRestoreState,
273
+ clearProductGridRestoreState,
274
+ ]
210
275
  );
211
276
 
212
- return <BookingDialogContext.Provider value={value}>{children}</BookingDialogContext.Provider>;
277
+ return (
278
+ <BookingDialogContext.Provider value={value}>
279
+ {children}
280
+ </BookingDialogContext.Provider>
281
+ );
213
282
  }
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, useState } from 'react';
4
+ import BottomSheet from '@/components/bottom-sheet';
5
+
6
+ type BottomSheetContextType = {
7
+ openSheet: (content: React.ReactNode) => void;
8
+ closeSheet: () => void;
9
+ };
10
+
11
+ const BottomSheetContext = createContext<BottomSheetContextType | null>(null);
12
+
13
+ export function BottomSheetProvider({ children }: { children: React.ReactNode }) {
14
+ const [isOpen, setIsOpen] = useState(false);
15
+ const [content, setContent] = useState<React.ReactNode>(null);
16
+
17
+ const openSheet = (content: React.ReactNode) => {
18
+ setContent(content);
19
+ setIsOpen(true);
20
+ };
21
+
22
+ const closeSheet = () => {
23
+ setIsOpen(false);
24
+ };
25
+
26
+ return (
27
+ <BottomSheetContext.Provider value={{ openSheet, closeSheet }}>
28
+ {children}
29
+ <BottomSheet isOpen={isOpen} onClose={closeSheet}>
30
+ {content}
31
+ </BottomSheet>
32
+ </BottomSheetContext.Provider>
33
+ );
34
+ }
35
+
36
+ export function useBottomSheet() {
37
+ const context = useContext(BottomSheetContext);
38
+ if (!context) throw new Error('useBottomSheet must be used within BottomSheetProvider');
39
+ return context;
40
+ }
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type ReactNode,
11
+ } from 'react';
12
+ import type { PhotoDapSlug } from '@/lib/photo-dap-config';
13
+
14
+ export type DependentAddOnProductOptionChoice = {
15
+ dependentAddOnProductOptionId: string;
16
+ label: string;
17
+ photosLabel?: string;
18
+ startingAtLabel?: string;
19
+ };
20
+
21
+ export type DependentAddOnDialogOpenPayload = {
22
+ /** Card title shown in dialog header */
23
+ productDisplayTitle: string;
24
+ dependentAddOnProductId: string;
25
+ /** Fixed catalog option (no picker) */
26
+ dependentAddOnProductOptionId?: string;
27
+ /** When provided without a fixed option id, user picks one (e.g. 30 / 60 / 90 min) */
28
+ productOptions?: DependentAddOnProductOptionChoice[];
29
+ /**
30
+ * Default session-length id when multiple `productOptions` exist (e.g. manage-booking upsell probed 30 min).
31
+ * Unlike `dependentAddOnProductOptionId`, this does not hide the picker — the user can switch length.
32
+ */
33
+ initialSelectedProductOptionId?: string;
34
+ /** Hero + grid images (DapFlowCollage); Bunny CDN IDs */
35
+ collageImageIds?: string[];
36
+ /** Loads expandable copy from dap-descriptions */
37
+ dapDescriptionSlug?: PhotoDapSlug;
38
+ /**
39
+ * From DAP catalog / TicketBooth product — days before the photo session for full-refund cancellation.
40
+ * Availability API may override when it returns the same field.
41
+ */
42
+ cancellationDaysBeforeSession: number;
43
+ /**
44
+ * Pre-fill primary booking reference (e.g. manage-booking upsell after shuttle checkout).
45
+ * Accepts short or bookRef_ form; dialog normalizes for display.
46
+ */
47
+ initialPrimaryBookingReference?: string;
48
+ /** Optional pre-fill for booking-owner verification on DAP availability checks. */
49
+ initialPrimaryBookingLastName?: string;
50
+ };
51
+
52
+ interface DependentAddOnDialogContextValue {
53
+ isOpen: boolean;
54
+ payload: DependentAddOnDialogOpenPayload | null;
55
+ open: (p: DependentAddOnDialogOpenPayload) => void;
56
+ close: () => void;
57
+ }
58
+
59
+ const DependentAddOnDialogContext =
60
+ createContext<DependentAddOnDialogContextValue | null>(null);
61
+
62
+ export function useDependentAddOnDialog() {
63
+ const ctx = useContext(DependentAddOnDialogContext);
64
+ if (!ctx) {
65
+ throw new Error(
66
+ 'useDependentAddOnDialog must be used within DependentAddOnDialogProvider'
67
+ );
68
+ }
69
+ return ctx;
70
+ }
71
+
72
+ export function DependentAddOnDialogProvider({ children }: { children: ReactNode }) {
73
+ const [isOpen, setIsOpen] = useState(false);
74
+ const [payload, setPayload] = useState<DependentAddOnDialogOpenPayload | null>(null);
75
+ const previouslyFocusedRef = useRef<HTMLElement | null>(null);
76
+
77
+ const open = useCallback((p: DependentAddOnDialogOpenPayload) => {
78
+ previouslyFocusedRef.current =
79
+ document.activeElement instanceof HTMLElement ? document.activeElement : null;
80
+ setPayload(p);
81
+ setIsOpen(true);
82
+ }, []);
83
+
84
+ const close = useCallback(() => {
85
+ setIsOpen(false);
86
+ setPayload(null);
87
+ const prev = previouslyFocusedRef.current;
88
+ requestAnimationFrame(() => {
89
+ if (prev && typeof prev.focus === 'function') {
90
+ prev.focus();
91
+ }
92
+ });
93
+ }, []);
94
+
95
+ const value = useMemo(
96
+ () => ({ isOpen, payload, open, close }),
97
+ [isOpen, payload, open, close]
98
+ );
99
+
100
+ return (
101
+ <DependentAddOnDialogContext.Provider value={value}>
102
+ {children}
103
+ </DependentAddOnDialogContext.Provider>
104
+ );
105
+ }
package/src/radius.css ADDED
@@ -0,0 +1,5 @@
1
+ :root {
2
+ --border-radius-small: 10px;
3
+ --border-radius-medium: 24px;
4
+ --border-radius-pill: 50px;
5
+ }
@@ -0,0 +1,7 @@
1
+ :root {
2
+ --spacing-small: 8px;
3
+ --spacing-medium: 16px;
4
+ --spacing-large: 32px;
5
+ --spacing-xlarge: 64px;
6
+ --spacing-xxlarge: 92px;
7
+ }