HubSpotMeetingSection.tsx173 lines · main
1'use client'
2
3import { useEffect, useMemo, useRef, useState } from 'react'
4
5import type { GoHubSpotMeetingSection } from '../schemas'
6import BookingForm from './hubspot-meeting/BookingForm'
7import CalendarPicker from './hubspot-meeting/CalendarPicker'
8import Confirmation from './hubspot-meeting/Confirmation'
9import TimeSlotPicker from './hubspot-meeting/TimeSlotPicker'
10import TimezoneSelector from './hubspot-meeting/TimezoneSelector'
11import { useScheduler } from './hubspot-meeting/useScheduler'
12
13export default function HubSpotMeetingSection({ section }: { section: GoHubSpotMeetingSection }) {
14 const sectionRef = useRef<HTMLDivElement>(null)
15 const [isInView, setIsInView] = useState(false)
16
17 useEffect(() => {
18 const el = sectionRef.current
19 if (!el) return
20
21 const observer = new IntersectionObserver(
22 ([entry]) => {
23 if (entry.isIntersecting) {
24 setIsInView(true)
25 observer.disconnect()
26 }
27 },
28 { rootMargin: '-40% 0px -40% 0px' }
29 )
30
31 observer.observe(el)
32 return () => observer.disconnect()
33 }, [])
34
35 const {
36 state,
37 timezone,
38 availableDates,
39 slotsForSelectedDate,
40 availableMonthOffsets,
41 selectDate,
42 selectSlot,
43 goBackToDate,
44 goBackToTime,
45 changeMonth,
46 setTimezone,
47 submitBooking,
48 retry,
49 dismissError,
50 } = useScheduler(section.meetingSlug, isInView)
51
52 const selectedTimeLabel = useMemo(() => {
53 if (!state.selectedSlot) return ''
54 return new Date(state.selectedSlot.startMillisUtc).toLocaleTimeString('en-US', {
55 hour: 'numeric',
56 minute: '2-digit',
57 timeZone: timezone,
58 })
59 }, [state.selectedSlot, timezone])
60
61 return (
62 <div ref={sectionRef} className="max-w-7xl mx-auto px-8">
63 {(section.title || section.description) && (
64 <div className="max-w-3xl mx-auto text-center mb-12">
65 {section.title && (
66 <h2 className="text-foreground text-2xl sm:text-3xl font-medium">{section.title}</h2>
67 )}
68 {section.description && (
69 <p className="text-foreground-lighter mt-3 text-lg">{section.description}</p>
70 )}
71 </div>
72 )}
73
74 <div className="max-w-md mx-auto border border-muted rounded-xl bg-surface-100 p-6 min-h-[440px] flex flex-col h-full">
75 {state.step === 'loading' && (
76 <div className="flex items-center justify-center h-full min-h-[inherit]">
77 <svg
78 width="24"
79 height="24"
80 viewBox="0 0 24 24"
81 fill="none"
82 stroke="currentColor"
83 strokeWidth="2"
84 strokeLinecap="round"
85 strokeLinejoin="round"
86 className="animate-spinner text-foreground-muted"
87 >
88 <path d="M21 12a9 9 0 1 1-6.219-8.56" />
89 </svg>
90 </div>
91 )}
92
93 {state.step === 'error' && (
94 <div className="flex flex-col items-center justify-center h-full min-h-[inherit] gap-3">
95 <div className="rounded-full bg-destructive-200/50 p-3">
96 <svg
97 width="20"
98 height="20"
99 viewBox="0 0 20 20"
100 fill="none"
101 xmlns="http://www.w3.org/2000/svg"
102 className="text-destructive-600"
103 >
104 <path
105 d="M10 6v4m0 4h.01M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z"
106 stroke="currentColor"
107 strokeWidth="1.5"
108 strokeLinecap="round"
109 strokeLinejoin="round"
110 />
111 </svg>
112 </div>
113 <p className="text-foreground text-sm font-medium">Something went wrong</p>
114 <p className="text-foreground-lighter text-sm text-center max-w-xs">{state.error}</p>
115 <button
116 type="button"
117 onClick={retry}
118 className="mt-1 px-4 py-2 text-sm font-medium rounded-md bg-brand-500 text-white hover:bg-brand-600 transition-colors"
119 >
120 Try again
121 </button>
122 </div>
123 )}
124
125 {(state.step === 'date-select' || state.step === 'time-select') && (
126 <div className="mb-6 flex items-center justify-center">
127 <TimezoneSelector value={timezone} onChange={setTimezone} />
128 </div>
129 )}
130
131 {state.step === 'date-select' && (
132 <CalendarPicker
133 monthOffset={state.monthOffset}
134 availableMonthOffsets={availableMonthOffsets}
135 timezone={timezone}
136 availableDates={availableDates}
137 selectedDate={state.selectedDate}
138 onSelectDate={selectDate}
139 onChangeMonth={changeMonth}
140 />
141 )}
142
143 {state.step === 'time-select' && state.selectedDate && (
144 <TimeSlotPicker
145 slots={slotsForSelectedDate}
146 timezone={timezone}
147 selectedSlot={state.selectedSlot}
148 onSelectSlot={selectSlot}
149 onBack={goBackToDate}
150 selectedDate={state.selectedDate}
151 />
152 )}
153
154 {(state.step === 'form' || state.step === 'submitting') && state.selectedDate && (
155 <BookingForm
156 onSubmit={submitBooking}
157 onBack={goBackToTime}
158 isSubmitting={state.step === 'submitting'}
159 error={state.error}
160 onDismissError={dismissError}
161 selectedDate={state.selectedDate}
162 selectedTimeLabel={selectedTimeLabel}
163 timezone={timezone}
164 />
165 )}
166
167 {state.step === 'confirmed' && state.confirmation && (
168 <Confirmation confirmation={state.confirmation} timezone={timezone} />
169 )}
170 </div>
171 </div>
172 )
173}