@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.
Files changed (83) hide show
  1. package/README.md +319 -0
  2. package/TENANT_DATA_INTEGRATION.md +402 -0
  3. package/TENANT_SETUP.md +316 -0
  4. package/components/BookingFlow/BookingFlow.tsx +790 -0
  5. package/components/BookingFlow/index.ts +5 -0
  6. package/components/BookingFlow/steps/AddonsSelection.tsx +118 -0
  7. package/components/BookingFlow/steps/Confirmation.tsx +185 -0
  8. package/components/BookingFlow/steps/ContactForm.tsx +292 -0
  9. package/components/BookingFlow/steps/CycleAwareDateSelection.tsx +277 -0
  10. package/components/BookingFlow/steps/DateSelection.tsx +473 -0
  11. package/components/BookingFlow/steps/ServiceSelection.tsx +315 -0
  12. package/components/BookingFlow/steps/TimeSelection.tsx +230 -0
  13. package/components/BookingFlow/steps/index.ts +10 -0
  14. package/components/BottomSheet/index.tsx +120 -0
  15. package/components/Forms/FormBlock.tsx +283 -0
  16. package/components/Forms/FormField.tsx +385 -0
  17. package/components/Forms/FormRenderer.tsx +216 -0
  18. package/components/Forms/FormValidation.ts +122 -0
  19. package/components/Forms/index.ts +4 -0
  20. package/components/HoldTimer/HoldTimer.tsx +266 -0
  21. package/components/HoldTimer/index.ts +2 -0
  22. package/components/SectionRenderer.tsx +558 -0
  23. package/components/Sections/About.tsx +145 -0
  24. package/components/Sections/BeforeAfter.tsx +81 -0
  25. package/components/Sections/BookingSection.tsx +76 -0
  26. package/components/Sections/Contact.tsx +103 -0
  27. package/components/Sections/FAQSection.tsx +239 -0
  28. package/components/Sections/FeatureContent.tsx +113 -0
  29. package/components/Sections/FeaturedLink.tsx +103 -0
  30. package/components/Sections/FixedInfoCard.tsx +189 -0
  31. package/components/Sections/Gallery.tsx +83 -0
  32. package/components/Sections/Header.tsx +78 -0
  33. package/components/Sections/Hero.tsx +178 -0
  34. package/components/Sections/ImageSection.tsx +147 -0
  35. package/components/Sections/InstagramFeed.tsx +38 -0
  36. package/components/Sections/LinkList.tsx +76 -0
  37. package/components/Sections/LocationMap.tsx +202 -0
  38. package/components/Sections/Logo.tsx +61 -0
  39. package/components/Sections/MinimalFooter.tsx +78 -0
  40. package/components/Sections/MinimalHeader.tsx +81 -0
  41. package/components/Sections/MinimalNavigation.tsx +63 -0
  42. package/components/Sections/Navbar.tsx +258 -0
  43. package/components/Sections/PricingTable.tsx +106 -0
  44. package/components/Sections/ScrollingTextDivider.tsx +138 -0
  45. package/components/Sections/ScrollingTextDivider.tsx.bak +138 -0
  46. package/components/Sections/ServicesPreview.tsx +129 -0
  47. package/components/Sections/SocialBar.tsx +177 -0
  48. package/components/Sections/Team.tsx +80 -0
  49. package/components/Sections/Testimonials.tsx +92 -0
  50. package/components/Sections/TextSection.tsx +116 -0
  51. package/components/Sections/VideoSection.tsx +178 -0
  52. package/components/Sections/index.ts +57 -0
  53. package/components/index.ts +21 -0
  54. package/dist/index-DAai7Glf.d.mts +474 -0
  55. package/dist/index-DAai7Glf.d.ts +474 -0
  56. package/dist/index.d.mts +1075 -0
  57. package/dist/index.d.ts +1075 -0
  58. package/dist/index.js +22 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/index.mjs +22 -0
  61. package/dist/index.mjs.map +1 -0
  62. package/dist/styles/index.d.mts +1 -0
  63. package/dist/styles/index.d.ts +1 -0
  64. package/dist/styles/index.js +2 -0
  65. package/dist/styles/index.js.map +1 -0
  66. package/dist/styles/index.mjs +2 -0
  67. package/dist/styles/index.mjs.map +1 -0
  68. package/docs/API.md +849 -0
  69. package/docs/CALLBACKS.md +760 -0
  70. package/docs/COMPLETE_SESSION_SUMMARY.md +404 -0
  71. package/docs/DATA_SHAPES.md +684 -0
  72. package/docs/MIGRATION.md +662 -0
  73. package/docs/PAYMENT_INTEGRATION.md +766 -0
  74. package/docs/SESSION_SUMMARY.md +185 -0
  75. package/docs/STYLING.md +735 -0
  76. package/index.ts +4 -0
  77. package/lib/storage.ts +239 -0
  78. package/package.json +59 -0
  79. package/styles/animations.ts +210 -0
  80. package/styles/index.ts +1 -0
  81. package/tsconfig.json +32 -0
  82. package/tsup.config.ts +13 -0
  83. 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