@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,790 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import {
6
+ ServiceSelection,
7
+ DateSelection,
8
+ TimeSelection,
9
+ AddonsSelection,
10
+ ContactForm,
11
+ Confirmation
12
+ } from './steps';
13
+ import { HoldTimer } from '../HoldTimer';
14
+ import type {
15
+ HoldTimerConfig,
16
+ HoldStatus,
17
+ CreateHoldFn,
18
+ ExtendHoldFn,
19
+ ReleaseHoldFn
20
+ } from '../../types';
21
+
22
+ // Local interface for step management
23
+ interface BookingFlowStep {
24
+ id: string;
25
+ name: string;
26
+ enabled: boolean;
27
+ settings?: any;
28
+ }
29
+
30
+ // Export simplified versions (not conflicting with types/index.ts)
31
+ export interface BookingFlowConfig {
32
+ steps: {
33
+ service_selection?: {
34
+ enabled: boolean;
35
+ settings?: any;
36
+ };
37
+ date_selection?: {
38
+ enabled: boolean;
39
+ settings?: any;
40
+ };
41
+ time_selection?: {
42
+ enabled: boolean;
43
+ settings?: any;
44
+ };
45
+ addon_selection?: {
46
+ enabled: boolean;
47
+ settings?: any;
48
+ };
49
+ contact_form?: {
50
+ enabled: boolean;
51
+ settings?: any;
52
+ };
53
+ confirmation?: {
54
+ enabled: boolean;
55
+ settings?: any;
56
+ };
57
+ };
58
+ progressBar?: {
59
+ style?: 'dots' | 'bar' | 'minimal';
60
+ };
61
+ transitions?: {
62
+ style?: 'slide' | 'fade';
63
+ speed?: 'fast' | 'normal' | 'slow';
64
+ };
65
+ background?: {
66
+ type: 'color' | 'gradient' | 'image';
67
+ value: string;
68
+ };
69
+ }
70
+
71
+ export interface BookingFlowProps {
72
+ config: BookingFlowConfig;
73
+ colors: {
74
+ primary: string;
75
+ secondary: string;
76
+ text?: string;
77
+ bookingText?: string;
78
+ };
79
+ // Data providers - pass your service/date/time data
80
+ services?: any[];
81
+ categories?: any[];
82
+ dates?: string[];
83
+ times?: string[];
84
+ addons?: any[];
85
+ // Payment configuration (for confirmation step)
86
+ paymentProvider?: 'square' | 'stripe';
87
+ paymentConfig?: any;
88
+ isPreview?: boolean;
89
+ // Callbacks
90
+ onComplete?: (bookingData: any) => void;
91
+ onStepChange?: (stepIndex: number, stepId: string) => void;
92
+ onServiceSelect?: (serviceId: string) => void;
93
+ onDateSelect?: (date: string) => void;
94
+ onContactInfoChange?: (contactInfo: any) => void; // NEW: Callback when contact info changes
95
+ // Optional: provide custom step validation
96
+ validateStep?: (stepId: string, data: any) => boolean;
97
+ // Hold timer integration (optional)
98
+ holdTimer?: {
99
+ enabled: boolean;
100
+ config: Partial<HoldTimerConfig>;
101
+ createHold: CreateHoldFn;
102
+ extendHold: ExtendHoldFn;
103
+ releaseHold: ReleaseHoldFn;
104
+ };
105
+ // Optional: custom step order for mod-based flows
106
+ stepOrder?: string[]; // NEW: Array of step IDs in desired order, e.g., ['details', 'service', 'date', 'time', 'addons', 'confirm']
107
+ // Optional: opt-in modules for contact form
108
+ optInModules?: import('../../types').OptInModule[]; // NEW: Modules for cycle-aware, etc.
109
+ onOptInData?: (moduleId: string, data: any) => void; // NEW: Callback for opt-in module data
110
+ // Optional: custom payment form component to replace default confirmation
111
+ customPaymentForm?: React.ComponentType<any>;
112
+ // Optional: cycle phase data for date selection
113
+ cyclePhases?: Record<string, string>; // NEW: Map of date -> phase for cycle-aware booking
114
+ }
115
+
116
+ /**
117
+ * BookingFlow - Main orchestrator component for multi-step booking
118
+ *
119
+ * Manages flow state, navigation, and renders appropriate step components.
120
+ *
121
+ * @example
122
+ * ```tsx
123
+ * <BookingFlow
124
+ * config={{
125
+ * steps: {
126
+ * service_selection: { enabled: true, settings: {} },
127
+ * date_selection: { enabled: true, settings: {} },
128
+ * time_selection: { enabled: true, settings: {} },
129
+ * contact_form: { enabled: true, settings: {} }
130
+ * },
131
+ * progressBar: { style: 'dots' },
132
+ * transitions: { style: 'slide', speed: 'normal' }
133
+ * }}
134
+ * colors={{ primary: '#BCB4FF', secondary: '#CAC426' }}
135
+ * services={services}
136
+ * onComplete={(data) => console.log('Booking complete!', data)}
137
+ * />
138
+ * ```
139
+ */
140
+ export function BookingFlow({
141
+ config,
142
+ colors,
143
+ services = [],
144
+ categories = [],
145
+ dates = [],
146
+ times = [],
147
+ addons = [],
148
+ paymentProvider,
149
+ paymentConfig,
150
+ isPreview = false,
151
+ onComplete,
152
+ onStepChange,
153
+ onServiceSelect,
154
+ onDateSelect,
155
+ onContactInfoChange,
156
+ validateStep,
157
+ holdTimer,
158
+ stepOrder,
159
+ optInModules = [],
160
+ onOptInData,
161
+ customPaymentForm,
162
+ cyclePhases
163
+ }: BookingFlowProps) {
164
+ // Flow state
165
+ const [currentStepIndex, setCurrentStepIndex] = useState(0);
166
+ const [direction, setDirection] = useState<'forward' | 'backward'>('forward');
167
+
168
+ // Booking data state
169
+ const [selectedService, setSelectedService] = useState<string | null>(null);
170
+ const [selectedDate, setSelectedDate] = useState<string | null>(null);
171
+ const [selectedTime, setSelectedTime] = useState<string | null>(null);
172
+ const [selectedAddons, setSelectedAddons] = useState<string[]>([]);
173
+ const [contactInfo, setContactInfo] = useState<any>({});
174
+ const [optInData, setOptInData] = useState<any>({});
175
+
176
+ // Hold timer state
177
+ const [activeHold, setActiveHold] = useState<HoldStatus | null>(null);
178
+
179
+ // Configuration
180
+ const textColor = colors.bookingText || colors.text || '#000000';
181
+ const transitions = config.transitions || {};
182
+ const transitionStyle = transitions.style || 'slide';
183
+ const transitionSpeed = transitions.speed || 'normal';
184
+ const reducedMotion = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
185
+ const background = config.background || { type: 'color', value: '#FFFFFF' };
186
+
187
+ // Build enabled steps array
188
+ const stepsMap: Record<string, BookingFlowStep> = {
189
+ service: {
190
+ id: 'service',
191
+ name: 'Service',
192
+ enabled: config.steps?.service_selection?.enabled !== false, // Core step - defaults to enabled
193
+ settings: config.steps?.service_selection?.settings || {}
194
+ },
195
+ date: {
196
+ id: 'date',
197
+ name: 'Date',
198
+ enabled: config.steps?.date_selection?.enabled !== false, // Core step - defaults to enabled
199
+ settings: config.steps?.date_selection?.settings || {}
200
+ },
201
+ time: {
202
+ id: 'time',
203
+ name: 'Time',
204
+ enabled: config.steps?.time_selection?.enabled !== false, // Core step - defaults to enabled
205
+ settings: config.steps?.time_selection?.settings || {}
206
+ },
207
+ addons: {
208
+ id: 'addons',
209
+ name: 'Add-ons',
210
+ enabled: config.steps?.addon_selection?.enabled === true, // Optional step - must be explicitly enabled
211
+ settings: config.steps?.addon_selection?.settings || {}
212
+ },
213
+ details: {
214
+ id: 'details',
215
+ name: 'Details',
216
+ enabled: config.steps?.contact_form?.enabled !== false, // Core step - defaults to enabled
217
+ settings: config.steps?.contact_form?.settings || {}
218
+ },
219
+ confirm: {
220
+ id: 'confirm',
221
+ name: 'Confirm',
222
+ enabled: config.steps?.confirmation?.enabled === true, // Conditional based on config
223
+ settings: config.steps?.confirmation?.settings || {}
224
+ }
225
+ };
226
+
227
+ // NEW: Use custom step order if provided, otherwise use default order
228
+ const defaultOrder = ['service', 'date', 'time', 'addons', 'details', 'confirm'];
229
+ const orderedStepIds = stepOrder || defaultOrder;
230
+
231
+ // Build steps array respecting the custom order and filtering disabled steps
232
+ const allSteps: BookingFlowStep[] = orderedStepIds
233
+ .map(id => stepsMap[id])
234
+ .filter(step => step && step.enabled);
235
+
236
+ console.log('[BookingFlow] Step configuration:', {
237
+ customStepOrder: stepOrder,
238
+ orderedStepIds,
239
+ allSteps: allSteps.map(s => s.id),
240
+ currentStepIndex,
241
+ currentStepId: allSteps[currentStepIndex]?.id
242
+ });
243
+
244
+ const currentStep = allSteps[currentStepIndex];
245
+
246
+ // Background styling
247
+ const getBackgroundStyle = () => {
248
+ if (background.type === 'gradient') {
249
+ return { background: background.value };
250
+ }
251
+ if (background.type === 'image') {
252
+ return {
253
+ backgroundImage: `url(${background.value})`,
254
+ backgroundSize: 'cover',
255
+ backgroundPosition: 'center'
256
+ };
257
+ }
258
+ return { backgroundColor: background.value };
259
+ };
260
+
261
+ // Animation configuration
262
+ const getDuration = () => {
263
+ if (reducedMotion) return 0.15;
264
+ switch (transitionSpeed) {
265
+ case 'fast': return 0.2;
266
+ case 'slow': return 0.5;
267
+ default: return 0.3;
268
+ }
269
+ };
270
+
271
+ const getVariants = () => {
272
+ const slideDistance = 50;
273
+
274
+ if (transitionStyle === 'fade') {
275
+ return {
276
+ enter: () => ({ opacity: 0, scale: 0.95 }),
277
+ center: { opacity: 1, scale: 1 },
278
+ exit: () => ({ opacity: 0, scale: 0.95 })
279
+ };
280
+ }
281
+
282
+ // Default: slide
283
+ return {
284
+ enter: () => ({
285
+ x: direction === 'forward' ? slideDistance : -slideDistance,
286
+ opacity: 0
287
+ }),
288
+ center: { x: 0, opacity: 1 },
289
+ exit: () => ({
290
+ x: direction === 'forward' ? -slideDistance : slideDistance,
291
+ opacity: 0
292
+ })
293
+ };
294
+ };
295
+
296
+ // Step validation
297
+ const canGoNext = () => {
298
+ const stepId = currentStep?.id;
299
+
300
+ // Custom validation if provided
301
+ if (validateStep) {
302
+ const currentData = {
303
+ service: selectedService,
304
+ date: selectedDate,
305
+ time: selectedTime,
306
+ addons: selectedAddons,
307
+ contact: contactInfo
308
+ };
309
+ return validateStep(stepId, currentData);
310
+ }
311
+
312
+ // Default validation
313
+ switch (stepId) {
314
+ case 'service':
315
+ return selectedService !== null;
316
+ case 'date':
317
+ return selectedDate !== null;
318
+ case 'time':
319
+ return selectedTime !== null;
320
+ case 'details':
321
+ // Validate contact form: name, email, and phone are required (notes optional)
322
+ if (isPreview) return true; // Skip validation in preview mode
323
+
324
+ const hasName = contactInfo?.name && contactInfo.name.trim().length >= 2;
325
+ const hasValidEmail = contactInfo?.email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(contactInfo.email);
326
+ const hasValidPhone = contactInfo?.phone && contactInfo.phone.replace(/\D/g, '').length >= 10;
327
+
328
+ return hasName && hasValidEmail && hasValidPhone;
329
+ default:
330
+ return true;
331
+ }
332
+ };
333
+
334
+ const isStepCompleted = (stepIndex: number) => {
335
+ return stepIndex < currentStepIndex;
336
+ };
337
+
338
+ // Navigation
339
+ const handleStepChange = (newStepIndex: number) => {
340
+ setDirection(newStepIndex > currentStepIndex ? 'forward' : 'backward');
341
+ setCurrentStepIndex(newStepIndex);
342
+
343
+ if (onStepChange) {
344
+ onStepChange(newStepIndex, allSteps[newStepIndex]?.id);
345
+ }
346
+ };
347
+
348
+ const handleNext = () => {
349
+ if (currentStepIndex < allSteps.length - 1) {
350
+ handleStepChange(currentStepIndex + 1);
351
+ }
352
+ };
353
+
354
+ const handleBack = async () => {
355
+ if (currentStepIndex === 0) return;
356
+
357
+ const prevStepIndex = currentStepIndex - 1;
358
+ const prevStep = allSteps[prevStepIndex];
359
+
360
+ // If going back to/before time selection, release hold
361
+ if (prevStep?.id === 'time' && activeHold && holdTimer?.releaseHold) {
362
+ try {
363
+ await holdTimer.releaseHold(activeHold.holdId);
364
+ setActiveHold(null);
365
+ setSelectedTime(null);
366
+ } catch (error) {
367
+ console.error('[BookingFlow] Failed to release hold:', error);
368
+ }
369
+ }
370
+
371
+ handleStepChange(prevStepIndex);
372
+ };
373
+
374
+ // Enhanced time selection handler with hold integration
375
+ const handleTimeSelect = async (time: string) => {
376
+ setSelectedTime(time);
377
+
378
+ // If hold timer enabled, create hold before proceeding
379
+ if (holdTimer?.enabled && holdTimer.createHold && selectedDate && selectedService) {
380
+ try {
381
+ const selectedServiceData = services.find((s: any) => s.id === selectedService);
382
+
383
+ const hold = await holdTimer.createHold({
384
+ date: selectedDate,
385
+ time,
386
+ serviceId: selectedService,
387
+ serviceDuration: selectedServiceData?.duration || 60
388
+ });
389
+
390
+ setActiveHold(hold);
391
+ } catch (error) {
392
+ console.error('[BookingFlow] Failed to create hold:', error);
393
+ // Don't proceed if hold creation fails
394
+ return;
395
+ }
396
+ }
397
+
398
+ // Navigate to next step
399
+ handleNext();
400
+ };
401
+
402
+ // Hold expiration handler
403
+ const handleHoldExpired = () => {
404
+ setActiveHold(null);
405
+ setSelectedTime(null);
406
+
407
+ // Find and navigate to time step
408
+ const timeStepIndex = allSteps.findIndex(s => s.id === 'time');
409
+ if (timeStepIndex >= 0) {
410
+ handleStepChange(timeStepIndex);
411
+ }
412
+ };
413
+
414
+ const handleComplete = async () => {
415
+ const bookingData = {
416
+ service: selectedService,
417
+ date: selectedDate,
418
+ time: selectedTime,
419
+ addons: selectedAddons,
420
+ contact: contactInfo,
421
+ optIn: optInData
422
+ };
423
+
424
+ if (onComplete) {
425
+ onComplete(bookingData);
426
+ }
427
+ };
428
+
429
+ // Render current step
430
+ const renderCurrentStep = () => {
431
+ if (!currentStep) return null;
432
+
433
+ const stepProps = {
434
+ settings: currentStep.settings,
435
+ colors
436
+ };
437
+
438
+ // Get selected service data once for all steps that need it
439
+ const selectedServiceData = services.find((s: any) => s.id === selectedService);
440
+
441
+ switch (currentStep.id) {
442
+ case 'service':
443
+ return (
444
+ <ServiceSelection
445
+ {...stepProps}
446
+ services={services}
447
+ categories={categories}
448
+ selectedService={selectedService}
449
+ onServiceSelect={(serviceId) => {
450
+ setSelectedService(serviceId);
451
+ // NEW: Notify parent of service selection
452
+ if (onServiceSelect) {
453
+ onServiceSelect(serviceId);
454
+ }
455
+ }}
456
+ />
457
+ );
458
+
459
+ case 'date':
460
+ return (
461
+ <DateSelection
462
+ {...stepProps}
463
+ availableDates={dates}
464
+ selectedDate={selectedDate}
465
+ onDateSelect={(date) => {
466
+ setSelectedDate(date);
467
+ // NEW: Notify parent of date selection
468
+ if (onDateSelect) {
469
+ onDateSelect(date);
470
+ }
471
+ }}
472
+ cyclePhases={cyclePhases} // NEW: Pass cycle phases for cycle-aware booking
473
+ selectedService={selectedServiceData} // NEW: Pass selected service for summary card
474
+ addons={addons} // NEW: Pass addons for selection
475
+ selectedAddons={selectedAddons} // NEW: Pass selected addons
476
+ onAddonsChange={setSelectedAddons} // NEW: Handle addon changes
477
+ addonPlacement={config.steps?.addon_selection?.settings?.placement || 'date_selection'} // NEW: Pass addon placement config
478
+ />
479
+ );
480
+
481
+ case 'time':
482
+ return (
483
+ <TimeSelection
484
+ {...stepProps}
485
+ availableTimes={times}
486
+ selectedTime={selectedTime}
487
+ onTimeSelect={handleTimeSelect}
488
+ serviceDuration={selectedServiceData?.duration || 60}
489
+ selectedService={selectedServiceData}
490
+ selectedDate={selectedDate}
491
+ selectedAddons={selectedAddons}
492
+ addons={addons}
493
+ />
494
+ );
495
+
496
+ case 'addons':
497
+ return (
498
+ <AddonsSelection
499
+ {...stepProps}
500
+ addons={addons}
501
+ selectedAddons={selectedAddons}
502
+ onAddonsChange={setSelectedAddons}
503
+ />
504
+ );
505
+
506
+ case 'details':
507
+ return (
508
+ <ContactForm
509
+ {...stepProps}
510
+ contactInfo={contactInfo}
511
+ onContactInfoChange={(info) => {
512
+ setContactInfo(info);
513
+ // NEW: Notify parent of contact info changes
514
+ if (onContactInfoChange) {
515
+ onContactInfoChange(info);
516
+ }
517
+ }}
518
+ optInModules={optInModules} // NEW: Pass opt-in modules from parent
519
+ onOptInData={onOptInData} // NEW: Pass opt-in data callback
520
+ />
521
+ );
522
+
523
+ case 'confirm':
524
+ const selectedAddonsData = addons.filter((a: any) => selectedAddons.includes(a.id));
525
+
526
+ // Calculate addon total cost
527
+ const addonTotalCost = selectedAddonsData.reduce((sum, addon) => {
528
+ return sum + (addon.addonPrice || addon.price || 0);
529
+ }, 0);
530
+
531
+ // Calculate total amount (service + addons)
532
+ const totalAmount = (selectedServiceData?.price || 0) + addonTotalCost;
533
+
534
+ // Calculate total duration (service + addons that affect duration)
535
+ const totalDuration = selectedAddonsData.reduce((duration, addon) => {
536
+ if (addon.affectsDuration && addon.additionalDuration) {
537
+ return duration + addon.additionalDuration;
538
+ }
539
+ return duration;
540
+ }, selectedServiceData?.duration || 60);
541
+
542
+ // Use custom payment form if provided, otherwise use default Confirmation
543
+ if (customPaymentForm) {
544
+ const CustomPaymentForm = customPaymentForm;
545
+ return (
546
+ <CustomPaymentForm
547
+ bookingData={{
548
+ service: selectedService,
549
+ serviceName: selectedServiceData?.name,
550
+ date: selectedDate,
551
+ time: selectedTime,
552
+ addons: selectedAddons,
553
+ contact: contactInfo,
554
+ amount: selectedServiceData?.price || 0,
555
+ duration: totalDuration,
556
+ // Contact info for automation
557
+ clientName: contactInfo?.name,
558
+ clientEmail: contactInfo?.email,
559
+ clientPhone: contactInfo?.phone,
560
+ // Add pendingBooking with COMPLETE booking data for PaymentForm
561
+ pendingBooking: {
562
+ // Customer info
563
+ customerClientId: null, // Will be populated by wrapper if returning customer
564
+
565
+ // Service info
566
+ serviceId: selectedService,
567
+ date: selectedDate,
568
+ time: selectedTime,
569
+ notes: contactInfo?.notes || '',
570
+
571
+ // Addon info
572
+ selectedAddons: selectedAddonsData, // Full addon objects for display
573
+ addonServices: selectedAddons, // Addon IDs for database
574
+ addonTotalCost: addonTotalCost,
575
+
576
+ // Payment calculations
577
+ totalAmount: totalAmount,
578
+ duration: totalDuration
579
+ }
580
+ }}
581
+ onPaymentSuccess={handleComplete}
582
+ onPaymentError={(error: any) => {
583
+ console.error('[BookingFlow] Payment error:', error);
584
+ }}
585
+ />
586
+ );
587
+ }
588
+
589
+ return (
590
+ <Confirmation
591
+ {...stepProps}
592
+ service={selectedServiceData}
593
+ addons={selectedAddonsData}
594
+ selectedDate={selectedDate || ''}
595
+ selectedTime={selectedTime || ''}
596
+ paymentProvider={paymentProvider || 'square'}
597
+ paymentConfig={paymentConfig || { depositPercentage: 20 }}
598
+ isPreview={isPreview}
599
+ onConfirm={handleComplete}
600
+ />
601
+ );
602
+
603
+ default:
604
+ return null;
605
+ }
606
+ };
607
+
608
+ // Render progress indicator
609
+ const renderProgressBar = () => {
610
+ const progressStyle = config.progressBar?.style || 'dots';
611
+
612
+ if (progressStyle === 'bar') {
613
+ return (
614
+ <div className="mb-6">
615
+ <div className="flex justify-between items-center mb-2">
616
+ <span className="text-sm font-medium">Step {currentStepIndex + 1} of {allSteps.length}</span>
617
+ <span className="text-xs opacity-60">{currentStep?.name}</span>
618
+ </div>
619
+ <div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
620
+ <motion.div
621
+ className="h-full rounded-full"
622
+ style={{ backgroundColor: colors.primary }}
623
+ initial={{ width: 0 }}
624
+ animate={{ width: `${((currentStepIndex + 1) / allSteps.length) * 100}%` }}
625
+ transition={{ duration: 0.3, ease: 'easeOut' }}
626
+ />
627
+ </div>
628
+ </div>
629
+ );
630
+ }
631
+
632
+ if (progressStyle === 'minimal') {
633
+ return (
634
+ <div className="mb-6 text-center">
635
+ <p className="text-sm font-medium" style={{ color: colors.primary }}>
636
+ {currentStep?.name}
637
+ </p>
638
+ <p className="text-xs opacity-60 mt-1">
639
+ Step {currentStepIndex + 1} of {allSteps.length}
640
+ </p>
641
+ </div>
642
+ );
643
+ }
644
+
645
+ // Default: dots
646
+ return (
647
+ <div className="flex justify-center mb-4 overflow-x-auto py-2">
648
+ <div className="flex items-center space-x-1.5 px-4 min-w-max">
649
+ {allSteps.map((step, index) => (
650
+ <div key={step.id} className="flex items-center">
651
+ <motion.div
652
+ className={`w-6 h-6 rounded-full flex items-center justify-center font-medium text-xs transition-all flex-shrink-0 ${
653
+ index === currentStepIndex
654
+ ? 'text-white shadow-md'
655
+ : isStepCompleted(index)
656
+ ? 'text-white'
657
+ : 'bg-gray-200 text-gray-500'
658
+ }`}
659
+ style={{
660
+ backgroundColor: index <= currentStepIndex ? colors.primary : undefined
661
+ }}
662
+ animate={{
663
+ scale: index === currentStepIndex ? 1.05 : 1
664
+ }}
665
+ transition={{ duration: 0.2 }}
666
+ >
667
+ {isStepCompleted(index) ? '✓' : index + 1}
668
+ </motion.div>
669
+ {index < allSteps.length - 1 && (
670
+ <motion.div
671
+ className={`w-4 h-0.5 mx-1 transition-all flex-shrink-0 ${
672
+ index < currentStepIndex ? '' : 'bg-gray-200'
673
+ }`}
674
+ style={{
675
+ backgroundColor: index < currentStepIndex ? colors.primary : undefined
676
+ }}
677
+ initial={{ scaleX: 0 }}
678
+ animate={{ scaleX: 1 }}
679
+ transition={{ duration: 0.3, delay: 0.1 }}
680
+ />
681
+ )}
682
+ </div>
683
+ ))}
684
+ </div>
685
+ </div>
686
+ );
687
+ };
688
+
689
+ return (
690
+ <div
691
+ className="w-full max-w-4xl mx-auto p-6 rounded-2xl shadow-xl"
692
+ style={{ ...getBackgroundStyle(), color: textColor }}
693
+ >
694
+ {/* Progress Indicator */}
695
+ {renderProgressBar()}
696
+
697
+ {/* Hold Timer - Show when active and past time step */}
698
+ {holdTimer?.enabled && activeHold && currentStep?.id !== 'time' && (
699
+ <HoldTimer
700
+ holdStatus={activeHold}
701
+ config={holdTimer.config}
702
+ colors={colors}
703
+ onExtend={async () => {
704
+ if (holdTimer.extendHold && activeHold) {
705
+ try {
706
+ const extended = await holdTimer.extendHold(activeHold.holdId);
707
+ setActiveHold(extended);
708
+ } catch (error) {
709
+ console.error('[BookingFlow] Failed to extend hold:', error);
710
+ }
711
+ }
712
+ }}
713
+ onRelease={async () => {
714
+ if (holdTimer.releaseHold && activeHold) {
715
+ try {
716
+ await holdTimer.releaseHold(activeHold.holdId);
717
+ setActiveHold(null);
718
+ setSelectedTime(null);
719
+ // Navigate to time step
720
+ const timeStepIndex = allSteps.findIndex(s => s.id === 'time');
721
+ if (timeStepIndex >= 0) {
722
+ handleStepChange(timeStepIndex);
723
+ }
724
+ } catch (error) {
725
+ console.error('[BookingFlow] Failed to release hold:', error);
726
+ }
727
+ }
728
+ }}
729
+ onExpired={handleHoldExpired}
730
+ className="mb-6"
731
+ />
732
+ )}
733
+
734
+ {/* Current Step Content */}
735
+ <div className="mb-6 min-h-[300px] relative overflow-hidden">
736
+ <AnimatePresence mode="wait" custom={direction}>
737
+ <motion.div
738
+ key={currentStepIndex}
739
+ custom={direction}
740
+ variants={getVariants()}
741
+ initial="enter"
742
+ animate="center"
743
+ exit="exit"
744
+ transition={{
745
+ duration: getDuration(),
746
+ ease: [0.4, 0, 0.2, 1]
747
+ }}
748
+ >
749
+ {renderCurrentStep()}
750
+ </motion.div>
751
+ </AnimatePresence>
752
+ </div>
753
+
754
+ {/* Navigation Buttons - Hide when on confirm step with custom payment form */}
755
+ {!(currentStep?.id === 'confirm' && customPaymentForm) && (
756
+ <div className="flex items-center justify-between pt-4 border-t border-gray-200">
757
+ <button
758
+ onClick={handleBack}
759
+ disabled={currentStepIndex === 0}
760
+ className="px-4 py-2 rounded-lg border border-gray-300 disabled:opacity-30 disabled:cursor-not-allowed hover:bg-gray-50 transition-all text-sm font-medium"
761
+ >
762
+ Back
763
+ </button>
764
+
765
+ {currentStepIndex < allSteps.length - 1 ? (
766
+ <button
767
+ onClick={handleNext}
768
+ disabled={!canGoNext()}
769
+ className="px-6 py-2 rounded-lg text-white disabled:opacity-50 disabled:cursor-not-allowed hover:opacity-90 transition-all text-sm font-medium shadow-md"
770
+ style={{ backgroundColor: colors.primary }}
771
+ >
772
+ Next
773
+ </button>
774
+ ) : (
775
+ <button
776
+ onClick={handleComplete}
777
+ disabled={!canGoNext()}
778
+ className="px-6 py-2 rounded-lg text-white disabled:opacity-50 disabled:cursor-not-allowed hover:opacity-90 transition-all text-sm font-medium shadow-md"
779
+ style={{ backgroundColor: colors.primary }}
780
+ >
781
+ {currentStep?.id === 'confirm' ? 'Pay & Confirm' : 'Complete'}
782
+ </button>
783
+ )}
784
+ </div>
785
+ )}
786
+ </div>
787
+ );
788
+ }
789
+
790
+ export default BookingFlow;