@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.
- package/package.json +29 -2
- package/src/assets/icons/minus.svg +7 -0
- package/src/assets/icons/partner-logos/getyourguide.svg +8 -0
- package/src/assets/icons/plus.svg +3 -0
- package/src/colours.css +23 -0
- package/src/components/BookingDetails.module.css +1591 -0
- package/src/components/BookingDetails.tsx +2264 -0
- package/src/components/BookingWidget.tsx +302 -0
- package/src/components/ManageBookingView.tsx +437 -0
- 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/PostBookingDependentAddOnUpsell.module.css +174 -0
- package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -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/booking/Calendar.tsx +1123 -0
- 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/booking/CheckoutModal.tsx +451 -0
- package/src/components/booking/CurrencySwitcher.tsx +81 -0
- 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/ErrorBoundary.tsx +63 -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/booking/ItineraryBuilder.tsx +82 -0
- package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
- package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
- package/src/components/booking/MealDrinkAddOnSelector.tsx +338 -0
- package/src/components/booking/PickupLocationSelector.module.css +124 -0
- package/src/components/booking/PickupLocationSelector.tsx +1566 -0
- package/src/components/booking/PickupTimeSelector.module.css +134 -0
- package/src/components/booking/PickupTimeSelector.tsx +112 -0
- package/src/components/booking/PriceBreakdown.tsx +154 -0
- package/src/components/booking/PriceSummary.tsx +234 -0
- 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/booking/TermsAcceptance.tsx +111 -0
- 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 +38 -0
- package/src/components/booking/booking-flow.css +944 -0
- package/src/components/button.css +245 -0
- package/src/components/button.tsx +152 -0
- package/src/components/colorable-svg.tsx +29 -0
- package/src/components/image.css +29 -0
- package/src/components/image.tsx +113 -0
- package/src/components/partner/PartnerBookingPage.module.css +130 -0
- package/src/components/partner/PartnerBookingPage.tsx +390 -0
- package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +45 -0
- package/src/components/product-tag.module.css +30 -0
- package/src/components/product-tag.tsx +34 -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/terms/TermsContent.tsx +178 -0
- package/src/components/value-pill.module.css +59 -0
- package/src/components/value-pill.tsx +46 -0
- package/src/constants/images.ts +556 -0
- package/src/constants/pill-values.ts +210 -0
- package/src/constants/products.ts +155 -0
- package/src/contexts/AvailabilitiesCacheContext.tsx +125 -0
- package/src/contexts/BookingAppContext.tsx +134 -0
- package/src/contexts/CompanyContext.tsx +70 -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/useBookingSourceMetadataFromLocation.ts +21 -0
- package/src/hooks/useIsBookingLaunchLive.ts +49 -0
- package/src/index.ts +79 -0
- package/src/lib/analytics.ts +197 -0
- package/src/lib/booking/booking-source.ts +51 -0
- package/src/lib/booking/checkout-breakdown.ts +69 -0
- package/src/lib/booking/correlation-id.ts +46 -0
- package/src/lib/booking/i18n/config.ts +21 -0
- package/src/lib/booking/i18n/index.tsx +144 -0
- package/src/lib/booking/i18n/messages/en.json +236 -0
- package/src/lib/booking/i18n/messages/fr.json +236 -0
- package/src/lib/booking/itinerary-display.ts +36 -0
- package/src/lib/booking/itinerary-labels.ts +70 -0
- package/src/lib/booking/location-calculations.ts +43 -0
- package/src/lib/booking/location-utils.ts +165 -0
- package/src/lib/booking/map-utils.ts +153 -0
- package/src/lib/booking/marker-icons.ts +113 -0
- package/src/lib/booking/normalize-booking-product-id.ts +21 -0
- package/src/lib/booking/pickup-location-types.ts +25 -0
- package/src/lib/booking/places-api.ts +154 -0
- package/src/lib/booking/pricing.ts +466 -0
- package/src/lib/booking/product-option-id.ts +35 -0
- package/src/lib/booking/source-metadata.ts +226 -0
- package/src/lib/booking/sunday-week.ts +14 -0
- package/src/lib/booking/theme.ts +83 -0
- package/src/lib/booking/trace-context.ts +62 -0
- package/src/lib/booking/utils.ts +9 -0
- package/src/lib/booking-api.ts +1793 -0
- package/src/lib/booking-constants.ts +23 -0
- package/src/lib/booking-ref.ts +13 -0
- package/src/lib/booking-types.ts +36 -0
- package/src/lib/currency.ts +81 -0
- 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 +96 -0
- 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/photo-packages.ts +75 -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 +282 -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 +56 -0
- package/src/utils/currency-converter.ts +101 -0
- package/tsconfig.json +8 -2
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useState,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from 'react';
|
|
13
|
+
import { getOrCreateBookingCorrelationId } from '@/lib/booking/correlation-id';
|
|
14
|
+
import { withBookingOutboundHeaders } from '@/lib/booking/trace-context';
|
|
15
|
+
import {
|
|
16
|
+
isSuspiciousBookingProductId,
|
|
17
|
+
normalizeBookingProductId,
|
|
18
|
+
} from '@/lib/booking/normalize-booking-product-id';
|
|
19
|
+
import { ENV } from '@/lib/env';
|
|
20
|
+
|
|
21
|
+
/** Filter IDs for the product grid. Must match BookingProductGrid FILTER_IDS. */
|
|
22
|
+
export type ProductGridFilterId =
|
|
23
|
+
| 'all'
|
|
24
|
+
| 'sunrise'
|
|
25
|
+
| 'moraine-lake'
|
|
26
|
+
| 'lake-louise'
|
|
27
|
+
| 'emerald-lake'
|
|
28
|
+
| 'private';
|
|
29
|
+
|
|
30
|
+
export type BookingScreen =
|
|
31
|
+
| { type: 'product-grid'; filterId?: ProductGridFilterId }
|
|
32
|
+
| { type: 'book-flow'; productId: string };
|
|
33
|
+
|
|
34
|
+
export type ProductGridRestoreState = {
|
|
35
|
+
expandedId: string | null;
|
|
36
|
+
scrollTop: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const OPEN_BOOKING_FOR_PRODUCT = 'openBookingForProduct';
|
|
40
|
+
export const OPEN_BOOKING_WITH_FILTER = 'openBookingWithFilter';
|
|
41
|
+
export const BOOKING_FLOW_ABANDON_EVENT = 'ticketbooth:booking-flow-abandon';
|
|
42
|
+
|
|
43
|
+
function reportSuspiciousBookingProductId(original: string, sanitized: string): void {
|
|
44
|
+
if (typeof window === 'undefined') return;
|
|
45
|
+
const telemetryEndpoint = `${ENV.API_URL}/1/client-telemetry`;
|
|
46
|
+
const correlationId = getOrCreateBookingCorrelationId();
|
|
47
|
+
const event = {
|
|
48
|
+
event: 'BOOKING_DIALOG_SUSPICIOUS_PRODUCT_ID',
|
|
49
|
+
endpoint: '/booking-open',
|
|
50
|
+
correlationId,
|
|
51
|
+
originalProductId: original,
|
|
52
|
+
sanitizedProductId: sanitized,
|
|
53
|
+
pageUrl: window.location.href,
|
|
54
|
+
referrer: document.referrer || null,
|
|
55
|
+
userAgent: window.navigator.userAgent,
|
|
56
|
+
occurredAt: new Date().toISOString(),
|
|
57
|
+
stack: new Error('Suspicious booking productId input').stack ?? null,
|
|
58
|
+
};
|
|
59
|
+
fetch(telemetryEndpoint, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: withBookingOutboundHeaders({ 'Content-Type': 'application/json' }),
|
|
62
|
+
body: JSON.stringify(event),
|
|
63
|
+
keepalive: true,
|
|
64
|
+
}).catch(() => {
|
|
65
|
+
// Never throw from telemetry reporting.
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface BookingDialogContextValue {
|
|
70
|
+
isOpen: boolean;
|
|
71
|
+
open: (options?: { filterId?: ProductGridFilterId }) => void;
|
|
72
|
+
openForProduct: (productId: string) => void;
|
|
73
|
+
close: (options?: { reason?: 'user' | 'completed' }) => void;
|
|
74
|
+
stack: BookingScreen[];
|
|
75
|
+
push: (
|
|
76
|
+
screen: BookingScreen,
|
|
77
|
+
options?: { productGridState?: ProductGridRestoreState }
|
|
78
|
+
) => void;
|
|
79
|
+
pop: () => void;
|
|
80
|
+
canGoBack: boolean;
|
|
81
|
+
productGridRestoreState: ProductGridRestoreState | null;
|
|
82
|
+
clearProductGridRestoreState: () => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const BookingDialogContext = createContext<BookingDialogContextValue | null>(
|
|
86
|
+
null
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
export function useBookingDialog() {
|
|
90
|
+
const ctx = useContext(BookingDialogContext);
|
|
91
|
+
if (!ctx) {
|
|
92
|
+
throw new Error('useBookingDialog must be used within BookingDialogProvider');
|
|
93
|
+
}
|
|
94
|
+
return ctx;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface BookingDialogProviderProps {
|
|
98
|
+
children: ReactNode;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function BookingDialogProvider({ children }: BookingDialogProviderProps) {
|
|
102
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
103
|
+
const [stack, setStack] = useState<BookingScreen[]>([{ type: 'product-grid' }]);
|
|
104
|
+
const [productGridRestoreState, setProductGridRestoreState] = useState<
|
|
105
|
+
ProductGridRestoreState | null
|
|
106
|
+
>(null);
|
|
107
|
+
const previouslyFocusedRef = useRef<HTMLElement | null>(null);
|
|
108
|
+
|
|
109
|
+
const open = useCallback((options?: { filterId?: ProductGridFilterId }) => {
|
|
110
|
+
previouslyFocusedRef.current =
|
|
111
|
+
document.activeElement instanceof HTMLElement
|
|
112
|
+
? document.activeElement
|
|
113
|
+
: null;
|
|
114
|
+
setStack([{ type: 'product-grid', filterId: options?.filterId }]);
|
|
115
|
+
setProductGridRestoreState(null);
|
|
116
|
+
setIsOpen(true);
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
const openForProduct = useCallback((productId: string) => {
|
|
120
|
+
const sanitizedProductId = normalizeBookingProductId(productId);
|
|
121
|
+
if (!sanitizedProductId) return;
|
|
122
|
+
if (isSuspiciousBookingProductId(productId)) {
|
|
123
|
+
console.warn('[booking-dialog] Suspicious productId input detected', {
|
|
124
|
+
original: productId,
|
|
125
|
+
sanitized: sanitizedProductId,
|
|
126
|
+
});
|
|
127
|
+
reportSuspiciousBookingProductId(productId, sanitizedProductId);
|
|
128
|
+
}
|
|
129
|
+
previouslyFocusedRef.current =
|
|
130
|
+
document.activeElement instanceof HTMLElement
|
|
131
|
+
? document.activeElement
|
|
132
|
+
: null;
|
|
133
|
+
setStack([{ type: 'book-flow', productId: sanitizedProductId }]);
|
|
134
|
+
setProductGridRestoreState(null);
|
|
135
|
+
setIsOpen(true);
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
const close = useCallback((options?: { reason?: 'user' | 'completed' }) => {
|
|
139
|
+
if (options?.reason !== 'completed' && typeof window !== 'undefined') {
|
|
140
|
+
window.dispatchEvent(new Event(BOOKING_FLOW_ABANDON_EVENT));
|
|
141
|
+
}
|
|
142
|
+
const prev = previouslyFocusedRef.current;
|
|
143
|
+
setIsOpen(false);
|
|
144
|
+
requestAnimationFrame(() => {
|
|
145
|
+
if (prev && typeof prev.focus === 'function') {
|
|
146
|
+
prev.focus();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
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
|
+
);
|
|
163
|
+
|
|
164
|
+
const pop = useCallback(() => {
|
|
165
|
+
setStack((prev) => (prev.length > 1 ? prev.slice(0, -1) : prev));
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
const clearProductGridRestoreState = useCallback(() => {
|
|
169
|
+
setProductGridRestoreState(null);
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
const handleOpen = () => open();
|
|
174
|
+
window.addEventListener('openSimpleModal', handleOpen);
|
|
175
|
+
return () => window.removeEventListener('openSimpleModal', handleOpen);
|
|
176
|
+
}, [open]);
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const handleOpenWithFilter = (e: Event) => {
|
|
180
|
+
const customEvent = e as CustomEvent<{ filterId: ProductGridFilterId }>;
|
|
181
|
+
const filterId = customEvent.detail?.filterId;
|
|
182
|
+
if (filterId) {
|
|
183
|
+
open({ filterId });
|
|
184
|
+
} else {
|
|
185
|
+
open();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
window.addEventListener(OPEN_BOOKING_WITH_FILTER, handleOpenWithFilter);
|
|
189
|
+
return () =>
|
|
190
|
+
window.removeEventListener(OPEN_BOOKING_WITH_FILTER, handleOpenWithFilter);
|
|
191
|
+
}, [open]);
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
const handleOpenForProduct = (e: Event) => {
|
|
195
|
+
const customEvent = e as CustomEvent<{ productId: string }>;
|
|
196
|
+
const productId = customEvent.detail?.productId;
|
|
197
|
+
if (productId) {
|
|
198
|
+
openForProduct(productId);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
window.addEventListener(OPEN_BOOKING_FOR_PRODUCT, handleOpenForProduct);
|
|
202
|
+
return () =>
|
|
203
|
+
window.removeEventListener(OPEN_BOOKING_FOR_PRODUCT, handleOpenForProduct);
|
|
204
|
+
}, [openForProduct]);
|
|
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.
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
const handlePageShow = (e: PageTransitionEvent) => {
|
|
210
|
+
if (e.persisted && isOpen) {
|
|
211
|
+
close();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
window.addEventListener('pageshow', handlePageShow);
|
|
215
|
+
return () => window.removeEventListener('pageshow', handlePageShow);
|
|
216
|
+
}, [isOpen, close]);
|
|
217
|
+
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
const handleBookNowClick = (e: Event) => {
|
|
220
|
+
const target = e.target as HTMLElement;
|
|
221
|
+
const link = target.closest('a[href^="#book-now"]') as HTMLAnchorElement | null;
|
|
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();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
document.addEventListener('click', handleBookNowClick);
|
|
245
|
+
return () => document.removeEventListener('click', handleBookNowClick);
|
|
246
|
+
}, [open, openForProduct]);
|
|
247
|
+
|
|
248
|
+
const canGoBack = stack.length > 1;
|
|
249
|
+
|
|
250
|
+
const value = useMemo(
|
|
251
|
+
() => ({
|
|
252
|
+
isOpen,
|
|
253
|
+
open,
|
|
254
|
+
openForProduct,
|
|
255
|
+
close,
|
|
256
|
+
stack,
|
|
257
|
+
push,
|
|
258
|
+
pop,
|
|
259
|
+
canGoBack,
|
|
260
|
+
productGridRestoreState,
|
|
261
|
+
clearProductGridRestoreState,
|
|
262
|
+
}),
|
|
263
|
+
[
|
|
264
|
+
isOpen,
|
|
265
|
+
open,
|
|
266
|
+
openForProduct,
|
|
267
|
+
close,
|
|
268
|
+
stack,
|
|
269
|
+
push,
|
|
270
|
+
pop,
|
|
271
|
+
canGoBack,
|
|
272
|
+
productGridRestoreState,
|
|
273
|
+
clearProductGridRestoreState,
|
|
274
|
+
]
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<BookingDialogContext.Provider value={value}>
|
|
279
|
+
{children}
|
|
280
|
+
</BookingDialogContext.Provider>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
@@ -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