@ticketboothapp/booking 0.1.19 → 0.1.20
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/components/BookingWidget.tsx +282 -26
- package/src/components/ManageBookingView.tsx +75 -23
- package/src/components/booking/BookingProductGrid.tsx +1 -1
- package/src/components/booking/Calendar.module.css +3 -3
- package/src/components/booking/CheckoutForm.tsx +1 -1
- package/src/components/booking/InfoTooltip.tsx +2 -13
- package/src/components/booking/PickupLocationSelector.tsx +2 -2
- package/src/components/booking/PriceBreakdown.tsx +11 -34
- package/src/index.ts +3 -1
- package/src/app/photo-sessions/photo-packages.ts +0 -75
- package/src/assets/icons/minus.svg +0 -7
- package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
- package/src/assets/icons/plus.svg +0 -3
- package/src/colours.css +0 -23
- package/src/components/BookingDetails.module.css +0 -1591
- package/src/components/BookingDetails.tsx +0 -2264
- package/src/components/JobApplicationDialog.module.css +0 -440
- package/src/components/JobApplicationDialog.tsx +0 -620
- package/src/components/PhoneInputWithCountry.module.css +0 -131
- package/src/components/PhoneInputWithCountry.tsx +0 -44
- package/src/components/PickupLocationDialog.module.css +0 -360
- package/src/components/PickupLocationDialog.tsx +0 -357
- package/src/components/PickupLocationMap.tsx +0 -110
- package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
- package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
- package/src/components/accordion.css +0 -27
- package/src/components/accordion.tsx +0 -29
- package/src/components/analytics/AnalyticsConsentRestore.tsx +0 -19
- package/src/components/analytics/AnalyticsScripts.tsx +0 -106
- package/src/components/analytics/CookieConsentBanner.css +0 -86
- package/src/components/analytics/CookieConsentBanner.tsx +0 -102
- package/src/components/bottom-sheet.module.css +0 -78
- package/src/components/bottom-sheet.tsx +0 -60
- package/src/components/breadcrumb.module.css +0 -40
- package/src/components/breadcrumb.tsx +0 -36
- package/src/components/button.css +0 -245
- package/src/components/button.tsx +0 -152
- package/src/components/client-bottom-sheet.tsx +0 -14
- package/src/components/colorable-svg.tsx +0 -29
- package/src/components/conditional-footer.tsx +0 -27
- package/src/components/contact-us.module.css +0 -147
- package/src/components/contact-us.tsx +0 -49
- package/src/components/email-signup.css +0 -151
- package/src/components/email-signup.tsx +0 -63
- package/src/components/faq-wrapper.module.css +0 -47
- package/src/components/faq-wrapper.tsx +0 -15
- package/src/components/footer.css +0 -187
- package/src/components/footer.tsx +0 -143
- package/src/components/global-simple-modal.tsx +0 -33
- package/src/components/google-review-summary.module.css +0 -77
- package/src/components/google-review-summary.tsx +0 -50
- package/src/components/hero-image.css +0 -13
- package/src/components/hero-image.tsx +0 -44
- package/src/components/image.css +0 -29
- package/src/components/image.tsx +0 -113
- package/src/components/language-aware-link.tsx +0 -72
- package/src/components/language-switcher.module.css +0 -124
- package/src/components/language-switcher.tsx +0 -75
- package/src/components/map-section.css +0 -59
- package/src/components/map-section.tsx +0 -63
- package/src/components/navbar.module.css +0 -152
- package/src/components/navbar.tsx +0 -125
- package/src/components/parallax-provider.tsx +0 -11
- package/src/components/product-tag.module.css +0 -30
- package/src/components/product-tag.tsx +0 -34
- package/src/components/product-theme-pages/best-option.module.css +0 -70
- package/src/components/product-theme-pages/best-option.tsx +0 -35
- package/src/components/product-theme-pages/extended-tour-options.module.css +0 -22
- package/src/components/product-theme-pages/extended-tour-options.tsx +0 -11
- package/src/components/product-theme-pages/image-modal.tsx +0 -248
- package/src/components/product-theme-pages/photo-gallery.module.css +0 -200
- package/src/components/product-theme-pages/photo-gallery.tsx +0 -90
- package/src/components/product-theme-pages/product-theme-page-layout.module.css +0 -13
- package/src/components/product-theme-pages/product-theme-page-layout.tsx +0 -67
- package/src/components/product-theme-pages/top-of-fold.module.css +0 -179
- package/src/components/product-theme-pages/top-of-fold.tsx +0 -80
- package/src/components/product-tile/image-only-product-tile-desktop.module.css +0 -106
- package/src/components/product-tile/image-only-product-tile-desktop.tsx +0 -56
- package/src/components/product-tile/image-only-product-tile-mobile.module.css +0 -122
- package/src/components/product-tile/image-only-product-tile-mobile.tsx +0 -89
- package/src/components/product-tile/image-only-product-tile.tsx +0 -44
- package/src/components/product-tile/product-tile-card.module.css +0 -84
- package/src/components/product-tile/product-tile-card.tsx +0 -61
- package/src/components/review-highlights-section.css +0 -85
- package/src/components/review-highlights-section.tsx +0 -127
- package/src/components/season-closure-overlay.module.css +0 -99
- package/src/components/season-closure-overlay.tsx +0 -98
- package/src/components/simple-modal.tsx +0 -69
- package/src/components/simple-top-of-fold.module.css +0 -76
- package/src/components/simple-top-of-fold.tsx +0 -34
- package/src/components/spacer.css +0 -41
- package/src/components/spacer.tsx +0 -23
- package/src/components/star-rating.module.css +0 -74
- package/src/components/star-rating.tsx +0 -48
- package/src/components/terms/TermsContent.tsx +0 -178
- package/src/components/title-subtitle.module.css +0 -10
- package/src/components/title-subtitle.tsx +0 -30
- package/src/components/translatable-reviews.tsx +0 -75
- package/src/components/value-pill.module.css +0 -59
- package/src/components/value-pill.tsx +0 -46
- package/src/components/value-props.css +0 -185
- package/src/components/value-props.tsx +0 -88
- package/src/constants/booking-guide-quiz.ts +0 -64
- package/src/constants/contact-info.ts +0 -2
- package/src/constants/faq.ts +0 -44
- package/src/constants/images.ts +0 -556
- package/src/constants/json-ld/faq-json-ld.tsx +0 -170
- package/src/constants/json-ld/homepage-json-ld.tsx +0 -138
- package/src/constants/json-ld/job-posting-json-ld.tsx +0 -92
- package/src/constants/json-ld/organization-json-ld.tsx +0 -62
- package/src/constants/json-ld/page-json-ld.tsx +0 -6
- package/src/constants/json-ld/product-json-ld.tsx +0 -154
- package/src/constants/json-ld/review-json-ld.tsx +0 -377
- package/src/constants/navigation-links/footer-links.ts +0 -48
- package/src/constants/navigation-links/nav-bar-links.ts +0 -41
- package/src/constants/navigation-links/navigation-link.ts +0 -6
- package/src/constants/pill-values.ts +0 -210
- package/src/constants/products.ts +0 -155
- package/src/constants/quiz-recommendations.ts +0 -506
- package/src/constants/reviews.ts +0 -75
- package/src/constants/staff.ts +0 -197
- package/src/constants/value-props.ts +0 -58
- package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
- package/src/data/dap-descriptions/session-elopements.en.json +0 -60
- package/src/data/dap-descriptions/session-proposals.en.json +0 -60
- package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
- package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
- package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
- package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
- package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
- package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
- package/src/data/product-descriptions/private-tour.en.json +0 -80
- package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
- package/src/data/products-config.json +0 -101
- package/src/hooks/use-bottom-sheet.tsx +0 -15
- package/src/hooks/use-simple-modal.tsx +0 -27
- package/src/hooks/useBookingSourceMetadataFromLocation.ts +0 -21
- package/src/hooks/useEmailSubscription.tsx +0 -103
- package/src/hooks/useEmbeddedInIframe.ts +0 -16
- package/src/hooks/useIsBookingLaunchLive.ts +0 -49
- package/src/hooks/useQuiz.tsx +0 -210
- package/src/providers/bottom-sheet-provider.tsx +0 -40
- package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
- package/src/radius.css +0 -5
- package/src/spacing.css +0 -7
- package/src/strings/en.json +0 -1774
- package/src/strings/es.json +0 -1573
- package/src/strings/fr.json +0 -1573
- package/src/strings/index.js +0 -23
- package/src/text-style.css +0 -97
- package/src/types/fareharbor.d.ts +0 -12
- package/src/types/quiz.ts +0 -59
- package/src/utils/currency-converter.ts +0 -101
package/package.json
CHANGED
|
@@ -1,46 +1,302 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect } from 'react';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { CompanyProvider } from '@/contexts/CompanyContext';
|
|
5
|
+
import {
|
|
6
|
+
BookingAppProvider,
|
|
7
|
+
type BookingAppMode,
|
|
8
|
+
type BookingAppPermissions,
|
|
9
|
+
type ManageParams,
|
|
10
|
+
} from '@/contexts/BookingAppContext';
|
|
11
|
+
import { BookingFlow } from '@/components/booking/BookingFlow';
|
|
12
|
+
import { PrivateShuttleBookingFlow } from '@/components/booking/PrivateShuttleBookingFlow';
|
|
13
|
+
import { CurrencySwitcher, useCurrency, type Currency } from '@/components/booking/CurrencySwitcher';
|
|
14
|
+
import { ErrorBoundary } from '@/components/booking/ErrorBoundary';
|
|
15
|
+
import { getProduct, setPartnerPortalBookingJwtGetter, type Product } from '@/lib/booking-api';
|
|
16
|
+
import { getProductByIdOrSlug } from '@/lib/products-config';
|
|
17
|
+
import { ENV } from '@/lib/env';
|
|
18
|
+
import BookingProductGrid from '@/components/booking/BookingProductGrid';
|
|
19
|
+
import { BookingDialogProvider } from '@/providers/booking-dialog-provider';
|
|
20
|
+
|
|
21
|
+
type Step = 'products' | 'booking';
|
|
22
|
+
|
|
23
|
+
interface NavState {
|
|
24
|
+
step: Step;
|
|
25
|
+
selectedProduct: Product | null;
|
|
26
|
+
}
|
|
8
27
|
|
|
9
28
|
export interface BookingWidgetProps {
|
|
29
|
+
initialProductId?: string;
|
|
30
|
+
initialCurrency?: Currency;
|
|
31
|
+
showHeader?: boolean;
|
|
10
32
|
authToken?: string | null;
|
|
33
|
+
onProductSelect?: (product: Product) => void;
|
|
11
34
|
onBookingSuccess?: (data: { reservationReference: string; sessionId?: string }) => void;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
35
|
+
onBack?: () => void;
|
|
36
|
+
className?: string;
|
|
37
|
+
mode?: BookingAppMode;
|
|
38
|
+
permissions?: Partial<BookingAppPermissions>;
|
|
39
|
+
googleMapsApiKey?: string;
|
|
40
|
+
onShowManage?: (params: ManageParams) => void;
|
|
41
|
+
getSuccessUrl?: (params: { reservationRef: string; lastName: string; focusDate?: string }) => string;
|
|
42
|
+
showLanguageSelector?: boolean;
|
|
43
|
+
initialProduct?: Product;
|
|
44
|
+
products?: Product[];
|
|
45
|
+
companyId?: string;
|
|
46
|
+
initialBooking?: {
|
|
47
|
+
bookingReference: string;
|
|
48
|
+
productId: string;
|
|
49
|
+
availabilityId?: string;
|
|
50
|
+
dateTime: string;
|
|
51
|
+
originalTotalAmount?: number;
|
|
52
|
+
originalCurrency?: string;
|
|
53
|
+
bookingItems: Array<{ category: string; count: number }>;
|
|
54
|
+
returnAvailabilityId?: string | null;
|
|
55
|
+
pickupLocationId?: string | null;
|
|
56
|
+
travelerHotel?: string | null;
|
|
57
|
+
startTime?: string | null;
|
|
58
|
+
privateShuttleDetails?: { passengerCount?: number };
|
|
59
|
+
cancellationPolicyId?: string | null;
|
|
60
|
+
promoCode?: string | null;
|
|
61
|
+
additionalHoursCount?: number | null;
|
|
62
|
+
addOnSelections?: Array<{ addOnId: string; variantId?: string; quantity?: number }> | null;
|
|
63
|
+
};
|
|
64
|
+
/** Last name on the booking — required for public change-quote APIs inside the flow. */
|
|
65
|
+
changeFlowLastName?: string;
|
|
15
66
|
}
|
|
16
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Embeddable booking UI (product grid + flow). Used by provider-dashboard change-booking in “change mode”.
|
|
70
|
+
*/
|
|
17
71
|
export function BookingWidget({
|
|
72
|
+
initialProductId,
|
|
73
|
+
initialProduct,
|
|
74
|
+
initialBooking,
|
|
75
|
+
changeFlowLastName = '',
|
|
76
|
+
products: productsProp,
|
|
77
|
+
companyId: _companyId,
|
|
78
|
+
initialCurrency = 'CAD',
|
|
79
|
+
showHeader = true,
|
|
18
80
|
authToken,
|
|
81
|
+
onProductSelect,
|
|
19
82
|
onBookingSuccess,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
83
|
+
onBack,
|
|
84
|
+
className = '',
|
|
85
|
+
mode = 'standalone',
|
|
86
|
+
permissions = {},
|
|
87
|
+
googleMapsApiKey,
|
|
88
|
+
onShowManage,
|
|
89
|
+
getSuccessUrl,
|
|
90
|
+
showLanguageSelector = true,
|
|
23
91
|
}: BookingWidgetProps) {
|
|
92
|
+
const { currency, setCurrency } = useCurrency();
|
|
93
|
+
|
|
24
94
|
useEffect(() => {
|
|
25
95
|
setPartnerPortalBookingJwtGetter(() => authToken ?? null);
|
|
26
96
|
return () => setPartnerPortalBookingJwtGetter(() => null);
|
|
27
97
|
}, [authToken]);
|
|
28
98
|
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (initialCurrency) setCurrency(initialCurrency);
|
|
101
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
const isChangeMode = !!(initialProduct && initialBooking);
|
|
105
|
+
|
|
106
|
+
const flowInitialValues = initialBooking
|
|
107
|
+
? {
|
|
108
|
+
bookingReference: initialBooking.bookingReference,
|
|
109
|
+
dateTime: initialBooking.dateTime,
|
|
110
|
+
availabilityId: initialBooking.availabilityId ?? null,
|
|
111
|
+
productOptionId: initialBooking.productId,
|
|
112
|
+
pickupLocationId: initialBooking.pickupLocationId ?? null,
|
|
113
|
+
returnAvailabilityId: initialBooking.returnAvailabilityId ?? null,
|
|
114
|
+
bookingItems: initialBooking.bookingItems,
|
|
115
|
+
addOnSelections: initialBooking.addOnSelections ?? null,
|
|
116
|
+
promoCode: initialBooking.promoCode ?? null,
|
|
117
|
+
cancellationPolicyId: initialBooking.cancellationPolicyId ?? null,
|
|
118
|
+
customer: changeFlowLastName ? { lastName: changeFlowLastName } : null,
|
|
119
|
+
}
|
|
120
|
+
: undefined;
|
|
121
|
+
|
|
122
|
+
const privateShuttleInitialValues = initialBooking
|
|
123
|
+
? {
|
|
124
|
+
bookingReference: initialBooking.bookingReference,
|
|
125
|
+
dateTime: initialBooking.dateTime,
|
|
126
|
+
pickupLocationId: initialBooking.pickupLocationId ?? null,
|
|
127
|
+
customPickupAddress: initialBooking.travelerHotel ?? null,
|
|
128
|
+
passengers: initialBooking.privateShuttleDetails?.passengerCount ?? null,
|
|
129
|
+
additionalHoursCount: initialBooking.additionalHoursCount ?? null,
|
|
130
|
+
notes: null,
|
|
131
|
+
specialRequest: null,
|
|
132
|
+
}
|
|
133
|
+
: undefined;
|
|
134
|
+
|
|
135
|
+
const bookingSourceAttribution: import('@/lib/booking/source-metadata').BookingSourceMetadata = {
|
|
136
|
+
pagePath: '/provider-dashboard',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const [navState, setNavState] = useState<NavState>(() => {
|
|
140
|
+
if (isChangeMode && initialProduct) {
|
|
141
|
+
return { step: 'booking', selectedProduct: initialProduct };
|
|
142
|
+
}
|
|
143
|
+
return { step: 'products', selectedProduct: null };
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const [error, setError] = useState('');
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!isChangeMode || !productsProp?.length || !initialProductId) return;
|
|
150
|
+
const product = productsProp.find(
|
|
151
|
+
(p) => p.productId === initialProductId || p.options?.some((o) => o.optionId === initialProductId)
|
|
152
|
+
);
|
|
153
|
+
if (product) setNavState({ step: 'booking', selectedProduct: product });
|
|
154
|
+
}, [isChangeMode, initialProductId, productsProp]);
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (isChangeMode || !initialProductId) return;
|
|
158
|
+
let cancelled = false;
|
|
159
|
+
(async () => {
|
|
160
|
+
const config = getProductByIdOrSlug(initialProductId);
|
|
161
|
+
const apiProductId = config?.productId ?? initialProductId;
|
|
162
|
+
try {
|
|
163
|
+
const p = await getProduct(apiProductId, ENV.COMPANY_ID);
|
|
164
|
+
if (!cancelled && p) setNavState({ step: 'booking', selectedProduct: p });
|
|
165
|
+
} catch (e) {
|
|
166
|
+
if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load product');
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
return () => {
|
|
170
|
+
cancelled = true;
|
|
171
|
+
};
|
|
172
|
+
}, [isChangeMode, initialProductId]);
|
|
173
|
+
|
|
174
|
+
const handleSelectProduct = useCallback(
|
|
175
|
+
(product: Product) => {
|
|
176
|
+
setNavState({ step: 'booking', selectedProduct: product });
|
|
177
|
+
onProductSelect?.(product);
|
|
178
|
+
},
|
|
179
|
+
[onProductSelect]
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const handleBookProductId = useCallback(
|
|
183
|
+
async (productSlugOrId: string) => {
|
|
184
|
+
const config = getProductByIdOrSlug(productSlugOrId);
|
|
185
|
+
const apiProductId = config?.productId ?? productSlugOrId;
|
|
186
|
+
try {
|
|
187
|
+
const p = await getProduct(apiProductId, ENV.COMPANY_ID);
|
|
188
|
+
if (p) handleSelectProduct(p);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
setError(e instanceof Error ? e.message : 'Failed to load product');
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
[handleSelectProduct]
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const handleBack = useCallback(() => {
|
|
197
|
+
setNavState({ step: 'products', selectedProduct: null });
|
|
198
|
+
onBack?.();
|
|
199
|
+
}, [onBack]);
|
|
200
|
+
|
|
201
|
+
const handleBookingSuccess = useCallback(
|
|
202
|
+
(data: { reservationReference: string; sessionId?: string }) => {
|
|
203
|
+
onBookingSuccess?.(data);
|
|
204
|
+
},
|
|
205
|
+
[onBookingSuccess]
|
|
206
|
+
);
|
|
207
|
+
|
|
29
208
|
return (
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
209
|
+
<CompanyProvider>
|
|
210
|
+
<BookingAppProvider
|
|
211
|
+
mode={mode}
|
|
212
|
+
permissions={permissions}
|
|
213
|
+
googleMapsApiKey={googleMapsApiKey}
|
|
214
|
+
onShowManage={onShowManage}
|
|
215
|
+
getSuccessUrl={getSuccessUrl}
|
|
216
|
+
showLanguageSelector={showLanguageSelector}
|
|
217
|
+
>
|
|
218
|
+
<BookingDialogProvider>
|
|
219
|
+
<div className={`min-h-0 overflow-x-hidden ${className}`}>
|
|
220
|
+
{showHeader && (
|
|
221
|
+
<header className="py-4 w-full overflow-hidden" style={{ backgroundColor: 'var(--booking-header-bg)', color: 'var(--booking-header-text)' }}>
|
|
222
|
+
<div className="max-w-4xl mx-auto px-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 min-w-0">
|
|
223
|
+
<h1 className="text-xl font-semibold shrink-0" style={{ fontFamily: 'var(--booking-font-sans)' }}>
|
|
224
|
+
Via Via Moraine Lake
|
|
225
|
+
</h1>
|
|
226
|
+
<div className="flex flex-wrap items-center gap-2 sm:gap-4 min-w-0">
|
|
227
|
+
<CurrencySwitcher currency={currency} onCurrencyChange={setCurrency} />
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</header>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{!showHeader && (
|
|
234
|
+
<div
|
|
235
|
+
className="border-b px-4 py-2 flex flex-wrap items-center justify-end gap-2 min-w-0"
|
|
236
|
+
style={{ borderColor: 'var(--booking-border)', backgroundColor: 'var(--booking-surface)' }}
|
|
237
|
+
>
|
|
238
|
+
<CurrencySwitcher currency={currency} onCurrencyChange={setCurrency} />
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
<main className={`max-w-4xl mx-auto px-4 py-8 ${!showHeader ? 'pt-4' : ''}`} style={{ fontFamily: 'var(--booking-font-sans)' }}>
|
|
243
|
+
<div
|
|
244
|
+
className="overflow-visible p-8"
|
|
245
|
+
style={{
|
|
246
|
+
backgroundColor: 'var(--booking-surface)',
|
|
247
|
+
borderRadius: 'var(--booking-radius)',
|
|
248
|
+
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.25)',
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
{error ? (
|
|
252
|
+
<div
|
|
253
|
+
className="p-4 rounded-lg"
|
|
254
|
+
style={{
|
|
255
|
+
backgroundColor: 'var(--booking-error-bg)',
|
|
256
|
+
borderWidth: '1px',
|
|
257
|
+
borderStyle: 'solid',
|
|
258
|
+
borderColor: 'var(--booking-error-border)',
|
|
259
|
+
color: 'var(--booking-error-text)',
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
{error}
|
|
263
|
+
</div>
|
|
264
|
+
) : navState.step === 'products' ? (
|
|
265
|
+
<BookingProductGrid
|
|
266
|
+
initialFilterId="all"
|
|
267
|
+
bookOnTileClick
|
|
268
|
+
onBookProduct={handleBookProductId}
|
|
269
|
+
/>
|
|
270
|
+
) : navState.selectedProduct ? (
|
|
271
|
+
<ErrorBoundary>
|
|
272
|
+
{navState.selectedProduct.productType === 'PRIVATE_SHUTTLE' ? (
|
|
273
|
+
<PrivateShuttleBookingFlow
|
|
274
|
+
product={navState.selectedProduct}
|
|
275
|
+
onBack={handleBack}
|
|
276
|
+
currency={currency}
|
|
277
|
+
onSuccess={handleBookingSuccess}
|
|
278
|
+
mode={isChangeMode ? 'change' : 'standard'}
|
|
279
|
+
initialValues={isChangeMode ? privateShuttleInitialValues : undefined}
|
|
280
|
+
bookingSourceAttribution={bookingSourceAttribution}
|
|
281
|
+
/>
|
|
282
|
+
) : (
|
|
283
|
+
<BookingFlow
|
|
284
|
+
product={navState.selectedProduct}
|
|
285
|
+
onBack={handleBack}
|
|
286
|
+
currency={currency}
|
|
287
|
+
onSuccess={handleBookingSuccess}
|
|
288
|
+
mode={isChangeMode ? 'change' : 'standard'}
|
|
289
|
+
initialValues={isChangeMode ? flowInitialValues : undefined}
|
|
290
|
+
bookingSourceAttribution={bookingSourceAttribution}
|
|
291
|
+
/>
|
|
292
|
+
)}
|
|
293
|
+
</ErrorBoundary>
|
|
294
|
+
) : null}
|
|
295
|
+
</div>
|
|
296
|
+
</main>
|
|
297
|
+
</div>
|
|
298
|
+
</BookingDialogProvider>
|
|
299
|
+
</BookingAppProvider>
|
|
300
|
+
</CompanyProvider>
|
|
45
301
|
);
|
|
46
302
|
}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useLayoutEffect } from 'react';
|
|
4
4
|
import { formatBookingRefForDisplay } from '@/lib/booking-ref';
|
|
5
|
-
import
|
|
6
|
-
import { type Currency } from '@/
|
|
5
|
+
import BookingDetails, { type BookingData } from '@/components/BookingDetails';
|
|
6
|
+
import { type Currency, formatCurrencyAmount } from '@/lib/currency';
|
|
7
7
|
import { ENV } from '@/lib/env';
|
|
8
|
-
import { formatCurrencyAmount } from '@/lib/currency';
|
|
9
8
|
|
|
10
9
|
const API_URL = ENV.API_URL;
|
|
11
10
|
|
|
11
|
+
/** Provider-only fields merged after public manage lookup (e.g. embedded in provider dashboard). */
|
|
12
12
|
export interface StaffBookingAttribution {
|
|
13
13
|
reportingSource: string;
|
|
14
14
|
partnerBookingPortal: boolean;
|
|
@@ -121,12 +121,14 @@ export function ManageBookingView({
|
|
|
121
121
|
if (cancelled) return;
|
|
122
122
|
setBooking(b);
|
|
123
123
|
|
|
124
|
-
const hasPaymentOwing = (
|
|
125
|
-
const status =
|
|
126
|
-
const balanceAmount =
|
|
127
|
-
const depositAmount =
|
|
128
|
-
return (
|
|
129
|
-
(status === '
|
|
124
|
+
const hasPaymentOwing = (bk: BookingData) => {
|
|
125
|
+
const status = bk?.payment?.status;
|
|
126
|
+
const balanceAmount = bk?.payment?.plan?.balanceAmount ?? 0;
|
|
127
|
+
const depositAmount = bk?.payment?.plan?.depositAmount ?? 0;
|
|
128
|
+
return (
|
|
129
|
+
(status === 'DEPOSIT_PAID' && balanceAmount > 0) ||
|
|
130
|
+
(status === 'AWAITING_PAYMENT' && (depositAmount > 0 || balanceAmount > 0))
|
|
131
|
+
);
|
|
130
132
|
};
|
|
131
133
|
|
|
132
134
|
if (initialPaymentSuccess && b && hasPaymentOwing(b)) {
|
|
@@ -147,7 +149,9 @@ export function ManageBookingView({
|
|
|
147
149
|
}
|
|
148
150
|
};
|
|
149
151
|
doLookup();
|
|
150
|
-
return () => {
|
|
152
|
+
return () => {
|
|
153
|
+
cancelled = true;
|
|
154
|
+
};
|
|
151
155
|
}, [initialRef, initialLastName, initialPaymentSuccess, replaceUrl]);
|
|
152
156
|
|
|
153
157
|
useEffect(() => {
|
|
@@ -180,7 +184,9 @@ export function ManageBookingView({
|
|
|
180
184
|
return;
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
|
-
} catch {
|
|
187
|
+
} catch {
|
|
188
|
+
/* ignore */
|
|
189
|
+
}
|
|
184
190
|
if (cancelled) return;
|
|
185
191
|
attempt += 1;
|
|
186
192
|
if (attempt * POLL_MS < MAX_MS) {
|
|
@@ -193,7 +199,9 @@ export function ManageBookingView({
|
|
|
193
199
|
await poll();
|
|
194
200
|
};
|
|
195
201
|
doLookup();
|
|
196
|
-
return () => {
|
|
202
|
+
return () => {
|
|
203
|
+
cancelled = true;
|
|
204
|
+
};
|
|
197
205
|
}, [initialReservationRef, initialLastName, initialRef, replaceUrl]);
|
|
198
206
|
|
|
199
207
|
async function handleLookup() {
|
|
@@ -239,7 +247,7 @@ export function ManageBookingView({
|
|
|
239
247
|
setBooking(data.data);
|
|
240
248
|
}
|
|
241
249
|
} catch {
|
|
242
|
-
|
|
250
|
+
/* ignore */
|
|
243
251
|
}
|
|
244
252
|
}
|
|
245
253
|
|
|
@@ -264,7 +272,11 @@ export function ManageBookingView({
|
|
|
264
272
|
</div>
|
|
265
273
|
)}
|
|
266
274
|
{staffAttribution ? (
|
|
267
|
-
<div
|
|
275
|
+
<div
|
|
276
|
+
className="mb-4 rounded-xl border border-stone-200 bg-stone-50 px-4 py-3 text-sm text-stone-800"
|
|
277
|
+
role="region"
|
|
278
|
+
aria-label="Staff booking attribution"
|
|
279
|
+
>
|
|
268
280
|
<p className="font-semibold text-stone-900 mb-2">Booking attribution</p>
|
|
269
281
|
<p>
|
|
270
282
|
<span className="text-stone-600">Channel:</span> {staffChannelLabel(staffAttribution.reportingSource)}
|
|
@@ -272,7 +284,9 @@ export function ManageBookingView({
|
|
|
272
284
|
{staffAttribution.partnerId ? (
|
|
273
285
|
<p>
|
|
274
286
|
<span className="text-stone-600">Partner:</span>{' '}
|
|
275
|
-
{staffAttribution.partnerName
|
|
287
|
+
{staffAttribution.partnerName
|
|
288
|
+
? `${staffAttribution.partnerName} (${staffAttribution.partnerId})`
|
|
289
|
+
: staffAttribution.partnerId}
|
|
276
290
|
</p>
|
|
277
291
|
) : null}
|
|
278
292
|
{staffAttribution.agentDisplay || staffAttribution.agentId ? (
|
|
@@ -308,7 +322,11 @@ export function ManageBookingView({
|
|
|
308
322
|
if (waitingForBooking && loading) {
|
|
309
323
|
return (
|
|
310
324
|
<div className={containerClass}>
|
|
311
|
-
<div
|
|
325
|
+
<div
|
|
326
|
+
className="w-10 h-10 border-2 border-t-transparent rounded-full animate-spin"
|
|
327
|
+
style={{ borderColor: 'var(--booking-primary)' }}
|
|
328
|
+
aria-hidden
|
|
329
|
+
/>
|
|
312
330
|
<p style={{ color: 'var(--booking-text-muted)' }}>Confirming your booking…</p>
|
|
313
331
|
</div>
|
|
314
332
|
);
|
|
@@ -316,10 +334,15 @@ export function ManageBookingView({
|
|
|
316
334
|
|
|
317
335
|
return (
|
|
318
336
|
<div className={formOuterClass || undefined}>
|
|
319
|
-
<div
|
|
337
|
+
<div
|
|
338
|
+
className="p-8 max-w-md w-full shadow-xl"
|
|
339
|
+
style={{ backgroundColor: 'var(--booking-surface)', borderRadius: 'var(--booking-radius)' }}
|
|
340
|
+
>
|
|
320
341
|
{isEmbed && (
|
|
321
342
|
<div className={`flex justify-between items-center mb-4 ${showClose ? '' : 'justify-start'}`}>
|
|
322
|
-
<h1 className="text-2xl font-bold" style={{ color: 'var(--booking-text)' }}>
|
|
343
|
+
<h1 className="text-2xl font-bold" style={{ color: 'var(--booking-text)' }}>
|
|
344
|
+
Manage Your Booking
|
|
345
|
+
</h1>
|
|
323
346
|
{showClose && (
|
|
324
347
|
<button type="button" onClick={onClose} className="text-sm hover:opacity-80" style={{ color: 'var(--booking-text-muted)' }}>
|
|
325
348
|
Close
|
|
@@ -327,9 +350,19 @@ export function ManageBookingView({
|
|
|
327
350
|
)}
|
|
328
351
|
</div>
|
|
329
352
|
)}
|
|
330
|
-
{!isEmbed &&
|
|
353
|
+
{!isEmbed && (
|
|
354
|
+
<h1 className="text-2xl font-bold mb-6" style={{ color: 'var(--booking-text)' }}>
|
|
355
|
+
Manage Your Booking
|
|
356
|
+
</h1>
|
|
357
|
+
)}
|
|
331
358
|
|
|
332
|
-
<form
|
|
359
|
+
<form
|
|
360
|
+
onSubmit={(e) => {
|
|
361
|
+
e.preventDefault();
|
|
362
|
+
handleLookup();
|
|
363
|
+
}}
|
|
364
|
+
className="space-y-4"
|
|
365
|
+
>
|
|
333
366
|
<div>
|
|
334
367
|
<label htmlFor="manage-booking-ref" className="block text-sm font-medium mb-1" style={{ color: 'var(--booking-text)' }}>
|
|
335
368
|
Booking Reference
|
|
@@ -361,7 +394,16 @@ export function ManageBookingView({
|
|
|
361
394
|
/>
|
|
362
395
|
</div>
|
|
363
396
|
{error && (
|
|
364
|
-
<div
|
|
397
|
+
<div
|
|
398
|
+
className="rounded-lg p-3 text-sm"
|
|
399
|
+
style={{
|
|
400
|
+
backgroundColor: 'var(--booking-error-bg)',
|
|
401
|
+
border: '1px solid var(--booking-error-border)',
|
|
402
|
+
color: 'var(--booking-error-text)',
|
|
403
|
+
}}
|
|
404
|
+
>
|
|
405
|
+
{error}
|
|
406
|
+
</div>
|
|
365
407
|
)}
|
|
366
408
|
<button
|
|
367
409
|
type="submit"
|
|
@@ -374,9 +416,19 @@ export function ManageBookingView({
|
|
|
374
416
|
</form>
|
|
375
417
|
|
|
376
418
|
{!isEmbed && (
|
|
377
|
-
<div
|
|
419
|
+
<div
|
|
420
|
+
className="mt-6 pt-6 text-center text-sm"
|
|
421
|
+
style={{
|
|
422
|
+
borderColor: 'var(--booking-border)',
|
|
423
|
+
borderTopWidth: '1px',
|
|
424
|
+
borderTopStyle: 'solid',
|
|
425
|
+
color: 'var(--booking-text-muted)',
|
|
426
|
+
}}
|
|
427
|
+
>
|
|
378
428
|
<p>Enter your booking reference and last name to view your booking details.</p>
|
|
379
|
-
<p className="mt-1" style={{ color: 'var(--booking-text-muted)' }}>
|
|
429
|
+
<p className="mt-1" style={{ color: 'var(--booking-text-muted)' }}>
|
|
430
|
+
You can also share a link: /manage?ref=ABC12345&lastName=Smith
|
|
431
|
+
</p>
|
|
380
432
|
</div>
|
|
381
433
|
)}
|
|
382
434
|
</div>
|
|
@@ -19,7 +19,7 @@ import defaultStrings from '@/strings';
|
|
|
19
19
|
import { getImageUrl } from '@/constants/images';
|
|
20
20
|
import BackgroundPlayer from 'next-video/background-player';
|
|
21
21
|
import styles from './BookingProductGrid.module.css';
|
|
22
|
-
import Button, { ButtonHoverColor } from '
|
|
22
|
+
import Button, { ButtonHoverColor } from '@/components/button';
|
|
23
23
|
|
|
24
24
|
const DEFAULT_VIDEO = {
|
|
25
25
|
src: '/videos/via-via-moraine-lake-tour-video.mp4',
|
|
@@ -381,17 +381,17 @@
|
|
|
381
381
|
flex-direction: column;
|
|
382
382
|
align-items: center;
|
|
383
383
|
justify-content: center;
|
|
384
|
-
min-height:
|
|
384
|
+
min-height: 5.5rem; /* Desktop: enough room for time pills */
|
|
385
385
|
cursor: pointer;
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
/* Admin (showCapacity): extra line under each time pill — taller cells on sm+ only */
|
|
389
389
|
@media (min-width: 640px) {
|
|
390
390
|
.calendar .calendarDayCell.calendarDayCellWithAdminCapacity {
|
|
391
|
-
min-height:
|
|
391
|
+
min-height: 7.5rem;
|
|
392
392
|
}
|
|
393
393
|
.calendar .calendarDayCell.calendarDayCellWithAdminCapacity.calendarDayCellWithAdminCapacityTall {
|
|
394
|
-
min-height:
|
|
394
|
+
min-height: 9rem;
|
|
395
395
|
}
|
|
396
396
|
}
|
|
397
397
|
|
|
@@ -6,7 +6,7 @@ import { PickupLocationSelector } from './PickupLocationSelector';
|
|
|
6
6
|
import type { Currency } from './CurrencySwitcher';
|
|
7
7
|
import type { PickupLocation, Destination } from '@/lib/booking-api';
|
|
8
8
|
import styles from './CheckoutForm.module.css';
|
|
9
|
-
import Button, { ButtonHoverColor } from '
|
|
9
|
+
import Button, { ButtonHoverColor } from '@/components/button';
|
|
10
10
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
11
11
|
|
|
12
12
|
type TranslationFn = (key: string, params?: Record<string, string>) => string;
|
|
@@ -96,19 +96,8 @@ export function InfoTooltip({ text }: InfoTooltipProps) {
|
|
|
96
96
|
typeof document !== 'undefined' &&
|
|
97
97
|
createPortal(
|
|
98
98
|
<span
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
whiteSpace: 'normal',
|
|
102
|
-
fontSize: '0.75rem',
|
|
103
|
-
lineHeight: 1.3,
|
|
104
|
-
backgroundColor: '#1f2937',
|
|
105
|
-
color: '#ffffff',
|
|
106
|
-
padding: '0.5rem 0.75rem',
|
|
107
|
-
borderRadius: '0.5rem',
|
|
108
|
-
boxShadow: '0 10px 20px rgba(0,0,0,0.25)',
|
|
109
|
-
pointerEvents: 'none',
|
|
110
|
-
border: '1px solid rgba(255,255,255,0.08)',
|
|
111
|
-
}}
|
|
99
|
+
className="whitespace-normal text-xs bg-stone-800 text-white px-3 py-2 rounded shadow-lg pointer-events-none"
|
|
100
|
+
style={tooltipStyle}
|
|
112
101
|
>
|
|
113
102
|
{text}
|
|
114
103
|
</span>,
|
|
@@ -890,7 +890,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
890
890
|
|
|
891
891
|
{/* Input with search icon inside - padding ensures text stays to the right of icon */}
|
|
892
892
|
<div className="relative">
|
|
893
|
-
<div className="absolute
|
|
893
|
+
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-stone-400">
|
|
894
894
|
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
895
895
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
896
896
|
</svg>
|
|
@@ -1167,7 +1167,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
1167
1167
|
<input type="radio" name="pickup-location" checked={selectedLocationId === location.id} onChange={() => handleLocationSelect(location.id, location.name)} className="mt-1 w-4 h-4 text-emerald-600 focus:ring-emerald-500 shrink-0" />
|
|
1168
1168
|
<div className="flex-1 min-w-0">
|
|
1169
1169
|
<div className="flex items-center gap-1.5">
|
|
1170
|
-
|
|
1170
|
+
<p className="font-medium text-stone-900 text-sm">{location.name}</p>
|
|
1171
1171
|
{isPartnerHighlighted && (
|
|
1172
1172
|
<span
|
|
1173
1173
|
className="inline-flex items-center justify-center rounded-full w-4 h-4 bg-emerald-100 text-emerald-700"
|