@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "private": false,
5
5
  "sideEffects": false,
6
6
  "publishConfig": {
@@ -1,13 +1,40 @@
1
1
  'use client';
2
2
 
3
- import { useMemo, type CSSProperties } from 'react';
4
- import type { BookingFlowUiOptions } from './booking/booking-flow-ui';
5
- import type { BookingSourceMetadata } from '../lib/booking/source-metadata';
6
- import type { BookingAppMode, BookingAppPermissions, ManageParams } from '../contexts/BookingAppContext';
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; focusDate?: string }) => string;
45
+ getSuccessUrl?: (params: { reservationRef: string; lastName: string }) => string;
19
46
  showLanguageSelector?: boolean;
20
- flowUi?: BookingFlowUiOptions;
21
- bookingSourceAttribution?: Partial<BookingSourceMetadata>;
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
- showLanguageSelector = true,
104
+ permissions = {},
32
105
  googleMapsApiKey,
33
- bookingSourceAttribution,
106
+ onShowManage,
107
+ getSuccessUrl,
108
+ showLanguageSelector = true,
109
+ theme: themeOverride,
34
110
  }: BookingWidgetProps) {
35
- const src = useMemo(() => {
36
- // Route directly to customer booking (not partner portal root).
37
- const url = new URL('/offers', getBookingHost());
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
- const frameStyle: CSSProperties = {
52
- width: '100%',
53
- minHeight: '900px',
54
- border: 0,
55
- display: 'block',
56
- backgroundColor: 'transparent',
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
- <div className={className}>
61
- <iframe title="Customer booking" src={src} style={frameStyle} loading="lazy" />
62
- </div>
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