@ticketboothapp/booking 0.1.11 → 0.1.13
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 +2 -1
- package/src/app/photo-sessions/photo-packages.ts +75 -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 +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 +28 -36
- 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
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef, type ReactNode } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
4
5
|
import { loadStripe } from '@stripe/stripe-js';
|
|
5
6
|
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
|
6
7
|
import { ENV } from '@/lib/env';
|
|
8
|
+
import { storePendingPurchase, trackBeginCheckout } from '@/lib/analytics';
|
|
7
9
|
import { formatCurrencyAmount } from '@/lib/currency';
|
|
8
10
|
import { PriceSummary, type PriceSummaryLine } from './PriceSummary';
|
|
9
11
|
import type { Currency } from './CurrencySwitcher';
|
|
10
|
-
import type { Locale } from '@/lib/i18n/config';
|
|
11
|
-
import type { OrderSummaryTicketLine, OrderSummaryFeeLine, PriceBreakdown as PriceBreakdownType } from '@/lib/pricing';
|
|
12
|
+
import type { Locale } from '@/lib/booking/i18n/config';
|
|
13
|
+
import type { OrderSummaryTicketLine, OrderSummaryFeeLine, PriceBreakdown as PriceBreakdownType } from '@/lib/booking/pricing';
|
|
12
14
|
|
|
13
15
|
export interface CheckoutModalLineItem {
|
|
14
16
|
line: OrderSummaryTicketLine;
|
|
@@ -18,8 +20,11 @@ export interface CheckoutModalLineItem {
|
|
|
18
20
|
export interface CheckoutModalProps {
|
|
19
21
|
open: boolean;
|
|
20
22
|
onClose: () => void;
|
|
23
|
+
onPaymentSubmitStart?: () => void;
|
|
24
|
+
onPaymentSubmitError?: () => void;
|
|
21
25
|
clientSecret: string;
|
|
22
26
|
reservationReference: string;
|
|
27
|
+
reservationExpiration?: string;
|
|
23
28
|
/** Last name for Stripe return_url so success page can poll and redirect to /manage */
|
|
24
29
|
customerLastName?: string;
|
|
25
30
|
/** When set (e.g. provider-dashboard), used as Stripe return_url instead of /manage */
|
|
@@ -44,6 +49,16 @@ export interface CheckoutModalProps {
|
|
|
44
49
|
isDepositPayment?: boolean;
|
|
45
50
|
/** Days before booking to charge balance (e.g. 7). */
|
|
46
51
|
balanceChargeDaysBefore?: number;
|
|
52
|
+
/**
|
|
53
|
+
* When `clientSecret` is not yet available, show this instead of the loading placeholder
|
|
54
|
+
* (e.g. collect email / last name before creating a PaymentIntent).
|
|
55
|
+
*/
|
|
56
|
+
prePaymentPanel?: ReactNode;
|
|
57
|
+
changeTotals?: {
|
|
58
|
+
previousTotal: number;
|
|
59
|
+
newTotal: number;
|
|
60
|
+
differenceTotal: number;
|
|
61
|
+
};
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
const stripePromise = ENV.STRIPE_PUBLISHABLE_KEY
|
|
@@ -53,6 +68,8 @@ const stripePromise = ENV.STRIPE_PUBLISHABLE_KEY
|
|
|
53
68
|
function CheckoutForm({
|
|
54
69
|
successUrl,
|
|
55
70
|
onClose,
|
|
71
|
+
onPaymentSubmitStart,
|
|
72
|
+
onPaymentSubmitError,
|
|
56
73
|
t,
|
|
57
74
|
total,
|
|
58
75
|
currency,
|
|
@@ -60,6 +77,8 @@ function CheckoutForm({
|
|
|
60
77
|
}: {
|
|
61
78
|
successUrl: string;
|
|
62
79
|
onClose: () => void;
|
|
80
|
+
onPaymentSubmitStart?: () => void;
|
|
81
|
+
onPaymentSubmitError?: () => void;
|
|
63
82
|
t: (key: string) => string;
|
|
64
83
|
total: number;
|
|
65
84
|
currency: Currency;
|
|
@@ -79,8 +98,12 @@ function CheckoutForm({
|
|
|
79
98
|
if (submitError) {
|
|
80
99
|
setError(submitError.message ?? 'Validation failed');
|
|
81
100
|
setLoading(false);
|
|
101
|
+
onPaymentSubmitError?.();
|
|
82
102
|
return;
|
|
83
103
|
}
|
|
104
|
+
onPaymentSubmitStart?.();
|
|
105
|
+
// Store before redirect so success page can fire purchase event
|
|
106
|
+
storePendingPurchase(total, currency);
|
|
84
107
|
const { error: confirmError } = await stripe.confirmPayment({
|
|
85
108
|
elements,
|
|
86
109
|
confirmParams: {
|
|
@@ -89,6 +112,7 @@ function CheckoutForm({
|
|
|
89
112
|
});
|
|
90
113
|
if (confirmError) {
|
|
91
114
|
setError(confirmError.message ?? 'Payment failed');
|
|
115
|
+
onPaymentSubmitError?.();
|
|
92
116
|
}
|
|
93
117
|
setLoading(false);
|
|
94
118
|
};
|
|
@@ -124,8 +148,11 @@ function CheckoutForm({
|
|
|
124
148
|
export function CheckoutModal({
|
|
125
149
|
open,
|
|
126
150
|
onClose,
|
|
151
|
+
onPaymentSubmitStart,
|
|
152
|
+
onPaymentSubmitError,
|
|
127
153
|
clientSecret,
|
|
128
154
|
reservationReference,
|
|
155
|
+
reservationExpiration,
|
|
129
156
|
customerLastName,
|
|
130
157
|
successUrlOverride,
|
|
131
158
|
ticketLines,
|
|
@@ -146,17 +173,72 @@ export function CheckoutModal({
|
|
|
146
173
|
t,
|
|
147
174
|
isDepositPayment = false,
|
|
148
175
|
balanceChargeDaysBefore = 7,
|
|
176
|
+
prePaymentPanel,
|
|
177
|
+
changeTotals,
|
|
149
178
|
}: CheckoutModalProps) {
|
|
150
179
|
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
|
|
151
180
|
const manageParams = new URLSearchParams({ reservationRef: reservationReference });
|
|
152
181
|
if (customerLastName?.trim()) manageParams.set('lastName', customerLastName.trim());
|
|
153
|
-
|
|
182
|
+
if (!successUrlOverride) manageParams.set('booking_complete', '1');
|
|
183
|
+
const successUrl = successUrlOverride ?? `${baseUrl}/manage-booking?${manageParams.toString()}`;
|
|
184
|
+
|
|
185
|
+
const hasFiredBeginCheckout = useRef(false);
|
|
186
|
+
/** Ensures we only fire expiry close once per modal open (avoids Strict Mode / repeated 0 remainingMs). */
|
|
187
|
+
const holdExpiryCloseSentRef = useRef(false);
|
|
188
|
+
const [nowMs, setNowMs] = useState(() => Date.now());
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (open && !hasFiredBeginCheckout.current) {
|
|
191
|
+
hasFiredBeginCheckout.current = true;
|
|
192
|
+
const items = ticketLines.map(({ line }) => ({
|
|
193
|
+
id: line.category,
|
|
194
|
+
name: line.category,
|
|
195
|
+
qty: line.qty,
|
|
196
|
+
price: line.itemTotal,
|
|
197
|
+
}));
|
|
198
|
+
trackBeginCheckout(total, currency, items);
|
|
199
|
+
}
|
|
200
|
+
if (!open) hasFiredBeginCheckout.current = false;
|
|
201
|
+
}, [open, total, currency, ticketLines]);
|
|
202
|
+
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (!open || !reservationExpiration) return;
|
|
205
|
+
const timer = window.setInterval(() => setNowMs(Date.now()), 1000);
|
|
206
|
+
return () => window.clearInterval(timer);
|
|
207
|
+
}, [open, reservationExpiration]);
|
|
208
|
+
|
|
209
|
+
const expirationMs = reservationExpiration ? Date.parse(reservationExpiration) : NaN;
|
|
210
|
+
const hasValidExpiration = Number.isFinite(expirationMs);
|
|
211
|
+
const remainingMs = hasValidExpiration ? Math.max(0, expirationMs - nowMs) : 0;
|
|
212
|
+
const remainingTotalSeconds = Math.ceil(remainingMs / 1000);
|
|
213
|
+
const remainingMinutes = Math.floor(remainingTotalSeconds / 60);
|
|
214
|
+
const remainingSeconds = remainingTotalSeconds % 60;
|
|
215
|
+
const remainingDisplay = `${String(remainingMinutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
|
|
216
|
+
const holdTone =
|
|
217
|
+
remainingTotalSeconds <= 120
|
|
218
|
+
? 'critical'
|
|
219
|
+
: remainingTotalSeconds <= 300
|
|
220
|
+
? 'warning'
|
|
221
|
+
: 'normal';
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (!open) {
|
|
225
|
+
holdExpiryCloseSentRef.current = false;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!hasValidExpiration || remainingMs > 0) return;
|
|
229
|
+
if (holdExpiryCloseSentRef.current) return;
|
|
230
|
+
holdExpiryCloseSentRef.current = true;
|
|
231
|
+
onClose();
|
|
232
|
+
}, [open, hasValidExpiration, remainingMs, onClose]);
|
|
154
233
|
|
|
155
234
|
if (!open) return null;
|
|
156
235
|
|
|
157
236
|
if (!ENV.STRIPE_PUBLISHABLE_KEY) {
|
|
158
|
-
|
|
159
|
-
<div
|
|
237
|
+
const noStripe = (
|
|
238
|
+
<div
|
|
239
|
+
className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50"
|
|
240
|
+
style={{ zIndex: 100_000 }}
|
|
241
|
+
>
|
|
160
242
|
<div className="bg-white rounded-xl shadow-xl max-w-md w-full p-6">
|
|
161
243
|
<h3 className="text-lg font-semibold text-stone-900 mb-2">
|
|
162
244
|
{t('booking.checkout') || 'Checkout'}
|
|
@@ -174,6 +256,7 @@ export function CheckoutModal({
|
|
|
174
256
|
</div>
|
|
175
257
|
</div>
|
|
176
258
|
);
|
|
259
|
+
return typeof document !== 'undefined' ? createPortal(noStripe, document.body) : null;
|
|
177
260
|
}
|
|
178
261
|
|
|
179
262
|
const options = {
|
|
@@ -187,8 +270,11 @@ export function CheckoutModal({
|
|
|
187
270
|
},
|
|
188
271
|
};
|
|
189
272
|
|
|
190
|
-
|
|
191
|
-
<div
|
|
273
|
+
const checkout = (
|
|
274
|
+
<div
|
|
275
|
+
className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50"
|
|
276
|
+
style={{ zIndex: 100_000 }}
|
|
277
|
+
>
|
|
192
278
|
<div className="bg-white rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-hidden flex flex-col">
|
|
193
279
|
<div className="p-6 border-b border-stone-200 flex-shrink-0">
|
|
194
280
|
<div className="flex justify-between items-start">
|
|
@@ -238,12 +324,18 @@ export function CheckoutModal({
|
|
|
238
324
|
},
|
|
239
325
|
]
|
|
240
326
|
: []),
|
|
241
|
-
...feeLineItems.map((fee) =>
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
327
|
+
...feeLineItems.map((fee) => {
|
|
328
|
+
const isMoraineLakeRoadAccessFee = fee.name.toLowerCase().includes('moraine') && (fee.name.toLowerCase().includes('access') || fee.name.toLowerCase().includes('road') || fee.name.toLowerCase().includes('license'));
|
|
329
|
+
return {
|
|
330
|
+
kind: 'line' as const,
|
|
331
|
+
label: `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`,
|
|
332
|
+
amount: fee.totalAmount,
|
|
333
|
+
type: 'fee',
|
|
334
|
+
tooltip: isMoraineLakeRoadAccessFee
|
|
335
|
+
? "Since 2025, Parks Canada charges a per-trip fee for License of Occupation. Based on our capacity, this per-person fee contributes towards Parks Canada's Moraine Lake Road operations."
|
|
336
|
+
: undefined,
|
|
337
|
+
};
|
|
338
|
+
}),
|
|
247
339
|
]}
|
|
248
340
|
total={total}
|
|
249
341
|
currency={currency}
|
|
@@ -255,18 +347,37 @@ export function CheckoutModal({
|
|
|
255
347
|
taxRate={taxRate}
|
|
256
348
|
size="sm"
|
|
257
349
|
t={t}
|
|
258
|
-
depositMode={isDepositPayment && subtotal > total ? { totalLabel: t('booking.deposit') || 'Deposit', balanceAmount: subtotal - total } : undefined}
|
|
350
|
+
depositMode={isDepositPayment && subtotal > total ? { totalLabel: t('booking.deposit') || 'Deposit', balanceAmount: subtotal - total, fullTotalAmount: subtotal } : undefined}
|
|
259
351
|
hideSubtotal={isDepositPayment}
|
|
352
|
+
extraBetweenTaxAndTotal={
|
|
353
|
+
changeTotals ? (
|
|
354
|
+
<div className="space-y-1 pt-2 border-t border-stone-200">
|
|
355
|
+
<div className="flex justify-between gap-3 min-w-0 text-sm">
|
|
356
|
+
<span className="text-stone-600 min-w-0 truncate">New Total</span>
|
|
357
|
+
<span className="flex-shrink-0 whitespace-nowrap font-medium text-stone-700">
|
|
358
|
+
{formatCurrencyAmount(changeTotals.newTotal, currency, locale)}
|
|
359
|
+
</span>
|
|
360
|
+
</div>
|
|
361
|
+
<div className="flex justify-between gap-3 min-w-0 text-sm">
|
|
362
|
+
<span className="text-stone-600 min-w-0 truncate">Previous Total</span>
|
|
363
|
+
<span className="flex-shrink-0 whitespace-nowrap font-medium text-stone-700">
|
|
364
|
+
{formatCurrencyAmount(changeTotals.previousTotal, currency, locale)}
|
|
365
|
+
</span>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
) : undefined
|
|
369
|
+
}
|
|
370
|
+
totalLabel={changeTotals ? 'New Booking Difference' : undefined}
|
|
260
371
|
/>
|
|
261
372
|
</div>
|
|
262
373
|
|
|
263
374
|
{/* Deposit payment notice */}
|
|
264
375
|
{isDepositPayment && (
|
|
265
|
-
<div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg text-
|
|
266
|
-
<p className="font-medium">
|
|
376
|
+
<div className="deposit-notice mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg text-amber-800">
|
|
377
|
+
<p className="text-xs font-medium">
|
|
267
378
|
{(t('booking.depositPaymentNotice') !== 'booking.depositPaymentNotice' ? t('booking.depositPaymentNotice') : null) ?? "You're paying the deposit today."}
|
|
268
379
|
</p>
|
|
269
|
-
<p className="mt-1 text-amber-700">
|
|
380
|
+
<p className="mt-1 text-xs text-amber-700/90">
|
|
270
381
|
{balanceChargeDaysBefore > 0
|
|
271
382
|
? (t('booking.balanceChargeNotice', { days: balanceChargeDaysBefore }) !== 'booking.balanceChargeNotice'
|
|
272
383
|
? t('booking.balanceChargeNotice', { days: balanceChargeDaysBefore })
|
|
@@ -277,13 +388,58 @@ export function CheckoutModal({
|
|
|
277
388
|
</p>
|
|
278
389
|
</div>
|
|
279
390
|
)}
|
|
391
|
+
{hasValidExpiration && (
|
|
392
|
+
<div
|
|
393
|
+
className={`mt-4 p-3 rounded-lg border ${
|
|
394
|
+
holdTone === 'critical'
|
|
395
|
+
? 'bg-red-50 border-red-300'
|
|
396
|
+
: holdTone === 'warning'
|
|
397
|
+
? 'bg-amber-50 border-amber-300'
|
|
398
|
+
: 'bg-emerald-50 border-emerald-200'
|
|
399
|
+
}`}
|
|
400
|
+
>
|
|
401
|
+
<p
|
|
402
|
+
className={`text-xs font-semibold ${
|
|
403
|
+
holdTone === 'critical'
|
|
404
|
+
? 'text-red-900'
|
|
405
|
+
: holdTone === 'warning'
|
|
406
|
+
? 'text-amber-900'
|
|
407
|
+
: 'text-emerald-900'
|
|
408
|
+
}`}
|
|
409
|
+
>
|
|
410
|
+
Your reservation is being held for {remainingDisplay}
|
|
411
|
+
</p>
|
|
412
|
+
<p
|
|
413
|
+
className={`mt-1 text-xs ${
|
|
414
|
+
holdTone === 'critical'
|
|
415
|
+
? 'text-red-800'
|
|
416
|
+
: holdTone === 'warning'
|
|
417
|
+
? 'text-amber-800'
|
|
418
|
+
: 'text-emerald-800'
|
|
419
|
+
}`}
|
|
420
|
+
>
|
|
421
|
+
Closing this payment dialog releases the hold and you will need to reserve again.
|
|
422
|
+
</p>
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
280
425
|
</div>
|
|
281
426
|
|
|
282
427
|
<div className="p-6 overflow-y-auto flex-1">
|
|
283
428
|
{stripePromise && clientSecret ? (
|
|
284
429
|
<Elements stripe={stripePromise} options={options}>
|
|
285
|
-
<CheckoutForm
|
|
430
|
+
<CheckoutForm
|
|
431
|
+
successUrl={successUrl}
|
|
432
|
+
onClose={onClose}
|
|
433
|
+
onPaymentSubmitStart={onPaymentSubmitStart}
|
|
434
|
+
onPaymentSubmitError={onPaymentSubmitError}
|
|
435
|
+
t={t}
|
|
436
|
+
total={total}
|
|
437
|
+
currency={currency}
|
|
438
|
+
locale={locale}
|
|
439
|
+
/>
|
|
286
440
|
</Elements>
|
|
441
|
+
) : prePaymentPanel ? (
|
|
442
|
+
prePaymentPanel
|
|
287
443
|
) : (
|
|
288
444
|
<p className="text-stone-500">{t('booking.loadingPayment') || 'Loading payment form...'}</p>
|
|
289
445
|
)}
|
|
@@ -291,4 +447,5 @@ export function CheckoutModal({
|
|
|
291
447
|
</div>
|
|
292
448
|
</div>
|
|
293
449
|
);
|
|
450
|
+
return typeof document !== 'undefined' ? createPortal(checkout, document.body) : null;
|
|
294
451
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import ViaViaImage from '@/components/image';
|
|
5
|
+
import ImageModal from '@/components/product-theme-pages/image-modal';
|
|
6
|
+
import styles from './BookingFlowCollage.module.css';
|
|
7
|
+
|
|
8
|
+
export interface DapFlowCollageProps {
|
|
9
|
+
/** Bunny CDN image IDs — first is the tall hero, next four fill the asymmetric grid */
|
|
10
|
+
imageIds: string[];
|
|
11
|
+
altPrefix?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Hero (left) + four-tile grid (right), same layout as BookingFlowCollage without video */
|
|
15
|
+
export function DapFlowCollage({ imageIds, altPrefix = 'Experience' }: DapFlowCollageProps) {
|
|
16
|
+
const slots = useMemo(() => {
|
|
17
|
+
const raw = imageIds.map((id) => id.trim()).filter(Boolean);
|
|
18
|
+
if (raw.length === 0) return [];
|
|
19
|
+
const out = [...raw];
|
|
20
|
+
while (out.length < 5) {
|
|
21
|
+
out.push(out[0]!);
|
|
22
|
+
}
|
|
23
|
+
return out.slice(0, 5);
|
|
24
|
+
}, [imageIds]);
|
|
25
|
+
|
|
26
|
+
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
|
27
|
+
|
|
28
|
+
const imageItems = useMemo(
|
|
29
|
+
() => slots.map((id, i) => ({ id, alt: `${altPrefix} - ${i + 1}` })),
|
|
30
|
+
[slots, altPrefix]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (slots.length === 0) return null;
|
|
34
|
+
|
|
35
|
+
const heroId = slots[0]!;
|
|
36
|
+
const gridIds = slots.slice(1, 5);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={styles.collage}>
|
|
40
|
+
<div className={styles.videoSlot}>
|
|
41
|
+
<div className={styles.videoWrapper}>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
className={styles.gridCell}
|
|
45
|
+
onClick={() => setSelectedIndex(0)}
|
|
46
|
+
aria-label="View featured image"
|
|
47
|
+
>
|
|
48
|
+
<ViaViaImage imageId={heroId} alt={`${altPrefix} - featured`} context="GALLERY" />
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div className={styles.imageGrid}>
|
|
53
|
+
{gridIds.map((id, i) => (
|
|
54
|
+
<button
|
|
55
|
+
key={`${id}-${i}`}
|
|
56
|
+
type="button"
|
|
57
|
+
className={styles.gridCell}
|
|
58
|
+
onClick={() => setSelectedIndex(i + 1)}
|
|
59
|
+
aria-label={`View image ${i + 2}`}
|
|
60
|
+
>
|
|
61
|
+
<ViaViaImage imageId={id} alt={`${altPrefix} - ${i + 2}`} context="GALLERY" />
|
|
62
|
+
</button>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{selectedIndex !== null && imageItems[selectedIndex] && (
|
|
67
|
+
<ImageModal
|
|
68
|
+
selectedImage={imageItems[selectedIndex]!}
|
|
69
|
+
currentIndex={selectedIndex}
|
|
70
|
+
totalImages={imageItems.length}
|
|
71
|
+
images={imageItems}
|
|
72
|
+
onClose={() => setSelectedIndex(null)}
|
|
73
|
+
onNext={() => {
|
|
74
|
+
if (selectedIndex < imageItems.length - 1) {
|
|
75
|
+
setSelectedIndex(selectedIndex + 1);
|
|
76
|
+
}
|
|
77
|
+
}}
|
|
78
|
+
onPrevious={() => {
|
|
79
|
+
if (selectedIndex > 0) {
|
|
80
|
+
setSelectedIndex(selectedIndex - 1);
|
|
81
|
+
}
|
|
82
|
+
}}
|
|
83
|
+
overlayZIndex={10000}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { TourDescription } from '@/components/booking/TourDescription';
|
|
5
|
+
import { useLocale, useTranslations } from '@/lib/booking/i18n';
|
|
6
|
+
import { getDapDescription } from '@/lib/dap-descriptions';
|
|
7
|
+
import type { PhotoDapSlug } from '@/lib/photo-dap-config';
|
|
8
|
+
|
|
9
|
+
export function DapTourDescription({
|
|
10
|
+
slug,
|
|
11
|
+
locale: localeProp,
|
|
12
|
+
}: {
|
|
13
|
+
slug: PhotoDapSlug;
|
|
14
|
+
locale?: string;
|
|
15
|
+
}) {
|
|
16
|
+
const { t } = useTranslations();
|
|
17
|
+
const { locale: contextLocale } = useLocale();
|
|
18
|
+
const locale = localeProp ?? contextLocale;
|
|
19
|
+
|
|
20
|
+
const content = useMemo(() => getDapDescription(slug, locale), [slug, locale]);
|
|
21
|
+
|
|
22
|
+
if (!content || (content.paragraphs.length === 0 && (content.sections?.length ?? 0) === 0)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<TourDescription
|
|
28
|
+
paragraphs={content.paragraphs}
|
|
29
|
+
review={content.review}
|
|
30
|
+
sections={content.sections}
|
|
31
|
+
defaultExpanded={false}
|
|
32
|
+
toggleLabel={t('booking.seeFullAddOnDescription')}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|