@ticketboothapp/booking 0.1.10 → 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.
- package/package.json +1 -1
- package/src/colours.css +23 -0
- package/src/components/BookingDetails.module.css +1591 -0
- package/src/components/BookingDetails.tsx +2072 -354
- package/src/components/BookingWidget.tsx +28 -248
- package/src/components/JobApplicationDialog.module.css +440 -0
- package/src/components/JobApplicationDialog.tsx +620 -0
- package/src/components/ManageBookingView.tsx +344 -34
- package/src/components/PhoneInputWithCountry.module.css +131 -0
- package/src/components/PhoneInputWithCountry.tsx +44 -0
- package/src/components/PickupLocationDialog.module.css +360 -0
- package/src/components/PickupLocationDialog.tsx +357 -0
- package/src/components/PickupLocationMap.tsx +110 -0
- package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
- package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
- package/src/components/accordion.css +27 -0
- package/src/components/accordion.tsx +29 -0
- package/src/components/analytics/AnalyticsConsentRestore.tsx +19 -0
- package/src/components/analytics/AnalyticsScripts.tsx +106 -0
- package/src/components/analytics/CookieConsentBanner.css +86 -0
- package/src/components/analytics/CookieConsentBanner.tsx +102 -0
- package/src/components/booking/AddOnsSection.module.css +10 -0
- package/src/components/booking/AddOnsSection.tsx +184 -0
- package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
- package/src/components/booking/BookingDialog.module.css +643 -0
- package/src/components/booking/BookingDialog.tsx +356 -0
- package/src/components/booking/BookingFlow.tsx +4385 -0
- package/src/components/booking/BookingFlowCollage.module.css +148 -0
- package/src/components/booking/BookingFlowCollage.tsx +184 -0
- package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
- package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
- package/src/components/booking/BookingFlowPreview.tsx +51 -0
- package/src/components/booking/BookingProductGrid.module.css +359 -0
- package/src/components/booking/BookingProductGrid.tsx +497 -0
- package/src/components/booking/Calendar.module.css +616 -0
- package/src/components/{Calendar.tsx → booking/Calendar.tsx} +464 -247
- package/src/components/booking/CancellationPolicySelector.module.css +124 -0
- package/src/components/booking/CancellationPolicySelector.tsx +142 -0
- package/src/components/booking/ChangeBookingDialog.tsx +562 -0
- package/src/components/booking/CheckoutForm.module.css +244 -0
- package/src/components/booking/CheckoutForm.tsx +364 -0
- package/src/components/{CheckoutModal.tsx → booking/CheckoutModal.tsx} +176 -19
- package/src/components/booking/DapFlowCollage.tsx +88 -0
- package/src/components/booking/DapTourDescription.tsx +35 -0
- package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
- package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
- package/src/components/booking/InfoTooltip.tsx +108 -0
- package/src/components/booking/ItineraryBox.module.css +258 -0
- package/src/components/booking/ItineraryBox.tsx +550 -0
- package/src/components/{ItineraryBuilder.tsx → booking/ItineraryBuilder.tsx} +1 -2
- package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
- package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
- package/src/components/{MealDrinkAddOnSelector.tsx → booking/MealDrinkAddOnSelector.tsx} +21 -13
- package/src/components/booking/PickupLocationSelector.module.css +124 -0
- package/src/components/{PickupLocationSelector.tsx → booking/PickupLocationSelector.tsx} +315 -290
- package/src/components/booking/PickupTimeSelector.module.css +134 -0
- package/src/components/booking/PickupTimeSelector.tsx +112 -0
- package/src/components/{PriceBreakdown.tsx → booking/PriceBreakdown.tsx} +3 -3
- package/src/components/{PriceSummary.tsx → booking/PriceSummary.tsx} +51 -28
- package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
- package/src/components/booking/PromoCodeInput.module.css +166 -0
- package/src/components/booking/PromoCodeInput.tsx +99 -0
- package/src/components/booking/ReturnTimeSelector.module.css +173 -0
- package/src/components/booking/ReturnTimeSelector.tsx +145 -0
- package/src/components/{TermsAcceptance.tsx → booking/TermsAcceptance.tsx} +9 -8
- package/src/components/booking/TicketSelector.module.css +164 -0
- package/src/components/booking/TicketSelector.tsx +199 -0
- package/src/components/booking/TourDescription.module.css +304 -0
- package/src/components/booking/TourDescription.tsx +273 -0
- package/src/components/booking/booking-flow-ui.ts +15 -1
- package/src/components/booking/booking-flow.css +944 -0
- package/src/components/bottom-sheet.module.css +78 -0
- package/src/components/bottom-sheet.tsx +60 -0
- package/src/components/breadcrumb.module.css +40 -0
- package/src/components/breadcrumb.tsx +36 -0
- package/src/components/button.css +245 -0
- package/src/components/button.tsx +152 -0
- package/src/components/client-bottom-sheet.tsx +14 -0
- package/src/components/colorable-svg.tsx +29 -0
- package/src/components/conditional-footer.tsx +27 -0
- package/src/components/contact-us.module.css +147 -0
- package/src/components/contact-us.tsx +49 -0
- package/src/components/email-signup.css +151 -0
- package/src/components/email-signup.tsx +63 -0
- package/src/components/faq-wrapper.module.css +47 -0
- package/src/components/faq-wrapper.tsx +15 -0
- package/src/components/footer.css +187 -0
- package/src/components/footer.tsx +143 -0
- package/src/components/global-simple-modal.tsx +33 -0
- package/src/components/google-review-summary.module.css +77 -0
- package/src/components/google-review-summary.tsx +50 -0
- package/src/components/hero-image.css +13 -0
- package/src/components/hero-image.tsx +44 -0
- package/src/components/image.css +29 -0
- package/src/components/image.tsx +113 -0
- package/src/components/language-aware-link.tsx +72 -0
- package/src/components/language-switcher.module.css +124 -0
- package/src/components/language-switcher.tsx +75 -0
- package/src/components/map-section.css +59 -0
- package/src/components/map-section.tsx +63 -0
- package/src/components/navbar.module.css +152 -0
- package/src/components/navbar.tsx +125 -0
- package/src/components/parallax-provider.tsx +11 -0
- package/src/components/partner/PartnerBookingPage.module.css +130 -0
- package/src/components/partner/PartnerBookingPage.tsx +390 -0
- package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +19 -35
- package/src/components/product-tag.module.css +30 -0
- package/src/components/product-tag.tsx +34 -0
- package/src/components/product-theme-pages/best-option.module.css +70 -0
- package/src/components/product-theme-pages/best-option.tsx +35 -0
- package/src/components/product-theme-pages/extended-tour-options.module.css +22 -0
- package/src/components/product-theme-pages/extended-tour-options.tsx +11 -0
- package/src/components/product-theme-pages/image-modal.tsx +248 -0
- package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
- package/src/components/product-theme-pages/photo-gallery.tsx +90 -0
- package/src/components/product-theme-pages/product-theme-page-layout.module.css +13 -0
- package/src/components/product-theme-pages/product-theme-page-layout.tsx +67 -0
- package/src/components/product-theme-pages/top-of-fold.module.css +179 -0
- package/src/components/product-theme-pages/top-of-fold.tsx +80 -0
- package/src/components/product-tile/image-only-product-tile-desktop.module.css +106 -0
- package/src/components/product-tile/image-only-product-tile-desktop.tsx +56 -0
- package/src/components/product-tile/image-only-product-tile-mobile.module.css +122 -0
- package/src/components/product-tile/image-only-product-tile-mobile.tsx +89 -0
- package/src/components/product-tile/image-only-product-tile.tsx +44 -0
- package/src/components/product-tile/product-tile-card.module.css +84 -0
- package/src/components/product-tile/product-tile-card.tsx +61 -0
- package/src/components/review-highlights-section.css +85 -0
- package/src/components/review-highlights-section.tsx +127 -0
- package/src/components/season-closure-overlay.module.css +99 -0
- package/src/components/season-closure-overlay.tsx +98 -0
- package/src/components/simple-modal.tsx +69 -0
- package/src/components/simple-top-of-fold.module.css +76 -0
- package/src/components/simple-top-of-fold.tsx +34 -0
- package/src/components/spacer.css +41 -0
- package/src/components/spacer.tsx +23 -0
- package/src/components/star-rating.module.css +74 -0
- package/src/components/star-rating.tsx +48 -0
- package/src/components/terms/TermsContent.tsx +178 -0
- package/src/components/title-subtitle.module.css +10 -0
- package/src/components/title-subtitle.tsx +30 -0
- package/src/components/translatable-reviews.tsx +75 -0
- package/src/components/value-pill.module.css +59 -0
- package/src/components/value-pill.tsx +46 -0
- package/src/components/value-props.css +185 -0
- package/src/components/value-props.tsx +88 -0
- package/src/constants/booking-guide-quiz.ts +64 -0
- package/src/constants/contact-info.ts +2 -0
- package/src/constants/faq.ts +44 -0
- package/src/constants/images.ts +556 -0
- package/src/constants/json-ld/faq-json-ld.tsx +170 -0
- package/src/constants/json-ld/homepage-json-ld.tsx +138 -0
- package/src/constants/json-ld/job-posting-json-ld.tsx +92 -0
- package/src/constants/json-ld/organization-json-ld.tsx +62 -0
- package/src/constants/json-ld/page-json-ld.tsx +6 -0
- package/src/constants/json-ld/product-json-ld.tsx +154 -0
- package/src/constants/json-ld/review-json-ld.tsx +377 -0
- package/src/constants/navigation-links/footer-links.ts +48 -0
- package/src/constants/navigation-links/nav-bar-links.ts +41 -0
- package/src/constants/navigation-links/navigation-link.ts +6 -0
- package/src/constants/pill-values.ts +210 -0
- package/src/constants/products.ts +155 -0
- package/src/constants/quiz-recommendations.ts +506 -0
- package/src/constants/reviews.ts +75 -0
- package/src/constants/staff.ts +197 -0
- package/src/constants/value-props.ts +58 -0
- package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
- package/src/data/dap-descriptions/session-elopements.en.json +60 -0
- package/src/data/dap-descriptions/session-proposals.en.json +60 -0
- package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
- package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
- package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
- package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
- package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
- package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
- package/src/data/product-descriptions/private-tour.en.json +80 -0
- package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
- package/src/data/products-config.json +101 -0
- package/src/hooks/use-bottom-sheet.tsx +15 -0
- package/src/hooks/use-simple-modal.tsx +27 -0
- package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
- package/src/hooks/useEmailSubscription.tsx +103 -0
- package/src/hooks/useEmbeddedInIframe.ts +16 -0
- package/src/hooks/useIsBookingLaunchLive.ts +49 -0
- package/src/hooks/useQuiz.tsx +210 -0
- package/src/index.ts +27 -2
- package/src/lib/analytics.ts +197 -0
- package/src/lib/booking/booking-source.ts +20 -2
- package/src/lib/{checkout-breakdown.ts → booking/checkout-breakdown.ts} +1 -1
- package/src/lib/booking/correlation-id.ts +46 -0
- package/src/lib/{i18n → booking/i18n}/messages/en.json +48 -4
- package/src/lib/{i18n → booking/i18n}/messages/fr.json +48 -4
- package/src/lib/booking/itinerary-display.ts +36 -0
- package/src/lib/{itinerary-labels.ts → booking/itinerary-labels.ts} +1 -1
- package/src/lib/{location-calculations.ts → booking/location-calculations.ts} +4 -4
- package/src/lib/{location-utils.ts → booking/location-utils.ts} +26 -0
- package/src/lib/{map-utils.ts → booking/map-utils.ts} +3 -3
- package/src/lib/booking/normalize-booking-product-id.ts +7 -0
- package/src/lib/{pickup-location-types.ts → booking/pickup-location-types.ts} +2 -2
- package/src/lib/{pricing.ts → booking/pricing.ts} +2 -2
- package/src/lib/booking/product-option-id.ts +35 -0
- package/src/lib/booking/source-metadata.ts +72 -7
- package/src/lib/booking/sunday-week.ts +14 -0
- package/src/lib/booking/trace-context.ts +62 -0
- package/src/lib/booking-api.ts +1793 -0
- package/src/lib/{constants.ts → booking-constants.ts} +11 -5
- package/src/lib/booking-types.ts +36 -0
- package/src/lib/currency.ts +38 -45
- package/src/lib/dap-descriptions.ts +50 -0
- package/src/lib/dap-itinerary-preview.ts +315 -0
- package/src/lib/dependent-add-on-api.ts +434 -0
- package/src/lib/env.ts +89 -5
- package/src/lib/firebase.ts +20 -0
- package/src/lib/job-application-api.ts +83 -0
- package/src/lib/manage-booking-embed-print.ts +16 -0
- package/src/lib/manage-booking-post-checkout.ts +68 -0
- package/src/lib/photo-dap-config.ts +228 -0
- package/src/lib/pickup/map-utils.ts +56 -0
- package/src/lib/pickup/marker-icons.ts +19 -0
- package/src/lib/product-descriptions.ts +66 -0
- package/src/lib/products-config.ts +73 -0
- package/src/providers/booking-dialog-provider.tsx +107 -38
- package/src/providers/bottom-sheet-provider.tsx +40 -0
- package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
- package/src/radius.css +5 -0
- package/src/spacing.css +7 -0
- package/src/strings/en.json +1774 -0
- package/src/strings/es.json +1573 -0
- package/src/strings/fr.json +1573 -0
- package/src/strings/index.js +23 -0
- package/src/text-style.css +97 -0
- package/src/types/fareharbor.d.ts +12 -0
- package/src/types/quiz.ts +59 -0
- package/src/utils/currency-converter.ts +101 -0
- package/src/components/BookingFlow.tsx +0 -2952
- package/src/components/LanguageSwitcher.tsx +0 -30
- package/src/components/PrivateShuttleBookingFlow.tsx +0 -2290
- package/src/components/ProductList.tsx +0 -78
- package/src/components/WhatsAppPhoneInput.tsx +0 -224
- package/src/components/index.ts +0 -31
- package/src/lib/api.ts +0 -801
- package/src/lib/booking-api-auth.ts +0 -9
- package/src/lib/checkout-breakdown.test.ts +0 -70
- package/src/types/google-maps.d.ts +0 -2
- /package/src/components/{CurrencySwitcher.tsx → booking/CurrencySwitcher.tsx} +0 -0
- /package/src/components/{ErrorBoundary.tsx → booking/ErrorBoundary.tsx} +0 -0
- /package/src/lib/{i18n → booking/i18n}/config.ts +0 -0
- /package/src/lib/{i18n → booking/i18n}/index.tsx +0 -0
- /package/src/lib/{marker-icons.ts → booking/marker-icons.ts} +0 -0
- /package/src/lib/{places-api.ts → booking/places-api.ts} +0 -0
- /package/src/lib/{theme.ts → booking/theme.ts} +0 -0
- /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 '
|
|
14
|
-
import { withBookingOutboundHeaders } from '
|
|
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 '
|
|
19
|
-
import { ENV } from '
|
|
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: (
|
|
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>(
|
|
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)
|
|
91
|
+
if (!ctx) {
|
|
92
|
+
throw new Error('useBookingDialog must be used within BookingDialogProvider');
|
|
93
|
+
}
|
|
84
94
|
return ctx;
|
|
85
95
|
}
|
|
86
96
|
|
|
87
|
-
|
|
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<
|
|
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 =
|
|
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 =
|
|
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')
|
|
145
|
+
if (prev && typeof prev.focus === 'function') {
|
|
146
|
+
prev.focus();
|
|
147
|
+
}
|
|
120
148
|
});
|
|
121
149
|
}, []);
|
|
122
150
|
|
|
123
|
-
const push = useCallback(
|
|
124
|
-
|
|
125
|
-
|
|
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)
|
|
147
|
-
|
|
182
|
+
if (filterId) {
|
|
183
|
+
open({ filterId });
|
|
184
|
+
} else {
|
|
185
|
+
open();
|
|
186
|
+
}
|
|
148
187
|
};
|
|
149
188
|
window.addEventListener(OPEN_BOOKING_WITH_FILTER, handleOpenWithFilter);
|
|
150
|
-
return () =>
|
|
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)
|
|
197
|
+
if (productId) {
|
|
198
|
+
openForProduct(productId);
|
|
199
|
+
}
|
|
158
200
|
};
|
|
159
201
|
window.addEventListener(OPEN_BOOKING_FOR_PRODUCT, handleOpenForProduct);
|
|
160
|
-
return () =>
|
|
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)
|
|
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 (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
[
|
|
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
|
|
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