@ticketboothapp/booking 0.1.9 → 0.1.10
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 +238 -37
package/package.json
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import type
|
|
5
|
-
import
|
|
6
|
-
import
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { getProducts, type Product, setAuthToken } from '@/lib/api';
|
|
5
|
+
import { ProductList } from '@/components/ProductList';
|
|
6
|
+
import { BookingFlow } from '@/components/BookingFlow';
|
|
7
|
+
import { PrivateShuttleBookingFlow } from '@/components/PrivateShuttleBookingFlow';
|
|
8
|
+
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
|
|
9
|
+
import { CurrencySwitcher, useCurrency, type Currency } from '@/components/CurrencySwitcher';
|
|
10
|
+
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
|
11
|
+
import { CompanyProvider } from '@/contexts/CompanyContext';
|
|
12
|
+
import { BookingAppProvider, type BookingAppMode, type BookingAppPermissions, type ManageParams } from '@/contexts/BookingAppContext';
|
|
13
|
+
import { mergeTheme, themeToCssVars, type BookingTheme } from '@/lib/theme';
|
|
7
14
|
|
|
8
15
|
export interface BookingWidgetProps {
|
|
16
|
+
/**
|
|
17
|
+
* Optional product ID to start with a specific product selected
|
|
18
|
+
*/
|
|
9
19
|
initialProductId?: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Optional initial currency
|
|
23
|
+
*/
|
|
24
|
+
initialCurrency?: Currency;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether to show the header (default: true)
|
|
28
|
+
*/
|
|
29
|
+
showHeader?: boolean;
|
|
30
|
+
|
|
10
31
|
authToken?: string | null;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Callback when a product is selected
|
|
35
|
+
*/
|
|
36
|
+
onProductSelect?: (product: Product) => void;
|
|
37
|
+
|
|
11
38
|
onBookingSuccess?: (data: { reservationReference: string; sessionId?: string }) => void;
|
|
12
39
|
onBack?: () => void;
|
|
13
40
|
className?: string;
|
|
@@ -15,51 +42,225 @@ export interface BookingWidgetProps {
|
|
|
15
42
|
permissions?: Partial<BookingAppPermissions>;
|
|
16
43
|
googleMapsApiKey?: string;
|
|
17
44
|
onShowManage?: (params: ManageParams) => void;
|
|
18
|
-
getSuccessUrl?: (params: { reservationRef: string; lastName: string
|
|
45
|
+
getSuccessUrl?: (params: { reservationRef: string; lastName: string }) => string;
|
|
19
46
|
showLanguageSelector?: boolean;
|
|
20
|
-
|
|
21
|
-
|
|
47
|
+
theme?: Partial<BookingTheme>;
|
|
48
|
+
initialProduct?: Product;
|
|
49
|
+
products?: Product[];
|
|
50
|
+
companyId?: string;
|
|
51
|
+
initialBooking?: {
|
|
52
|
+
bookingReference: string;
|
|
53
|
+
productId: string;
|
|
54
|
+
availabilityId?: string;
|
|
55
|
+
dateTime: string;
|
|
56
|
+
originalTotalAmount?: number;
|
|
57
|
+
originalCurrency?: string;
|
|
58
|
+
bookingItems: Array<{ category: string; count: number }>;
|
|
59
|
+
returnAvailabilityId?: string | null;
|
|
60
|
+
pickupLocationId?: string | null;
|
|
61
|
+
travelerHotel?: string | null;
|
|
62
|
+
startTime?: string | null;
|
|
63
|
+
privateShuttleDetails?: { passengerCount?: number };
|
|
64
|
+
cancellationPolicyId?: string | null;
|
|
65
|
+
promoCode?: string | null;
|
|
66
|
+
additionalHoursCount?: number | null;
|
|
67
|
+
addOnSelections?: Array<{ addOnId: string; variantId?: string; quantity?: number }> | null;
|
|
68
|
+
};
|
|
69
|
+
onChangeBooking?: (data: {
|
|
70
|
+
productId: string;
|
|
71
|
+
dateTime: string;
|
|
72
|
+
bookingItems: Array<{ category: string; count: number }>;
|
|
73
|
+
returnAvailabilityId?: string | null;
|
|
74
|
+
pickupLocationId?: string | null;
|
|
75
|
+
travelerHotel?: string | null;
|
|
76
|
+
startTime?: string | null;
|
|
77
|
+
passengerCount?: number | null;
|
|
78
|
+
childSafetySeatsCount?: number | null;
|
|
79
|
+
foodRestrictions?: string | null;
|
|
80
|
+
addOnSelections?: Array<{ addOnId: string; variantId?: string; quantity?: number }> | null;
|
|
81
|
+
cancellationPolicyId?: string | null;
|
|
82
|
+
promoCode?: string | null;
|
|
83
|
+
newTotalAmount?: number;
|
|
84
|
+
keepOriginalPrice?: boolean;
|
|
85
|
+
additionalHoursCount?: number | null;
|
|
86
|
+
}) => Promise<void>;
|
|
22
87
|
}
|
|
23
88
|
|
|
24
|
-
const getBookingHost = (): string =>
|
|
25
|
-
process.env.NEXT_PUBLIC_BOOKING_WIDGET_URL || 'https://viaviamorainelake.com';
|
|
26
|
-
|
|
27
89
|
export function BookingWidget({
|
|
28
90
|
initialProductId,
|
|
91
|
+
initialProduct,
|
|
92
|
+
initialBooking,
|
|
93
|
+
onChangeBooking,
|
|
94
|
+
products: productsProp,
|
|
95
|
+
companyId,
|
|
96
|
+
initialCurrency = 'CAD',
|
|
97
|
+
showHeader = true,
|
|
98
|
+
authToken,
|
|
99
|
+
onProductSelect,
|
|
100
|
+
onBookingSuccess,
|
|
101
|
+
onBack,
|
|
29
102
|
className = '',
|
|
30
103
|
mode = 'standalone',
|
|
31
|
-
|
|
104
|
+
permissions = {},
|
|
32
105
|
googleMapsApiKey,
|
|
33
|
-
|
|
106
|
+
onShowManage,
|
|
107
|
+
getSuccessUrl,
|
|
108
|
+
showLanguageSelector = true,
|
|
109
|
+
theme: themeOverride,
|
|
34
110
|
}: BookingWidgetProps) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (initialProductId) url.searchParams.set('productId', initialProductId);
|
|
39
|
-
if (!showLanguageSelector) url.searchParams.set('showLanguageSelector', '0');
|
|
40
|
-
if (googleMapsApiKey) url.searchParams.set('gmapsKey', googleMapsApiKey);
|
|
41
|
-
if (mode) url.searchParams.set('embedMode', mode);
|
|
42
|
-
if (bookingSourceAttribution?.utmSource) url.searchParams.set('utm_source', bookingSourceAttribution.utmSource);
|
|
43
|
-
if (bookingSourceAttribution?.utmCampaign) url.searchParams.set('utm_campaign', bookingSourceAttribution.utmCampaign);
|
|
44
|
-
if (bookingSourceAttribution?.utmMedium) url.searchParams.set('utm_medium', bookingSourceAttribution.utmMedium);
|
|
45
|
-
if (bookingSourceAttribution?.partnerId) url.searchParams.set('partnerId', bookingSourceAttribution.partnerId);
|
|
46
|
-
if (bookingSourceAttribution?.agentId) url.searchParams.set('agentId', bookingSourceAttribution.agentId);
|
|
47
|
-
if (bookingSourceAttribution?.agentName) url.searchParams.set('agentName', bookingSourceAttribution.agentName);
|
|
48
|
-
return url.toString();
|
|
49
|
-
}, [bookingSourceAttribution, googleMapsApiKey, initialProductId, mode, showLanguageSelector]);
|
|
111
|
+
const { currency, setCurrency } = useCurrency();
|
|
112
|
+
const theme = mergeTheme(themeOverride);
|
|
113
|
+
const themeVars = themeToCssVars(theme);
|
|
50
114
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
setAuthToken(authToken || null);
|
|
117
|
+
return () => setAuthToken(null);
|
|
118
|
+
}, [authToken]);
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (initialCurrency) {
|
|
122
|
+
setCurrency(initialCurrency);
|
|
123
|
+
}
|
|
124
|
+
}, [initialCurrency, setCurrency]);
|
|
125
|
+
|
|
126
|
+
const isChangeMode = !!(initialProduct && initialBooking && onChangeBooking);
|
|
127
|
+
const [selectedProduct, setSelectedProduct] = useState<Product | null>(
|
|
128
|
+
() => (isChangeMode && initialProduct ? initialProduct : null),
|
|
129
|
+
);
|
|
130
|
+
const [products, setProducts] = useState<Product[]>(() =>
|
|
131
|
+
productsProp && productsProp.length > 0
|
|
132
|
+
? productsProp
|
|
133
|
+
: (isChangeMode && initialProduct ? [initialProduct] : []),
|
|
134
|
+
);
|
|
135
|
+
const [loadingProducts, setLoadingProducts] = useState(
|
|
136
|
+
!(productsProp && productsProp.length > 0) && !isChangeMode,
|
|
137
|
+
);
|
|
138
|
+
const [error, setError] = useState('');
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (isChangeMode) return;
|
|
142
|
+
if (initialProductId && products.length > 0) {
|
|
143
|
+
const product = products.find(
|
|
144
|
+
(p) => p.productId === initialProductId || p.options?.some((o) => o.optionId === initialProductId),
|
|
145
|
+
);
|
|
146
|
+
if (product) {
|
|
147
|
+
setSelectedProduct(product);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}, [isChangeMode, initialProductId, products]);
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (productsProp && productsProp.length > 0) {
|
|
154
|
+
setProducts(productsProp);
|
|
155
|
+
setLoadingProducts(false);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (isChangeMode) {
|
|
159
|
+
setLoadingProducts(false);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
async function fetchProducts() {
|
|
163
|
+
try {
|
|
164
|
+
const data = await getProducts();
|
|
165
|
+
setProducts(data);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
setError(err instanceof Error ? err.message : 'Failed to load products');
|
|
168
|
+
} finally {
|
|
169
|
+
setLoadingProducts(false);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
fetchProducts();
|
|
173
|
+
}, [isChangeMode, productsProp]);
|
|
174
|
+
|
|
175
|
+
const handleSelectProduct = useCallback((product: Product) => {
|
|
176
|
+
setSelectedProduct(product);
|
|
177
|
+
onProductSelect?.(product);
|
|
178
|
+
}, [onProductSelect]);
|
|
179
|
+
|
|
180
|
+
const handleBack = useCallback(() => {
|
|
181
|
+
setSelectedProduct(null);
|
|
182
|
+
onBack?.();
|
|
183
|
+
}, [onBack]);
|
|
184
|
+
|
|
185
|
+
const handleBookingSuccess = useCallback((data: { reservationReference: string; sessionId?: string }) => {
|
|
186
|
+
onBookingSuccess?.(data);
|
|
187
|
+
}, [onBookingSuccess]);
|
|
58
188
|
|
|
59
189
|
return (
|
|
60
|
-
<
|
|
61
|
-
<
|
|
62
|
-
|
|
190
|
+
<CompanyProvider companyId={companyId}>
|
|
191
|
+
<BookingAppProvider
|
|
192
|
+
mode={mode}
|
|
193
|
+
permissions={permissions}
|
|
194
|
+
googleMapsApiKey={googleMapsApiKey}
|
|
195
|
+
onShowManage={onShowManage}
|
|
196
|
+
getSuccessUrl={getSuccessUrl}
|
|
197
|
+
showLanguageSelector={showLanguageSelector}
|
|
198
|
+
>
|
|
199
|
+
<div className={`min-h-screen overflow-x-hidden ${className}`} style={{ ...themeVars, background: `linear-gradient(to bottom, var(--booking-bg), var(--booking-bg-end))` }}>
|
|
200
|
+
{showHeader && (
|
|
201
|
+
<header className="py-4 w-full overflow-hidden" style={{ backgroundColor: 'var(--booking-header-bg)', color: 'var(--booking-header-text)' }}>
|
|
202
|
+
<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">
|
|
203
|
+
<h1 className="text-xl font-semibold shrink-0" style={{ fontFamily: 'var(--booking-font-sans)' }}>Via Via Moraine Lake</h1>
|
|
204
|
+
<div className="flex flex-wrap items-center gap-2 sm:gap-4 min-w-0">
|
|
205
|
+
<CurrencySwitcher currency={currency} onCurrencyChange={setCurrency} />
|
|
206
|
+
{showLanguageSelector && <LanguageSwitcher />}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</header>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{!showHeader && (
|
|
213
|
+
<div className="border-b px-4 py-2 flex flex-wrap items-center justify-end gap-2 min-w-0" style={{ borderColor: 'var(--booking-border)', backgroundColor: 'var(--booking-surface)' }}>
|
|
214
|
+
<CurrencySwitcher currency={currency} onCurrencyChange={setCurrency} />
|
|
215
|
+
{showLanguageSelector && <LanguageSwitcher />}
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
<main className={`max-w-4xl mx-auto px-4 py-8 ${!showHeader ? 'pt-4' : ''}`} style={{ fontFamily: 'var(--booking-font-sans)' }}>
|
|
220
|
+
<div className="overflow-visible p-8" style={{ backgroundColor: 'var(--booking-surface)', borderRadius: 'var(--booking-radius)', boxShadow: '0 25px 50px -12px rgba(0,0,0,0.25)' }}>
|
|
221
|
+
{loadingProducts ? (
|
|
222
|
+
<div className="flex items-center justify-center py-16">
|
|
223
|
+
<div style={{ color: 'var(--booking-text-muted)' }}>Loading experiences...</div>
|
|
224
|
+
</div>
|
|
225
|
+
) : error ? (
|
|
226
|
+
<div className="p-4 rounded-lg" style={{ backgroundColor: 'var(--booking-error-bg)', borderWidth: '1px', borderStyle: 'solid', borderColor: 'var(--booking-error-border)', color: 'var(--booking-error-text)' }}>
|
|
227
|
+
{error}
|
|
228
|
+
</div>
|
|
229
|
+
) : !selectedProduct ? (
|
|
230
|
+
<div>
|
|
231
|
+
<h2 className="text-2xl font-bold mb-6" style={{ color: 'var(--booking-text)' }}>
|
|
232
|
+
Choose Your Experience
|
|
233
|
+
</h2>
|
|
234
|
+
<ProductList products={products} onSelect={handleSelectProduct} currency={currency} />
|
|
235
|
+
</div>
|
|
236
|
+
) : (
|
|
237
|
+
<ErrorBoundary>
|
|
238
|
+
{selectedProduct.productType === 'PRIVATE_SHUTTLE' ? (
|
|
239
|
+
<PrivateShuttleBookingFlow
|
|
240
|
+
product={selectedProduct}
|
|
241
|
+
onBack={handleBack}
|
|
242
|
+
currency={currency}
|
|
243
|
+
onSuccess={handleBookingSuccess}
|
|
244
|
+
initialBooking={isChangeMode ? initialBooking : undefined}
|
|
245
|
+
onChangeBooking={isChangeMode ? onChangeBooking : undefined}
|
|
246
|
+
/>
|
|
247
|
+
) : (
|
|
248
|
+
<BookingFlow
|
|
249
|
+
product={selectedProduct}
|
|
250
|
+
onBack={handleBack}
|
|
251
|
+
currency={currency}
|
|
252
|
+
onSuccess={handleBookingSuccess}
|
|
253
|
+
initialBooking={isChangeMode ? initialBooking : undefined}
|
|
254
|
+
onChangeBooking={isChangeMode ? onChangeBooking : undefined}
|
|
255
|
+
/>
|
|
256
|
+
)}
|
|
257
|
+
</ErrorBoundary>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
</main>
|
|
261
|
+
</div>
|
|
262
|
+
</BookingAppProvider>
|
|
263
|
+
</CompanyProvider>
|
|
63
264
|
);
|
|
64
265
|
}
|
|
65
266
|
|