@bailierich/booking-components 2.0.0
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/README.md +319 -0
- package/TENANT_DATA_INTEGRATION.md +402 -0
- package/TENANT_SETUP.md +316 -0
- package/components/BookingFlow/BookingFlow.tsx +790 -0
- package/components/BookingFlow/index.ts +5 -0
- package/components/BookingFlow/steps/AddonsSelection.tsx +118 -0
- package/components/BookingFlow/steps/Confirmation.tsx +185 -0
- package/components/BookingFlow/steps/ContactForm.tsx +292 -0
- package/components/BookingFlow/steps/CycleAwareDateSelection.tsx +277 -0
- package/components/BookingFlow/steps/DateSelection.tsx +473 -0
- package/components/BookingFlow/steps/ServiceSelection.tsx +315 -0
- package/components/BookingFlow/steps/TimeSelection.tsx +230 -0
- package/components/BookingFlow/steps/index.ts +10 -0
- package/components/BottomSheet/index.tsx +120 -0
- package/components/Forms/FormBlock.tsx +283 -0
- package/components/Forms/FormField.tsx +385 -0
- package/components/Forms/FormRenderer.tsx +216 -0
- package/components/Forms/FormValidation.ts +122 -0
- package/components/Forms/index.ts +4 -0
- package/components/HoldTimer/HoldTimer.tsx +266 -0
- package/components/HoldTimer/index.ts +2 -0
- package/components/SectionRenderer.tsx +558 -0
- package/components/Sections/About.tsx +145 -0
- package/components/Sections/BeforeAfter.tsx +81 -0
- package/components/Sections/BookingSection.tsx +76 -0
- package/components/Sections/Contact.tsx +103 -0
- package/components/Sections/FAQSection.tsx +239 -0
- package/components/Sections/FeatureContent.tsx +113 -0
- package/components/Sections/FeaturedLink.tsx +103 -0
- package/components/Sections/FixedInfoCard.tsx +189 -0
- package/components/Sections/Gallery.tsx +83 -0
- package/components/Sections/Header.tsx +78 -0
- package/components/Sections/Hero.tsx +178 -0
- package/components/Sections/ImageSection.tsx +147 -0
- package/components/Sections/InstagramFeed.tsx +38 -0
- package/components/Sections/LinkList.tsx +76 -0
- package/components/Sections/LocationMap.tsx +202 -0
- package/components/Sections/Logo.tsx +61 -0
- package/components/Sections/MinimalFooter.tsx +78 -0
- package/components/Sections/MinimalHeader.tsx +81 -0
- package/components/Sections/MinimalNavigation.tsx +63 -0
- package/components/Sections/Navbar.tsx +258 -0
- package/components/Sections/PricingTable.tsx +106 -0
- package/components/Sections/ScrollingTextDivider.tsx +138 -0
- package/components/Sections/ScrollingTextDivider.tsx.bak +138 -0
- package/components/Sections/ServicesPreview.tsx +129 -0
- package/components/Sections/SocialBar.tsx +177 -0
- package/components/Sections/Team.tsx +80 -0
- package/components/Sections/Testimonials.tsx +92 -0
- package/components/Sections/TextSection.tsx +116 -0
- package/components/Sections/VideoSection.tsx +178 -0
- package/components/Sections/index.ts +57 -0
- package/components/index.ts +21 -0
- package/dist/index-DAai7Glf.d.mts +474 -0
- package/dist/index-DAai7Glf.d.ts +474 -0
- package/dist/index.d.mts +1075 -0
- package/dist/index.d.ts +1075 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +22 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles/index.d.mts +1 -0
- package/dist/styles/index.d.ts +1 -0
- package/dist/styles/index.js +2 -0
- package/dist/styles/index.js.map +1 -0
- package/dist/styles/index.mjs +2 -0
- package/dist/styles/index.mjs.map +1 -0
- package/docs/API.md +849 -0
- package/docs/CALLBACKS.md +760 -0
- package/docs/COMPLETE_SESSION_SUMMARY.md +404 -0
- package/docs/DATA_SHAPES.md +684 -0
- package/docs/MIGRATION.md +662 -0
- package/docs/PAYMENT_INTEGRATION.md +766 -0
- package/docs/SESSION_SUMMARY.md +185 -0
- package/docs/STYLING.md +735 -0
- package/index.ts +4 -0
- package/lib/storage.ts +239 -0
- package/package.json +59 -0
- package/styles/animations.ts +210 -0
- package/styles/index.ts +1 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +13 -0
- package/types/index.ts +369 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
# Callback Patterns & Event Handling
|
|
2
|
+
|
|
3
|
+
Guide to implementing callbacks, event handlers, and data flow in `@oviah/booking-components`.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [BookingFlow Callbacks](#bookingflow-callbacks)
|
|
8
|
+
- [Step Component Callbacks](#step-component-callbacks)
|
|
9
|
+
- [Section Interactions](#section-interactions)
|
|
10
|
+
- [Payment Callbacks](#payment-callbacks)
|
|
11
|
+
- [Error Handling](#error-handling)
|
|
12
|
+
- [Best Practices](#best-practices)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## BookingFlow Callbacks
|
|
17
|
+
|
|
18
|
+
### onComplete
|
|
19
|
+
|
|
20
|
+
Called when the booking flow is completed and the user confirms their booking.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
interface BookingData {
|
|
24
|
+
service: string | null;
|
|
25
|
+
date: string | null;
|
|
26
|
+
time: string | null;
|
|
27
|
+
addons: string[];
|
|
28
|
+
contact: ContactInfo;
|
|
29
|
+
optIn?: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handleComplete = async (bookingData: BookingData) => {
|
|
33
|
+
try {
|
|
34
|
+
// 1. Validate data
|
|
35
|
+
if (!bookingData.service || !bookingData.date || !bookingData.time) {
|
|
36
|
+
throw new Error('Missing required booking information');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Submit to backend
|
|
40
|
+
const response = await fetch('/api/bookings', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify(bookingData)
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error('Failed to create booking');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const booking = await response.json();
|
|
51
|
+
|
|
52
|
+
// 3. Show success message
|
|
53
|
+
toast.success('Booking confirmed!');
|
|
54
|
+
|
|
55
|
+
// 4. Redirect to confirmation page
|
|
56
|
+
router.push(`/booking/confirmation/${booking.id}`);
|
|
57
|
+
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('Booking error:', error);
|
|
60
|
+
toast.error('Failed to complete booking. Please try again.');
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
<BookingFlow
|
|
65
|
+
config={config}
|
|
66
|
+
colors={colors}
|
|
67
|
+
onComplete={handleComplete}
|
|
68
|
+
/>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### onStepChange
|
|
74
|
+
|
|
75
|
+
Called whenever the user navigates between steps.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const handleStepChange = (stepIndex: number, stepId: string) => {
|
|
79
|
+
// Track analytics
|
|
80
|
+
analytics.track('Booking Step Changed', {
|
|
81
|
+
stepIndex,
|
|
82
|
+
stepId,
|
|
83
|
+
timestamp: new Date().toISOString()
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Update URL (optional)
|
|
87
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
88
|
+
searchParams.set('step', stepId);
|
|
89
|
+
router.replace(`?${searchParams.toString()}`, { scroll: false });
|
|
90
|
+
|
|
91
|
+
// Save progress (optional)
|
|
92
|
+
localStorage.setItem('bookingProgress', JSON.stringify({
|
|
93
|
+
currentStep: stepIndex,
|
|
94
|
+
timestamp: Date.now()
|
|
95
|
+
}));
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
<BookingFlow
|
|
99
|
+
config={config}
|
|
100
|
+
colors={colors}
|
|
101
|
+
onStepChange={handleStepChange}
|
|
102
|
+
/>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### validateStep
|
|
108
|
+
|
|
109
|
+
Custom validation for each step before allowing navigation to the next step.
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const validateStep = (stepId: string, data: any): boolean => {
|
|
113
|
+
switch (stepId) {
|
|
114
|
+
case 'service':
|
|
115
|
+
// Ensure service is selected
|
|
116
|
+
if (!data.service) {
|
|
117
|
+
toast.error('Please select a service');
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
|
|
122
|
+
case 'date':
|
|
123
|
+
// Ensure date is in the future
|
|
124
|
+
if (!data.date) {
|
|
125
|
+
toast.error('Please select a date');
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const selectedDate = new Date(data.date);
|
|
129
|
+
if (selectedDate < new Date()) {
|
|
130
|
+
toast.error('Please select a future date');
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
|
|
135
|
+
case 'time':
|
|
136
|
+
// Ensure time is selected
|
|
137
|
+
if (!data.time) {
|
|
138
|
+
toast.error('Please select a time slot');
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
|
|
143
|
+
case 'details':
|
|
144
|
+
// Validate contact information
|
|
145
|
+
const { name, email, phone } = data.contact || {};
|
|
146
|
+
|
|
147
|
+
if (!name || name.trim().length < 2) {
|
|
148
|
+
toast.error('Please enter your name');
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!email || !isValidEmail(email)) {
|
|
153
|
+
toast.error('Please enter a valid email address');
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!phone || !isValidPhone(phone)) {
|
|
158
|
+
toast.error('Please enter a valid phone number');
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return true;
|
|
163
|
+
|
|
164
|
+
default:
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
<BookingFlow
|
|
170
|
+
config={config}
|
|
171
|
+
colors={colors}
|
|
172
|
+
validateStep={validateStep}
|
|
173
|
+
/>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Step Component Callbacks
|
|
179
|
+
|
|
180
|
+
### Service Selection
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { ServiceSelection } from '@oviah/booking-components';
|
|
184
|
+
|
|
185
|
+
const [selectedService, setSelectedService] = useState<string | null>(null);
|
|
186
|
+
|
|
187
|
+
const handleServiceSelect = (serviceId: string) => {
|
|
188
|
+
// Update state
|
|
189
|
+
setSelectedService(serviceId);
|
|
190
|
+
|
|
191
|
+
// Track analytics
|
|
192
|
+
analytics.track('Service Selected', {
|
|
193
|
+
serviceId,
|
|
194
|
+
timestamp: new Date().toISOString()
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Fetch available dates for this service (optional)
|
|
198
|
+
fetchAvailableDates(serviceId).then(dates => {
|
|
199
|
+
setAvailableDates(dates);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Update pricing display (optional)
|
|
203
|
+
const service = services.find(s => s.id === serviceId);
|
|
204
|
+
if (service) {
|
|
205
|
+
setEstimatedPrice(service.price);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
<ServiceSelection
|
|
210
|
+
services={services}
|
|
211
|
+
categories={categories}
|
|
212
|
+
selectedService={selectedService}
|
|
213
|
+
onServiceSelect={handleServiceSelect}
|
|
214
|
+
settings={settings}
|
|
215
|
+
colors={colors}
|
|
216
|
+
/>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
### Date Selection
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import { DateSelection } from '@oviah/booking-components';
|
|
225
|
+
|
|
226
|
+
const [selectedDate, setSelectedDate] = useState<string | null>(null);
|
|
227
|
+
const [availableTimes, setAvailableTimes] = useState<string[]>([]);
|
|
228
|
+
|
|
229
|
+
const handleDateSelect = async (date: string) => {
|
|
230
|
+
// Update state
|
|
231
|
+
setSelectedDate(date);
|
|
232
|
+
|
|
233
|
+
// Fetch available time slots for this date
|
|
234
|
+
try {
|
|
235
|
+
const response = await fetch(`/api/availability?date=${date}&service=${selectedService}`);
|
|
236
|
+
const data = await response.json();
|
|
237
|
+
setAvailableTimes(data.times);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error('Failed to fetch time slots:', error);
|
|
240
|
+
toast.error('Failed to load available times');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Track analytics
|
|
244
|
+
analytics.track('Date Selected', { date });
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
<DateSelection
|
|
248
|
+
availableDates={availableDates}
|
|
249
|
+
selectedDate={selectedDate}
|
|
250
|
+
onDateSelect={handleDateSelect}
|
|
251
|
+
settings={settings}
|
|
252
|
+
colors={colors}
|
|
253
|
+
/>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### Time Selection
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { TimeSelection } from '@oviah/booking-components';
|
|
262
|
+
|
|
263
|
+
const [selectedTime, setSelectedTime] = useState<string | null>(null);
|
|
264
|
+
|
|
265
|
+
const handleTimeSelect = (time: string) => {
|
|
266
|
+
// Update state
|
|
267
|
+
setSelectedTime(time);
|
|
268
|
+
|
|
269
|
+
// Calculate end time
|
|
270
|
+
const serviceDuration = selectedServiceData?.duration || 60;
|
|
271
|
+
const endTime = calculateEndTime(time, serviceDuration);
|
|
272
|
+
setEstimatedEndTime(endTime);
|
|
273
|
+
|
|
274
|
+
// Track analytics
|
|
275
|
+
analytics.track('Time Selected', {
|
|
276
|
+
time,
|
|
277
|
+
endTime,
|
|
278
|
+
duration: serviceDuration
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
<TimeSelection
|
|
283
|
+
availableTimes={availableTimes}
|
|
284
|
+
selectedTime={selectedTime}
|
|
285
|
+
onTimeSelect={handleTimeSelect}
|
|
286
|
+
serviceDuration={selectedServiceData?.duration}
|
|
287
|
+
settings={settings}
|
|
288
|
+
colors={colors}
|
|
289
|
+
/>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
### Addons Selection
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { AddonsSelection } from '@oviah/booking-components';
|
|
298
|
+
|
|
299
|
+
const [selectedAddons, setSelectedAddons] = useState<string[]>([]);
|
|
300
|
+
const [totalPrice, setTotalPrice] = useState(0);
|
|
301
|
+
|
|
302
|
+
const handleAddonsChange = (addonIds: string[]) => {
|
|
303
|
+
// Update state
|
|
304
|
+
setSelectedAddons(addonIds);
|
|
305
|
+
|
|
306
|
+
// Calculate total price
|
|
307
|
+
const basePrice = selectedServiceData?.price || 0;
|
|
308
|
+
const addonsPrice = addonIds.reduce((sum, id) => {
|
|
309
|
+
const addon = addons.find(a => a.id === id);
|
|
310
|
+
return sum + (addon?.price || 0);
|
|
311
|
+
}, 0);
|
|
312
|
+
const total = basePrice + addonsPrice;
|
|
313
|
+
setTotalPrice(total);
|
|
314
|
+
|
|
315
|
+
// Calculate total duration
|
|
316
|
+
const baseDuration = selectedServiceData?.duration || 0;
|
|
317
|
+
const addonsDuration = addonIds.reduce((sum, id) => {
|
|
318
|
+
const addon = addons.find(a => a.id === id);
|
|
319
|
+
return sum + (addon?.duration || 0);
|
|
320
|
+
}, 0);
|
|
321
|
+
const totalDuration = baseDuration + addonsDuration;
|
|
322
|
+
setEstimatedDuration(totalDuration);
|
|
323
|
+
|
|
324
|
+
// Track analytics
|
|
325
|
+
analytics.track('Addons Changed', {
|
|
326
|
+
addonIds,
|
|
327
|
+
totalPrice: total,
|
|
328
|
+
totalDuration
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
<AddonsSelection
|
|
333
|
+
addons={addons}
|
|
334
|
+
selectedAddons={selectedAddons}
|
|
335
|
+
onAddonsChange={handleAddonsChange}
|
|
336
|
+
settings={settings}
|
|
337
|
+
colors={colors}
|
|
338
|
+
/>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
### Contact Form
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { ContactForm } from '@oviah/booking-components';
|
|
347
|
+
|
|
348
|
+
const [contactInfo, setContactInfo] = useState({
|
|
349
|
+
name: '',
|
|
350
|
+
email: '',
|
|
351
|
+
phone: '',
|
|
352
|
+
notes: ''
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const [optInData, setOptInData] = useState<any>({});
|
|
356
|
+
|
|
357
|
+
const handleContactInfoChange = (info: any) => {
|
|
358
|
+
// Update state
|
|
359
|
+
setContactInfo(info);
|
|
360
|
+
|
|
361
|
+
// Validate in real-time (optional)
|
|
362
|
+
validateContactInfo(info);
|
|
363
|
+
|
|
364
|
+
// Save to localStorage (optional)
|
|
365
|
+
localStorage.setItem('bookingContact', JSON.stringify(info));
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const handleOptInData = (data: any) => {
|
|
369
|
+
// Store opt-in module data
|
|
370
|
+
setOptInData(data);
|
|
371
|
+
|
|
372
|
+
// Track that user opted in
|
|
373
|
+
analytics.track('Opt-In Completed', {
|
|
374
|
+
moduleId: data.moduleId,
|
|
375
|
+
timestamp: new Date().toISOString()
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
<ContactForm
|
|
380
|
+
contactInfo={contactInfo}
|
|
381
|
+
onContactInfoChange={handleContactInfoChange}
|
|
382
|
+
optInModules={optInModules}
|
|
383
|
+
onOptInData={handleOptInData}
|
|
384
|
+
settings={settings}
|
|
385
|
+
colors={colors}
|
|
386
|
+
/>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
### Confirmation
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import { Confirmation } from '@oviah/booking-components';
|
|
395
|
+
|
|
396
|
+
const handleConfirm = async () => {
|
|
397
|
+
try {
|
|
398
|
+
// 1. Create booking
|
|
399
|
+
const bookingResponse = await fetch('/api/bookings', {
|
|
400
|
+
method: 'POST',
|
|
401
|
+
headers: { 'Content-Type': 'application/json' },
|
|
402
|
+
body: JSON.stringify({
|
|
403
|
+
serviceId: selectedService,
|
|
404
|
+
date: selectedDate,
|
|
405
|
+
time: selectedTime,
|
|
406
|
+
addonIds: selectedAddons,
|
|
407
|
+
contact: contactInfo,
|
|
408
|
+
optInData
|
|
409
|
+
})
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (!bookingResponse.ok) {
|
|
413
|
+
throw new Error('Failed to create booking');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const booking = await bookingResponse.json();
|
|
417
|
+
|
|
418
|
+
// 2. Process payment
|
|
419
|
+
const paymentResponse = await fetch('/api/payments/process', {
|
|
420
|
+
method: 'POST',
|
|
421
|
+
headers: { 'Content-Type': 'application/json' },
|
|
422
|
+
body: JSON.stringify({
|
|
423
|
+
bookingId: booking.id,
|
|
424
|
+
amount: depositAmount,
|
|
425
|
+
paymentMethod: paymentMethodId
|
|
426
|
+
})
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (!paymentResponse.ok) {
|
|
430
|
+
// Rollback booking
|
|
431
|
+
await fetch(`/api/bookings/${booking.id}`, { method: 'DELETE' });
|
|
432
|
+
throw new Error('Payment failed');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const payment = await paymentResponse.json();
|
|
436
|
+
|
|
437
|
+
// 3. Send confirmation email
|
|
438
|
+
await fetch('/api/emails/booking-confirmation', {
|
|
439
|
+
method: 'POST',
|
|
440
|
+
headers: { 'Content-Type': 'application/json' },
|
|
441
|
+
body: JSON.stringify({
|
|
442
|
+
bookingId: booking.id,
|
|
443
|
+
email: contactInfo.email
|
|
444
|
+
})
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// 4. Track conversion
|
|
448
|
+
analytics.track('Booking Completed', {
|
|
449
|
+
bookingId: booking.id,
|
|
450
|
+
paymentId: payment.id,
|
|
451
|
+
amount: depositAmount,
|
|
452
|
+
revenue: totalPrice
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// 5. Redirect to success page
|
|
456
|
+
router.push(`/booking/success?id=${booking.id}`);
|
|
457
|
+
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error('Confirmation error:', error);
|
|
460
|
+
toast.error('Failed to complete booking. Please try again.');
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
<Confirmation
|
|
465
|
+
service={selectedServiceData}
|
|
466
|
+
addons={selectedAddonsData}
|
|
467
|
+
selectedDate={selectedDate}
|
|
468
|
+
selectedTime={selectedTime}
|
|
469
|
+
contactInfo={contactInfo}
|
|
470
|
+
paymentProvider="square"
|
|
471
|
+
paymentConfig={{ depositPercentage: 20 }}
|
|
472
|
+
onConfirm={handleConfirm}
|
|
473
|
+
colors={colors}
|
|
474
|
+
/>
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Section Interactions
|
|
480
|
+
|
|
481
|
+
Most section components are presentational, but some have interactive elements:
|
|
482
|
+
|
|
483
|
+
### Navigation Links
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
const handleNavClick = (url: string, linkType: 'internal' | 'external') => {
|
|
487
|
+
// Track click
|
|
488
|
+
analytics.track('Navigation Link Clicked', { url, linkType });
|
|
489
|
+
|
|
490
|
+
// Handle internal navigation
|
|
491
|
+
if (linkType === 'internal') {
|
|
492
|
+
router.push(url);
|
|
493
|
+
}
|
|
494
|
+
// External links handled automatically by anchor tag
|
|
495
|
+
};
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### CTA Buttons
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
const handleCTAClick = (buttonText: string, url: string) => {
|
|
502
|
+
// Track conversion opportunity
|
|
503
|
+
analytics.track('CTA Clicked', { buttonText, url });
|
|
504
|
+
|
|
505
|
+
// Custom handling (e.g., open booking modal)
|
|
506
|
+
if (url === '#booking') {
|
|
507
|
+
openBookingModal();
|
|
508
|
+
} else {
|
|
509
|
+
router.push(url);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Payment Callbacks
|
|
517
|
+
|
|
518
|
+
### Square Payment
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
const handleSquarePayment = async (paymentMethodId: string) => {
|
|
522
|
+
try {
|
|
523
|
+
const response = await fetch('/api/payments/square', {
|
|
524
|
+
method: 'POST',
|
|
525
|
+
headers: { 'Content-Type': 'application/json' },
|
|
526
|
+
body: JSON.stringify({
|
|
527
|
+
paymentMethodId,
|
|
528
|
+
amount: depositAmount,
|
|
529
|
+
bookingId: booking.id
|
|
530
|
+
})
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
if (!response.ok) {
|
|
534
|
+
const error = await response.json();
|
|
535
|
+
throw new Error(error.message || 'Payment failed');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const result = await response.json();
|
|
539
|
+
return result;
|
|
540
|
+
|
|
541
|
+
} catch (error) {
|
|
542
|
+
console.error('Square payment error:', error);
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Stripe Payment
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
const handleStripePayment = async (paymentIntentId: string) => {
|
|
552
|
+
try {
|
|
553
|
+
const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY!);
|
|
554
|
+
|
|
555
|
+
const result = await stripe.confirmCardPayment(paymentIntentId, {
|
|
556
|
+
payment_method: {
|
|
557
|
+
card: cardElement,
|
|
558
|
+
billing_details: {
|
|
559
|
+
name: contactInfo.name,
|
|
560
|
+
email: contactInfo.email,
|
|
561
|
+
phone: contactInfo.phone
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
if (result.error) {
|
|
567
|
+
throw new Error(result.error.message);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return result.paymentIntent;
|
|
571
|
+
|
|
572
|
+
} catch (error) {
|
|
573
|
+
console.error('Stripe payment error:', error);
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## Error Handling
|
|
582
|
+
|
|
583
|
+
### Global Error Handler
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
const handleBookingError = (error: Error, context: string) => {
|
|
587
|
+
// Log error
|
|
588
|
+
console.error(`Booking error in ${context}:`, error);
|
|
589
|
+
|
|
590
|
+
// Track error
|
|
591
|
+
analytics.track('Booking Error', {
|
|
592
|
+
context,
|
|
593
|
+
error: error.message,
|
|
594
|
+
stack: error.stack
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Show user-friendly message
|
|
598
|
+
const userMessage = getUserFriendlyError(error);
|
|
599
|
+
toast.error(userMessage);
|
|
600
|
+
|
|
601
|
+
// Optional: Send to error tracking service
|
|
602
|
+
if (typeof window !== 'undefined' && window.Sentry) {
|
|
603
|
+
window.Sentry.captureException(error, {
|
|
604
|
+
tags: { context },
|
|
605
|
+
extra: { bookingData: getCurrentBookingData() }
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const getUserFriendlyError = (error: Error): string => {
|
|
611
|
+
if (error.message.includes('network')) {
|
|
612
|
+
return 'Network error. Please check your connection and try again.';
|
|
613
|
+
}
|
|
614
|
+
if (error.message.includes('payment')) {
|
|
615
|
+
return 'Payment failed. Please check your payment details.';
|
|
616
|
+
}
|
|
617
|
+
return 'Something went wrong. Please try again or contact support.';
|
|
618
|
+
};
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### Step-Specific Error Handling
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
const validateStepWithErrors = (stepId: string, data: any): {
|
|
625
|
+
valid: boolean;
|
|
626
|
+
errors: string[]
|
|
627
|
+
} => {
|
|
628
|
+
const errors: string[] = [];
|
|
629
|
+
|
|
630
|
+
switch (stepId) {
|
|
631
|
+
case 'service':
|
|
632
|
+
if (!data.service) errors.push('Please select a service');
|
|
633
|
+
break;
|
|
634
|
+
|
|
635
|
+
case 'date':
|
|
636
|
+
if (!data.date) {
|
|
637
|
+
errors.push('Please select a date');
|
|
638
|
+
} else if (new Date(data.date) < new Date()) {
|
|
639
|
+
errors.push('Selected date must be in the future');
|
|
640
|
+
}
|
|
641
|
+
break;
|
|
642
|
+
|
|
643
|
+
case 'time':
|
|
644
|
+
if (!data.time) errors.push('Please select a time slot');
|
|
645
|
+
break;
|
|
646
|
+
|
|
647
|
+
case 'details':
|
|
648
|
+
const { name, email, phone } = data.contact || {};
|
|
649
|
+
if (!name) errors.push('Name is required');
|
|
650
|
+
if (!email) errors.push('Email is required');
|
|
651
|
+
else if (!isValidEmail(email)) errors.push('Email is invalid');
|
|
652
|
+
if (!phone) errors.push('Phone number is required');
|
|
653
|
+
else if (!isValidPhone(phone)) errors.push('Phone number is invalid');
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
valid: errors.length === 0,
|
|
659
|
+
errors
|
|
660
|
+
};
|
|
661
|
+
};
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## Best Practices
|
|
667
|
+
|
|
668
|
+
### 1. Always Handle Errors
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
// ❌ Bad
|
|
672
|
+
const handleComplete = async (data: BookingData) => {
|
|
673
|
+
const response = await fetch('/api/bookings', { ... });
|
|
674
|
+
const booking = await response.json();
|
|
675
|
+
router.push(`/confirmation/${booking.id}`);
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// ✅ Good
|
|
679
|
+
const handleComplete = async (data: BookingData) => {
|
|
680
|
+
try {
|
|
681
|
+
const response = await fetch('/api/bookings', { ... });
|
|
682
|
+
if (!response.ok) throw new Error('Failed to create booking');
|
|
683
|
+
|
|
684
|
+
const booking = await response.json();
|
|
685
|
+
router.push(`/confirmation/${booking.id}`);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
handleBookingError(error, 'complete');
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### 2. Provide User Feedback
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
// ✅ Show loading states
|
|
696
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
697
|
+
|
|
698
|
+
const handleSubmit = async () => {
|
|
699
|
+
setIsSubmitting(true);
|
|
700
|
+
try {
|
|
701
|
+
await submitBooking();
|
|
702
|
+
toast.success('Booking confirmed!');
|
|
703
|
+
} catch (error) {
|
|
704
|
+
toast.error('Failed to book');
|
|
705
|
+
} finally {
|
|
706
|
+
setIsSubmitting(false);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### 3. Track Analytics
|
|
712
|
+
|
|
713
|
+
```typescript
|
|
714
|
+
// ✅ Track important events
|
|
715
|
+
const handleStepChange = (index: number, stepId: string) => {
|
|
716
|
+
analytics.track('Booking Step Changed', {
|
|
717
|
+
step: stepId,
|
|
718
|
+
stepIndex: index,
|
|
719
|
+
timestamp: new Date().toISOString()
|
|
720
|
+
});
|
|
721
|
+
};
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
### 4. Validate Early and Often
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
// ✅ Real-time validation
|
|
728
|
+
const handleContactInfoChange = (info: ContactInfo) => {
|
|
729
|
+
setContactInfo(info);
|
|
730
|
+
|
|
731
|
+
// Validate as user types
|
|
732
|
+
const errors = validateContactInfo(info);
|
|
733
|
+
setValidationErrors(errors);
|
|
734
|
+
};
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### 5. Save Progress
|
|
738
|
+
|
|
739
|
+
```typescript
|
|
740
|
+
// ✅ Auto-save booking progress
|
|
741
|
+
useEffect(() => {
|
|
742
|
+
const progress = {
|
|
743
|
+
service: selectedService,
|
|
744
|
+
date: selectedDate,
|
|
745
|
+
time: selectedTime,
|
|
746
|
+
addons: selectedAddons,
|
|
747
|
+
contact: contactInfo
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
localStorage.setItem('bookingProgress', JSON.stringify(progress));
|
|
751
|
+
}, [selectedService, selectedDate, selectedTime, selectedAddons, contactInfo]);
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
## See Also
|
|
757
|
+
|
|
758
|
+
- [DATA_SHAPES.md](./DATA_SHAPES.md) - Data structures reference
|
|
759
|
+
- [API.md](./API.md) - Component API reference
|
|
760
|
+
- [PAYMENT_INTEGRATION.md](./PAYMENT_INTEGRATION.md) - Payment setup guide
|