@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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One opaque id per browser tab/session for joining client telemetry → TicketBooth Lambda logs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const BOOKING_CORRELATION_HEADER = 'X-Correlation-Id';
|
|
6
|
+
|
|
7
|
+
const STORAGE_KEY = 'tb_booking_correlation_id';
|
|
8
|
+
|
|
9
|
+
function newCorrelationId(): string {
|
|
10
|
+
try {
|
|
11
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
12
|
+
return crypto.randomUUID();
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
/* fall through */
|
|
16
|
+
}
|
|
17
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Stable for the lifetime of this tab's sessionStorage (until the tab closes / storage cleared). */
|
|
21
|
+
export function getOrCreateBookingCorrelationId(): string {
|
|
22
|
+
if (typeof window === 'undefined') return '';
|
|
23
|
+
try {
|
|
24
|
+
let id = sessionStorage.getItem(STORAGE_KEY);
|
|
25
|
+
if (!id) {
|
|
26
|
+
id = newCorrelationId();
|
|
27
|
+
sessionStorage.setItem(STORAGE_KEY, id);
|
|
28
|
+
}
|
|
29
|
+
return id;
|
|
30
|
+
} catch {
|
|
31
|
+
return newCorrelationId();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Merge correlation header into outbound API headers (browser only). */
|
|
36
|
+
export function withBookingCorrelationId(
|
|
37
|
+
headers: Record<string, string>
|
|
38
|
+
): Record<string, string> {
|
|
39
|
+
if (typeof window === 'undefined') return headers;
|
|
40
|
+
const id = getOrCreateBookingCorrelationId();
|
|
41
|
+
if (!id) return headers;
|
|
42
|
+
return {
|
|
43
|
+
...headers,
|
|
44
|
+
[BOOKING_CORRELATION_HEADER]: id,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
"change": "Change",
|
|
7
7
|
"cancel": "Cancel",
|
|
8
8
|
"close": "Close",
|
|
9
|
+
"videoPause": "Pause video",
|
|
10
|
+
"videoPlay": "Play video",
|
|
11
|
+
"videoMute": "Mute",
|
|
12
|
+
"videoUnmute": "Unmute",
|
|
9
13
|
"loading": "Loading...",
|
|
10
14
|
"error": "Error",
|
|
11
15
|
"total": "Total",
|
|
@@ -19,6 +23,13 @@
|
|
|
19
23
|
"requestDifferentTime": "Request different time",
|
|
20
24
|
"preferredPickupTime": "Preferred pickup time",
|
|
21
25
|
"selectReturnTime": "Select Return Time",
|
|
26
|
+
"selectPhotoSessionTime": "Select session time",
|
|
27
|
+
"dapPhotoSessionAddOn": "Photo session (add-on)",
|
|
28
|
+
"dapYourCurrentItinerary": "Your current itinerary",
|
|
29
|
+
"dapRefItineraryLoading": "Looking up your booking…",
|
|
30
|
+
"dapRefItineraryEmpty": "We didn’t get shuttle times for this reference yet. Your booking may still be valid — choose a session length (if needed) and load available times.",
|
|
31
|
+
"dapRefItineraryLookupFailed": "We couldn’t find a booking with that reference, or it isn’t eligible for this add-on. Double-check the code from your confirmation.",
|
|
32
|
+
"dapItineraryFallbackFoot": "After you pay, you’ll see this add-on with your full itinerary in Manage booking.",
|
|
22
33
|
"mostPopular": "Most popular",
|
|
23
34
|
"pickupAtTourStart": "<b>Your pickup time depends on your pickup location.</b><br>Your exact pickup time will be confirmed once you have confirmed your pickup location. Pickups start in Canmore and end in Lake Louise.",
|
|
24
35
|
"pickupAtTourStartLocation": "tour start location",
|
|
@@ -53,15 +64,25 @@
|
|
|
53
64
|
"dropOffAtPickup": "Arrive at {pickupLocation}",
|
|
54
65
|
"yourPickupLocation": "your pickup location",
|
|
55
66
|
"tickets": "Tickets",
|
|
67
|
+
"overbookingWarnDeparture": "WARN OVERBOOKING: Only {count} seats available on {time} departure.",
|
|
68
|
+
"overbookingWarnReturn": "WARN OVERBOOKING: Only {count} seats available on {time} return.",
|
|
69
|
+
"overbookingWarnBoth": "WARN OVERBOOKING: Only {count} seats available on both trip directions.",
|
|
70
|
+
"overbookingResourcesSuffix": "({count} shared resources.)",
|
|
71
|
+
"adminBookingLoadLine1": "Booking {projected} /",
|
|
72
|
+
"adminBookingLoadLine2": "{total} total",
|
|
56
73
|
"available": "available",
|
|
57
74
|
"soldOut": "Sold out",
|
|
58
75
|
"continueToPayment": "Continue to Payment",
|
|
76
|
+
"continueToSecurePayment": "Continue to secure payment",
|
|
77
|
+
"preparingCheckout": "Preparing…",
|
|
59
78
|
"creatingReservation": "Creating reservation...",
|
|
60
79
|
"securePayment": "Secure payment powered by Stripe",
|
|
61
80
|
"selectTimeAndTickets": "Please select a time and at least one ticket",
|
|
62
81
|
"selectPickupLocation": "Please select a pickup location",
|
|
63
82
|
"loadingTimes": "Loading available times...",
|
|
64
83
|
"noAvailability": "No availability found for the next 30 days. Please check back later.",
|
|
84
|
+
"seeFullTourDescription": "See full tour description",
|
|
85
|
+
"seeFullAddOnDescription": "See full experience details",
|
|
65
86
|
"communicationPreference": "How would you like to receive future communication (confirmation, reminders, etc.)?",
|
|
66
87
|
"communicationEmail": "Email",
|
|
67
88
|
"communicationEmailDesc": "Send confirmation via email",
|
|
@@ -74,17 +95,24 @@
|
|
|
74
95
|
"invalidPhoneNumber": "Please enter a valid phone number",
|
|
75
96
|
"selectCommunicationPreference": "Please select how you would like to receive your confirmation",
|
|
76
97
|
"enterEmail": "Please enter your email address",
|
|
98
|
+
"enterFirstName": "Please enter your first name",
|
|
77
99
|
"enterLastName": "Please enter your last name",
|
|
78
100
|
"enterPhoneNumber": "Please enter your phone number",
|
|
79
101
|
"firstName": "First Name",
|
|
80
102
|
"firstNamePlaceholder": "Jane",
|
|
81
103
|
"lastName": "Last Name",
|
|
82
104
|
"lastNamePlaceholder": "Smith",
|
|
105
|
+
"dapNoLastNameOnFile": "We couldn’t find a last name on this booking in our system. Please contact us, or use Manage booking with the last name from your confirmation email.",
|
|
106
|
+
"dapCancellationPolicyHeading": "Cancellation policy",
|
|
107
|
+
"dapCancellationPolicyBody": "You may cancel this add-on for a full refund up to {days} {daysUnit} before your scheduled photo session. Cancellations made after that deadline may forfeit the entire add-on cost.",
|
|
108
|
+
"dapCancellationPolicyDayUnit": "day",
|
|
109
|
+
"dapCancellationPolicyDaysUnit": "days",
|
|
83
110
|
"noActiveOption": "No active product options available",
|
|
84
111
|
"people": "people",
|
|
85
112
|
"person": "person",
|
|
86
113
|
"rounding": "Rounding",
|
|
87
114
|
"deposit": "Deposit",
|
|
115
|
+
"totalOwedForBookingChange": "Total owed for booking difference",
|
|
88
116
|
"subtotal": "Subtotal",
|
|
89
117
|
"tax": "Taxes and fees",
|
|
90
118
|
"returnOption": "Return Option",
|
|
@@ -110,8 +138,11 @@
|
|
|
110
138
|
"promoCodesCannotStackWithDiscounts": "Promo codes cannot be stacked with deals",
|
|
111
139
|
"discount": "Discount",
|
|
112
140
|
"cancellationPolicy": "Cancellation policy",
|
|
113
|
-
"
|
|
114
|
-
"
|
|
141
|
+
"promoIncludesCancellationPolicy": "This promo includes {label} cancellation policy",
|
|
142
|
+
"cancellationRefundTierDays": "Cancel <b>{days} days before</b> for <b>{percent}% refund</b>",
|
|
143
|
+
"cancellationRefundTierHours": "Cancel <b>{hours} hours before</b> for <b>{percent}% refund</b>",
|
|
144
|
+
"cancellationNoRefunds": "<b>No cancellation, no refunds</b>. You can make changes to your booking up to <b>{days} days before</b>.",
|
|
145
|
+
"cancellationNoRefundsNoDays": "<b>No cancellation, no refunds</b>. You can make changes to your booking before your booking date.",
|
|
115
146
|
"cancellationStandard": "Standard cancellation",
|
|
116
147
|
"included": "Included",
|
|
117
148
|
"flexibleCancellation": "Flexible cancellation",
|
|
@@ -145,13 +176,18 @@
|
|
|
145
176
|
"yesAddNowSubtext": "You can change this later.",
|
|
146
177
|
"skipWarningTitle": "Important Notice",
|
|
147
178
|
"skipWarningMessage": "You are not guaranteed to be picked up if you do not <b>update your booking with your pickup location at least 12 hours prior</b>.",
|
|
148
|
-
"iUnderstand": "I understand"
|
|
179
|
+
"iUnderstand": "I understand",
|
|
180
|
+
"nevermindSelectLocation": "Nevermind, I'll select my location",
|
|
181
|
+
"outsideServiceArea": "This address is outside our service area. We offer pickups between Lake Louise and Kananaskis. Please choose a preset location or search for an address in that area."
|
|
149
182
|
},
|
|
150
183
|
"calendar": {
|
|
151
184
|
"previousWeeks": "Previous 2 weeks",
|
|
152
185
|
"nextWeeks": "Next 2 weeks",
|
|
153
186
|
"soldOut": "Sold out",
|
|
154
187
|
"left": "{count} left",
|
|
188
|
+
"timesAvailable": "{count} times",
|
|
189
|
+
"spotsAvailable": "{count} available",
|
|
190
|
+
"available": "Available",
|
|
155
191
|
"days": {
|
|
156
192
|
"sun": "SUN",
|
|
157
193
|
"mon": "MON",
|
|
@@ -179,7 +215,15 @@
|
|
|
179
215
|
"products": {
|
|
180
216
|
"backToExperiences": "Back to experiences",
|
|
181
217
|
"from": "From",
|
|
182
|
-
"noDescription": "No description available"
|
|
218
|
+
"noDescription": "No description available",
|
|
219
|
+
"productGridFilters": {
|
|
220
|
+
"all": "All",
|
|
221
|
+
"moraineLake": "Moraine Lake",
|
|
222
|
+
"lakeLouise": "Lake Louise",
|
|
223
|
+
"emeraldLake": "Emerald Lake",
|
|
224
|
+
"private": "Private",
|
|
225
|
+
"sunrise": "Sunrise"
|
|
226
|
+
}
|
|
183
227
|
},
|
|
184
228
|
"terms": {
|
|
185
229
|
"title": "Terms & Conditions",
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
"change": "Changer",
|
|
7
7
|
"cancel": "Annuler",
|
|
8
8
|
"close": "Fermer",
|
|
9
|
+
"videoPause": "Mettre en pause",
|
|
10
|
+
"videoPlay": "Lire la vidéo",
|
|
11
|
+
"videoMute": "Couper le son",
|
|
12
|
+
"videoUnmute": "Activer le son",
|
|
9
13
|
"loading": "Chargement...",
|
|
10
14
|
"error": "Erreur",
|
|
11
15
|
"total": "Total",
|
|
@@ -19,6 +23,13 @@
|
|
|
19
23
|
"requestDifferentTime": "Demander une autre heure",
|
|
20
24
|
"preferredPickupTime": "Heure de prise en charge préférée",
|
|
21
25
|
"selectReturnTime": "Sélectionner l'heure de retour",
|
|
26
|
+
"selectPhotoSessionTime": "Choisir l'heure de la séance photo",
|
|
27
|
+
"dapPhotoSessionAddOn": "Séance photo (option)",
|
|
28
|
+
"dapYourCurrentItinerary": "Votre itinéraire actuel",
|
|
29
|
+
"dapRefItineraryLoading": "Recherche de votre réservation…",
|
|
30
|
+
"dapRefItineraryEmpty": "Nous n’avons pas encore d’horaires de navette pour cette référence. Votre réservation peut quand même être valide — choisissez une durée de séance (si besoin) et chargez les heures disponibles.",
|
|
31
|
+
"dapRefItineraryLookupFailed": "Nous n’avons pas trouvé de réservation avec cette référence, ou elle n’est pas admissible à cette option. Vérifiez le code dans votre courriel de confirmation.",
|
|
32
|
+
"dapItineraryFallbackFoot": "Après le paiement, vous verrez cette option avec l’itinéraire complet dans Gérer la réservation.",
|
|
22
33
|
"mostPopular": "Le plus populaire",
|
|
23
34
|
"pickupAtTourStart": "<b>Votre heure de prise en charge dépend de votre lieu de prise en charge.</b><br>Votre heure de prise en charge exacte sera confirmée une fois que vous aurez confirmé votre lieu de prise en charge. Les prises en charge commencent à Canmore et se terminent à Lake Louise.",
|
|
24
35
|
"pickupAtTourStartLocation": "lieu de départ du tour",
|
|
@@ -53,15 +64,25 @@
|
|
|
53
64
|
"dropOffAtPickup": "Arrivée à {pickupLocation}",
|
|
54
65
|
"yourPickupLocation": "votre lieu de prise en charge",
|
|
55
66
|
"tickets": "Billets",
|
|
67
|
+
"overbookingWarnDeparture": "ALERTE SURRÉSERVATION : seulement {count} places pour le départ à {time}.",
|
|
68
|
+
"overbookingWarnReturn": "ALERTE SURRÉSERVATION : seulement {count} places pour le retour à {time}.",
|
|
69
|
+
"overbookingWarnBoth": "ALERTE SURRÉSERVATION : seulement {count} places sur les deux segments du trajet (aller et retour).",
|
|
70
|
+
"overbookingResourcesSuffix": "({count} ressources partagées.)",
|
|
71
|
+
"adminBookingLoadLine1": "Réservation {projected} /",
|
|
72
|
+
"adminBookingLoadLine2": "{total} au total",
|
|
56
73
|
"available": "disponible",
|
|
57
74
|
"soldOut": "Complet",
|
|
58
75
|
"continueToPayment": "Continuer vers le paiement",
|
|
76
|
+
"continueToSecurePayment": "Continuer vers le paiement sécurisé",
|
|
77
|
+
"preparingCheckout": "Préparation…",
|
|
59
78
|
"creatingReservation": "Création de la réservation...",
|
|
60
79
|
"securePayment": "Paiement sécurisé par Stripe",
|
|
61
80
|
"selectTimeAndTickets": "Veuillez sélectionner une heure et au moins un billet",
|
|
62
81
|
"selectPickupLocation": "Veuillez sélectionner un lieu de prise en charge",
|
|
63
82
|
"loadingTimes": "Chargement des heures disponibles...",
|
|
64
83
|
"noAvailability": "Aucune disponibilité trouvée pour les 30 prochains jours. Veuillez réessayer plus tard.",
|
|
84
|
+
"seeFullTourDescription": "Voir la description complète du circuit",
|
|
85
|
+
"seeFullAddOnDescription": "Voir tous les détails de l'expérience",
|
|
65
86
|
"communicationPreference": "Comment souhaitez-vous recevoir les communications futures (confirmation, rappels, etc.)?",
|
|
66
87
|
"communicationEmail": "Courriel",
|
|
67
88
|
"communicationEmailDesc": "Envoyer la confirmation par courriel",
|
|
@@ -74,17 +95,24 @@
|
|
|
74
95
|
"invalidPhoneNumber": "Veuillez entrer un numéro de téléphone valide",
|
|
75
96
|
"selectCommunicationPreference": "Veuillez sélectionner comment vous souhaitez recevoir votre confirmation",
|
|
76
97
|
"enterEmail": "Veuillez entrer votre adresse courriel",
|
|
98
|
+
"enterFirstName": "Veuillez entrer votre prénom",
|
|
77
99
|
"enterLastName": "Veuillez entrer votre nom de famille",
|
|
78
100
|
"enterPhoneNumber": "Veuillez entrer votre numéro de téléphone",
|
|
79
101
|
"firstName": "Prénom",
|
|
80
102
|
"firstNamePlaceholder": "Jeanne",
|
|
81
103
|
"lastName": "Nom de famille",
|
|
82
104
|
"lastNamePlaceholder": "Dupont",
|
|
105
|
+
"dapNoLastNameOnFile": "Nous n’avons pas trouvé de nom de famille pour cette réservation. Contactez-nous, ou utilisez Gérer la réservation avec le nom figurant sur votre courriel de confirmation.",
|
|
106
|
+
"dapCancellationPolicyHeading": "Politique d’annulation",
|
|
107
|
+
"dapCancellationPolicyBody": "Vous pouvez annuler cette option avec remboursement intégral jusqu’à {days} {daysUnit} avant votre séance photo prévue. Toute annulation après cette date peut entraîner la perte du montant total de l’option.",
|
|
108
|
+
"dapCancellationPolicyDayUnit": "jour",
|
|
109
|
+
"dapCancellationPolicyDaysUnit": "jours",
|
|
83
110
|
"noActiveOption": "Aucune option de produit disponible",
|
|
84
111
|
"people": "personnes",
|
|
85
112
|
"person": "personne",
|
|
86
113
|
"rounding": "Arrondi",
|
|
87
114
|
"deposit": "Acompte",
|
|
115
|
+
"totalOwedForBookingChange": "Montant dû pour le changement de réservation",
|
|
88
116
|
"subtotal": "Sous-total",
|
|
89
117
|
"tax": "Taxes et frais",
|
|
90
118
|
"returnOption": "Option de retour",
|
|
@@ -110,8 +138,11 @@
|
|
|
110
138
|
"promoCodesCannotStackWithDiscounts": "Les codes promo ne peuvent pas être cumulés avec les offres",
|
|
111
139
|
"discount": "Réduction",
|
|
112
140
|
"cancellationPolicy": "Politique d'annulation",
|
|
113
|
-
"
|
|
114
|
-
"
|
|
141
|
+
"promoIncludesCancellationPolicy": "Ce code promo inclut la politique d'annulation {label}",
|
|
142
|
+
"cancellationRefundTierDays": "Annuler <b>{days} jours avant</b> pour <b>{percent}% de remboursement</b>",
|
|
143
|
+
"cancellationRefundTierHours": "Annuler <b>{hours} heures avant</b> pour <b>{percent}% de remboursement</b>",
|
|
144
|
+
"cancellationNoRefunds": "<b>Pas d'annulation, pas de remboursement</b>. Vous pouvez modifier votre réservation jusqu'à <b>{days} jours avant</b>.",
|
|
145
|
+
"cancellationNoRefundsNoDays": "<b>Pas d'annulation, pas de remboursement</b>. Vous pouvez modifier votre réservation avant la date de votre réservation.",
|
|
115
146
|
"cancellationStandard": "Annulation standard",
|
|
116
147
|
"included": "Inclus",
|
|
117
148
|
"flexibleCancellation": "Annulation flexible",
|
|
@@ -145,13 +176,18 @@
|
|
|
145
176
|
"yesAddNowSubtext": "Vous pourrez modifier cela plus tard.",
|
|
146
177
|
"skipWarningTitle": "Avis important",
|
|
147
178
|
"skipWarningMessage": "Vous n'êtes pas garanti d'être pris en charge si vous ne <b>mettez pas à jour votre réservation avec votre lieu de prise en charge au moins 12 heures à l'avance</b>.",
|
|
148
|
-
"iUnderstand": "Je comprends"
|
|
179
|
+
"iUnderstand": "Je comprends",
|
|
180
|
+
"nevermindSelectLocation": "Annuler, je vais sélectionner mon lieu",
|
|
181
|
+
"outsideServiceArea": "Cette adresse est en dehors de notre zone de service. Nous proposons des prises en charge entre le lac Louise et Kananaskis. Veuillez choisir un lieu prédéfini ou rechercher une adresse dans cette zone."
|
|
149
182
|
},
|
|
150
183
|
"calendar": {
|
|
151
184
|
"previousWeeks": "2 semaines précédentes",
|
|
152
185
|
"nextWeeks": "2 semaines suivantes",
|
|
153
186
|
"soldOut": "Complet",
|
|
154
187
|
"left": "{count} restant",
|
|
188
|
+
"timesAvailable": "{count} départs",
|
|
189
|
+
"spotsAvailable": "{count} places",
|
|
190
|
+
"available": "Disponible",
|
|
155
191
|
"days": {
|
|
156
192
|
"sun": "DIM",
|
|
157
193
|
"mon": "LUN",
|
|
@@ -179,7 +215,15 @@
|
|
|
179
215
|
"products": {
|
|
180
216
|
"backToExperiences": "Retour aux expériences",
|
|
181
217
|
"from": "À partir de",
|
|
182
|
-
"noDescription": "Aucune description disponible"
|
|
218
|
+
"noDescription": "Aucune description disponible",
|
|
219
|
+
"productGridFilters": {
|
|
220
|
+
"all": "Tous",
|
|
221
|
+
"moraineLake": "Lac Moraine",
|
|
222
|
+
"lakeLouise": "Lac Louise",
|
|
223
|
+
"emeraldLake": "Lac Émeraude",
|
|
224
|
+
"private": "Privé",
|
|
225
|
+
"sunrise": "Levé de soleil"
|
|
226
|
+
}
|
|
183
227
|
},
|
|
184
228
|
"terms": {
|
|
185
229
|
"title": "Conditions générales",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared itinerary step labels for manage booking, change-booking dialog, etc.
|
|
3
|
+
* Keeps display consistent with BookingDetails.
|
|
4
|
+
*/
|
|
5
|
+
export type ItineraryStepForLabel = {
|
|
6
|
+
label?: string | null;
|
|
7
|
+
place?: string | null;
|
|
8
|
+
stepType?: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function getItineraryStepLabel(step: ItineraryStepForLabel): string {
|
|
12
|
+
if (step.label?.trim()) return step.label.trim();
|
|
13
|
+
const place = step.place?.trim();
|
|
14
|
+
const placeDisplay =
|
|
15
|
+
place === 'your_pickup_location'
|
|
16
|
+
? 'your pickup location'
|
|
17
|
+
: place === 'the_destination'
|
|
18
|
+
? 'the destination'
|
|
19
|
+
: place ?? '';
|
|
20
|
+
switch (step.stepType) {
|
|
21
|
+
case 'pickup':
|
|
22
|
+
return placeDisplay ? `Pickup at ${placeDisplay}` : 'Pickup';
|
|
23
|
+
case 'drop_off':
|
|
24
|
+
return placeDisplay ? `Drop off at ${placeDisplay}` : 'Drop-off';
|
|
25
|
+
case 'arrive':
|
|
26
|
+
return placeDisplay ? `Arrive at ${placeDisplay}` : 'Arrive';
|
|
27
|
+
case 'depart':
|
|
28
|
+
return placeDisplay ? `Depart ${placeDisplay}` : 'Depart';
|
|
29
|
+
case 'trip_end':
|
|
30
|
+
return 'Trip ends';
|
|
31
|
+
case 'draft':
|
|
32
|
+
return placeDisplay || 'Stop';
|
|
33
|
+
default:
|
|
34
|
+
return placeDisplay || (step.stepType ?? 'Step');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* Location calculation utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { PickupLocation } from '
|
|
6
|
-
import type { Coordinates } from '
|
|
7
|
-
import type { NearbyLocation } from '
|
|
5
|
+
import type { PickupLocation } from '@/lib/booking-api';
|
|
6
|
+
import type { Coordinates } from '@/lib/booking/location-utils';
|
|
7
|
+
import type { NearbyLocation } from '@/lib/booking/pickup-location-types';
|
|
8
8
|
import {
|
|
9
9
|
calculateDistance,
|
|
10
10
|
estimateWalkingTime,
|
|
11
11
|
estimateDrivingTime,
|
|
12
|
-
} from '
|
|
12
|
+
} from '@/lib/booking/location-utils';
|
|
13
13
|
|
|
14
14
|
const EXACT_MATCH_THRESHOLD_KM = 0.05; // 50 meters - tighter threshold for exact matches
|
|
15
15
|
|
|
@@ -104,6 +104,32 @@ export function formatTime(minutes: number): string {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Service area for Private Shuttle custom pickups: Lake Louise to Kananaskis casino corridor.
|
|
109
|
+
* Bounding box encompasses: Lake Louise, Banff, Canmore, Kananaskis.
|
|
110
|
+
* Excludes Calgary and areas east.
|
|
111
|
+
*/
|
|
112
|
+
export const PRIVATE_SHUTTLE_SERVICE_BOUNDS = {
|
|
113
|
+
latMin: 51.0,
|
|
114
|
+
latMax: 51.5,
|
|
115
|
+
lngMin: -116.5,
|
|
116
|
+
lngMax: -114.8,
|
|
117
|
+
} as const;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if coordinates are within the Private Shuttle service area
|
|
121
|
+
* (corridor from Lake Louise to Kananaskis casino).
|
|
122
|
+
*/
|
|
123
|
+
export function isWithinPrivateShuttleServiceArea(coords: Coordinates): boolean {
|
|
124
|
+
const { latMin, latMax, lngMin, lngMax } = PRIVATE_SHUTTLE_SERVICE_BOUNDS;
|
|
125
|
+
return (
|
|
126
|
+
coords.lat >= latMin &&
|
|
127
|
+
coords.lat <= latMax &&
|
|
128
|
+
coords.lng >= lngMin &&
|
|
129
|
+
coords.lng <= lngMax
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
/**
|
|
108
134
|
* Geocode an address using Google Maps Geocoding API
|
|
109
135
|
* Note: This requires a Google Maps API key
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Google Maps utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { Coordinates } from '
|
|
6
|
-
import type { PickupLocation } from '
|
|
7
|
-
import type { NearbyLocation } from '
|
|
5
|
+
import type { Coordinates } from '@/lib/booking/location-utils';
|
|
6
|
+
import type { PickupLocation } from '@/lib/booking-api';
|
|
7
|
+
import type { NearbyLocation } from '@/lib/booking/pickup-location-types';
|
|
8
8
|
|
|
9
9
|
export interface MapCenter {
|
|
10
10
|
lat: number;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize IDs used in TicketBooth query params (product id, product option id).
|
|
3
|
+
* Strips hash fragments, accidental query strings, or concatenated `&…` tails — aligned with
|
|
4
|
+
* defensive parsing in GetAvailabilitiesHandler.
|
|
5
|
+
*/
|
|
1
6
|
export function normalizeBookingProductId(rawId: string): string {
|
|
2
7
|
const trimmed = rawId.trim();
|
|
3
8
|
if (!trimmed) return '';
|
|
@@ -7,8 +12,10 @@ export function normalizeBookingProductId(rawId: string): string {
|
|
|
7
12
|
return normalized.trim();
|
|
8
13
|
}
|
|
9
14
|
|
|
15
|
+
/** True when the raw value clearly contains URL/query junk and should be normalized before use. */
|
|
10
16
|
export function isSuspiciousBookingProductId(value: string): boolean {
|
|
11
17
|
return /[?&=]/.test(value);
|
|
12
18
|
}
|
|
13
19
|
|
|
20
|
+
/** Alias: same normalization for get-availabilities lookup id (product or option). */
|
|
14
21
|
export const normalizeAvailabilityLookupId = normalizeBookingProductId;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Types for pickup location selector
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { PickupLocation } from '
|
|
6
|
-
import type { Coordinates } from '
|
|
5
|
+
import type { PickupLocation } from '@/lib/booking-api';
|
|
6
|
+
import type { Coordinates } from '@/lib/booking/location-utils';
|
|
7
7
|
|
|
8
8
|
export interface NearbyLocation extends PickupLocation {
|
|
9
9
|
distance: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Currency } from '@/components/CurrencySwitcher';
|
|
2
|
-
import type { PricingConfig } from '@/lib/api';
|
|
1
|
+
import type { Currency } from '@/components/booking/CurrencySwitcher';
|
|
2
|
+
import type { PricingConfig } from '@/lib/booking-api';
|
|
3
3
|
import { DEFAULT_EXCHANGE_RATES } from '@/lib/currency';
|
|
4
4
|
|
|
5
5
|
const BASE_CURRENCY = 'CAD' as const;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TicketBooth / GYG-style ids: bookings often persist parent product_id (`p_…`) while
|
|
3
|
+
* availabilities and change sessions use option ids (`po_…`). Match backend semantics:
|
|
4
|
+
* never use a parent id where an option id is required (e.g. change-booking preselection).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function isParentProductId(id: string | null | undefined): boolean {
|
|
8
|
+
const t = id?.trim();
|
|
9
|
+
if (!t) return false;
|
|
10
|
+
return t.startsWith('p_') && !t.startsWith('po_');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Returns the id if it is usable as a product option id for matching availabilities; otherwise null. */
|
|
14
|
+
export function normalizeProductOptionIdForChangeFlow(
|
|
15
|
+
id: string | null | undefined
|
|
16
|
+
): string | null {
|
|
17
|
+
const t = id?.trim() || null;
|
|
18
|
+
if (!t || isParentProductId(t)) return null;
|
|
19
|
+
return t;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Option id to seed change-booking UI: prefer explicit productOptionId, else productId when it is
|
|
24
|
+
* already an option id (e.g. some payloads).
|
|
25
|
+
*/
|
|
26
|
+
export function effectiveProductOptionIdForChangeFlow(booking: {
|
|
27
|
+
productId?: string | null;
|
|
28
|
+
productOptionId?: string | null;
|
|
29
|
+
}): string | null {
|
|
30
|
+
const fromOption = normalizeProductOptionIdForChangeFlow(booking.productOptionId);
|
|
31
|
+
if (fromOption) return fromOption;
|
|
32
|
+
const pid = booking.productId?.trim() || null;
|
|
33
|
+
if (pid?.startsWith('po_')) return pid;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -4,9 +4,9 @@ export {
|
|
|
4
4
|
PARTNER_PORTAL_BOOKING_SOURCE,
|
|
5
5
|
inferClientBookingSourceFromProductIds,
|
|
6
6
|
mergedMetadataImpliesPartnerPortal,
|
|
7
|
-
} from '
|
|
7
|
+
} from '@/lib/booking/booking-source';
|
|
8
8
|
|
|
9
|
-
import { KnownBookingSource, mergedMetadataImpliesPartnerPortal } from '
|
|
9
|
+
import { KnownBookingSource, mergedMetadataImpliesPartnerPortal } from '@/lib/booking/booking-source';
|
|
10
10
|
|
|
11
11
|
export interface BookingSourceMetadata {
|
|
12
12
|
pageUrl?: string;
|
|
@@ -33,17 +33,37 @@ function withDefinedValues<T extends object>(obj: T): Partial<T> {
|
|
|
33
33
|
) as Partial<T>;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Public marketing embed routes on the main site (`/partner/{slug}`). Used to decide when
|
|
38
|
+
* `partnerId` in the query string is intentional vs noisy on generic tour/product URLs.
|
|
39
|
+
*/
|
|
36
40
|
export function isPublicPartnerMarketingPath(pathname: string): boolean {
|
|
37
41
|
return /^\/partner\/[^/]+/i.test(pathname.trim());
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
/** Partner booking SPA (`booking.*` prod/staging) — `?partnerId=` is intentional on `/` or any path. */
|
|
40
45
|
export function isDedicatedPartnerBookingPortalHost(hostname: string): boolean {
|
|
41
46
|
const h = hostname.trim().toLowerCase();
|
|
42
47
|
return h === 'booking.viaviamorainelake.com' || h === 'staging.booking.viaviamorainelake.com';
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Reads the **current browser URL** (and referrer) into attribution fields. Supplements
|
|
52
|
+
* `mergeBookingSourceMetadata` with `canonicalAttribution` from the route so identity does
|
|
53
|
+
* not depend on the visible URL alone.
|
|
54
|
+
*
|
|
55
|
+
* **`partnerId` from `?partnerId=`** is included when:
|
|
56
|
+
* - the path is **`/partner/:slug`** on the **main marketing site**, or
|
|
57
|
+
* - the host is the **dedicated partner booking app** (`booking.*` prod/staging).
|
|
58
|
+
*
|
|
59
|
+
* On other generic marketing pages we still capture UTMs and full `pageUrl`, but omit loose
|
|
60
|
+
* `partnerId` query params so promo + backend rules aren’t overshadowed by “explicit” metadata.
|
|
61
|
+
*/
|
|
45
62
|
export function buildBookingSourceMetadataFromLocation(): Partial<BookingSourceMetadata> {
|
|
46
|
-
if (typeof window === 'undefined')
|
|
63
|
+
if (typeof window === 'undefined') {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
const currentUrl = new URL(window.location.href);
|
|
48
68
|
const params = currentUrl.searchParams;
|
|
49
69
|
const partnerMatch = currentUrl.pathname.match(/^\/partner\/([^/]+)/i);
|
|
@@ -75,6 +95,7 @@ export function buildBookingSourceMetadataFromLocation(): Partial<BookingSourceM
|
|
|
75
95
|
});
|
|
76
96
|
}
|
|
77
97
|
|
|
98
|
+
/** Later layers override earlier ones (undefined / null / empty string in a layer are omitted). */
|
|
78
99
|
export function mergeBookingSourceMetadata(
|
|
79
100
|
...layers: Array<Partial<BookingSourceMetadata> | null | undefined>
|
|
80
101
|
): Partial<BookingSourceMetadata> {
|
|
@@ -104,11 +125,22 @@ function hostnameForSourceDecision(merged: Partial<BookingSourceMetadata>): stri
|
|
|
104
125
|
}
|
|
105
126
|
|
|
106
127
|
export type BuildBookingSourceContextOptions = {
|
|
128
|
+
/**
|
|
129
|
+
* When metadata does not imply partner portal, use this channel (GYG/VIATOR/WEBSITE from product ids).
|
|
130
|
+
* AFFILIATE/DASHBOARD are normally set server-side from request headers.
|
|
131
|
+
*/
|
|
107
132
|
clientChannelSource?: KnownBookingSource;
|
|
133
|
+
/** Dedicated partner booking app (`booking.*`): persist `source` as PARTNER_PORTAL (not PUBLIC_PARTNER_WEBSITE). */
|
|
108
134
|
forcePartnerPortalChannel?: boolean;
|
|
135
|
+
/** TicketBooth provider dashboard embed (`BookingAppProvider` mode `provider-dashboard`): persist `source` as DASHBOARD. */
|
|
109
136
|
forceDashboardSource?: boolean;
|
|
110
137
|
};
|
|
111
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Builds reserve/checkout `source` + `sourceMetadata` from composed metadata. Partner portal is
|
|
141
|
+
* inferred from `par_` org id or a valid marketing `partnerSlug` (including route-level canonical),
|
|
142
|
+
* not from hostname alone. Optional `clientChannelSource` supplies GYG/VIATOR/WEBSITE otherwise.
|
|
143
|
+
*/
|
|
112
144
|
export function buildBookingSourceContext(
|
|
113
145
|
mergedMetadata: Partial<BookingSourceMetadata>,
|
|
114
146
|
options?: BuildBookingSourceContextOptions,
|
|
@@ -122,7 +154,11 @@ export function buildBookingSourceContext(
|
|
|
122
154
|
|
|
123
155
|
if (options?.forceDashboardSource === true) {
|
|
124
156
|
const hasMeta = Object.keys(merged).length > 0;
|
|
125
|
-
|
|
157
|
+
const context: {
|
|
158
|
+
source: KnownBookingSource;
|
|
159
|
+
sourceMetadata?: BookingSourceMetadata;
|
|
160
|
+
source_metadata?: BookingSourceMetadata;
|
|
161
|
+
} = {
|
|
126
162
|
source: KnownBookingSource.DASHBOARD,
|
|
127
163
|
...(hasMeta
|
|
128
164
|
? {
|
|
@@ -131,13 +167,26 @@ export function buildBookingSourceContext(
|
|
|
131
167
|
}
|
|
132
168
|
: {}),
|
|
133
169
|
};
|
|
170
|
+
try {
|
|
171
|
+
const md = context.sourceMetadata ?? context.source_metadata;
|
|
172
|
+
console.info(
|
|
173
|
+
`[booking-source-debug] built-context summary source=${context.source} path=${md?.pagePath ?? 'n/a'} partner=${md?.partnerId ?? 'n/a'} partner_slug=${md?.partnerSlug ?? 'n/a'} utm_source=${md?.utmSource ?? 'n/a'} utm_medium=${md?.utmMedium ?? 'n/a'} utm_campaign=${md?.utmCampaign ?? 'n/a'}`,
|
|
174
|
+
);
|
|
175
|
+
console.info(`[booking-source-debug] built-context payload\n${JSON.stringify(context, null, 2)}`);
|
|
176
|
+
} catch {
|
|
177
|
+
console.info('[booking-source-debug] built-context', context);
|
|
178
|
+
}
|
|
179
|
+
return context;
|
|
134
180
|
}
|
|
135
181
|
|
|
136
|
-
if (Object.keys(merged).length === 0)
|
|
182
|
+
if (Object.keys(merged).length === 0) {
|
|
183
|
+
return { source: channel };
|
|
184
|
+
}
|
|
137
185
|
|
|
138
186
|
const host = hostnameForSourceDecision(merged);
|
|
139
187
|
const dedicatedPartnerBookingHost =
|
|
140
|
-
host === 'booking.viaviamorainelake.com' ||
|
|
188
|
+
host === 'booking.viaviamorainelake.com' ||
|
|
189
|
+
host === 'staging.booking.viaviamorainelake.com';
|
|
141
190
|
const pid = typeof merged.partnerId === 'string' ? merged.partnerId.trim() : '';
|
|
142
191
|
const pslug = typeof merged.partnerSlug === 'string' ? merged.partnerSlug.trim() : '';
|
|
143
192
|
|
|
@@ -153,9 +202,25 @@ export function buildBookingSourceContext(
|
|
|
153
202
|
? KnownBookingSource.PUBLIC_PARTNER_WEBSITE
|
|
154
203
|
: channel;
|
|
155
204
|
|
|
156
|
-
|
|
205
|
+
const context: {
|
|
206
|
+
source: KnownBookingSource;
|
|
207
|
+
sourceMetadata?: BookingSourceMetadata;
|
|
208
|
+
source_metadata?: BookingSourceMetadata;
|
|
209
|
+
} = {
|
|
157
210
|
source,
|
|
158
211
|
sourceMetadata: merged as BookingSourceMetadata,
|
|
159
212
|
source_metadata: merged as BookingSourceMetadata,
|
|
160
213
|
};
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const md = context.sourceMetadata ?? context.source_metadata;
|
|
217
|
+
console.info(
|
|
218
|
+
`[booking-source-debug] built-context summary source=${context.source} path=${md?.pagePath ?? 'n/a'} partner=${md?.partnerId ?? 'n/a'} partner_slug=${md?.partnerSlug ?? 'n/a'} utm_source=${md?.utmSource ?? 'n/a'} utm_medium=${md?.utmMedium ?? 'n/a'} utm_campaign=${md?.utmCampaign ?? 'n/a'}`,
|
|
219
|
+
);
|
|
220
|
+
console.info(`[booking-source-debug] built-context payload\n${JSON.stringify(context, null, 2)}`);
|
|
221
|
+
} catch {
|
|
222
|
+
console.info('[booking-source-debug] built-context', context);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return context;
|
|
161
226
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { addDays, parseISO } from 'date-fns';
|
|
2
|
+
import { formatInTimeZone, fromZonedTime } from 'date-fns-tz';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sunday (week start) of the week containing `dateStr` (yyyy-MM-dd), in `timezone`.
|
|
6
|
+
* Matches the booking calendar grid so fetch windows align with the visible week.
|
|
7
|
+
*/
|
|
8
|
+
export function getSundayOfWeek(dateStr: string, timezone: string): string {
|
|
9
|
+
const noonInTz = fromZonedTime(parseISO(`${dateStr}T12:00:00`), timezone);
|
|
10
|
+
const isoDay = parseInt(formatInTimeZone(noonInTz, timezone, 'i'), 10); // 1=Mon, 7=Sun
|
|
11
|
+
const daysToSubtract = isoDay === 7 ? 0 : isoDay;
|
|
12
|
+
const sundayDate = addDays(noonInTz, -daysToSubtract);
|
|
13
|
+
return formatInTimeZone(sundayDate, timezone, 'yyyy-MM-dd');
|
|
14
|
+
}
|