This commit is contained in:
Alan Daniel committed 2026-03-26 17:37:01 -04:00
1 parent f070beb6e6
commit 295c002882
8 files changed
+63 -35

No files matched your search

@@ -5,10 +5,7 @@ import { getMockAvailability, shouldUseMock } from '../../mock'
const HUBSPOT_BASE =
'https://api.hubspot.com/scheduler/v3/meetings/meeting-links/book/availability-page'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
export async function GET(request: NextRequest, { params }: { params: Promise<{ slug: string }> }) {
const { searchParams } = new URL(request.url)
const timezone = searchParams.get('timezone') || 'America/New_York'
@@ -30,10 +27,7 @@ export async function GET(
if (!res.ok) {
const text = await res.text()
return NextResponse.json(
{ error: 'HubSpot API error', detail: text },
{ status: res.status }
)
return NextResponse.json({ error: 'HubSpot API error', detail: text }, { status: res.status })
}
const data = await res.json()
@@ -4,10 +4,7 @@ import { getMockBookingInfo, shouldUseMock } from '../mock'
const HUBSPOT_BASE = 'https://api.hubspot.com/scheduler/v3/meetings/meeting-links/book'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
export async function GET(request: NextRequest, { params }: { params: Promise<{ slug: string }> }) {
const { searchParams } = new URL(request.url)
const timezone = searchParams.get('timezone') || 'America/New_York'
const monthOffset = parseInt(searchParams.get('monthOffset') || '0', 10)
@@ -30,10 +27,7 @@ export async function GET(
if (!res.ok) {
const text = await res.text()
return NextResponse.json(
{ error: 'HubSpot API error', detail: text },
{ status: res.status }
)
return NextResponse.json({ error: 'HubSpot API error', detail: text }, { status: res.status })
}
const data = await res.json()
+4 -1
View File
@@ -10,7 +10,10 @@ export function shouldUseMock(): boolean {
}
/** Generate mock time slots for the next 30 days, excluding weekends */
function generateMockSlots(timezone: string, monthOffset: number): Record<string, Array<{ startMillisUtc: number; endMillisUtc: number }>> {
function generateMockSlots(
timezone: string,
monthOffset: number
): Record<string, Array<{ startMillisUtc: number; endMillisUtc: number }>> {
const now = new Date()
const target = new Date(now.getFullYear(), now.getMonth() + monthOffset, 1)
const daysInMonth = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate()
@@ -67,7 +67,9 @@ export default function CalendarPicker({
disabled={!hasPrev}
className={cn(
'p-2 rounded-md text-foreground-lighter transition-colors',
hasPrev ? 'hover:bg-surface-300/50 hover:text-foreground' : 'opacity-30 cursor-not-allowed'
hasPrev
? 'hover:bg-surface-300/50 hover:text-foreground'
: 'opacity-30 cursor-not-allowed'
)}
aria-label="Previous month"
>
@@ -80,7 +82,9 @@ export default function CalendarPicker({
disabled={!hasNext}
className={cn(
'p-2 rounded-md text-foreground-lighter transition-colors',
hasNext ? 'hover:bg-surface-300/50 hover:text-foreground' : 'opacity-30 cursor-not-allowed'
hasNext
? 'hover:bg-surface-300/50 hover:text-foreground'
: 'opacity-30 cursor-not-allowed'
)}
aria-label="Next month"
>
@@ -133,7 +137,13 @@ export default function CalendarPicker({
function ChevronLeftIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12L6 8L10 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path
d="M10 12L6 8L10 4"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
@@ -141,7 +151,13 @@ function ChevronLeftIcon() {
function ChevronRightIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 4L10 8L6 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path
d="M6 4L10 8L6 12"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
@@ -30,7 +30,13 @@ export default function Confirmation({ confirmation, timezone }: ConfirmationPro
return (
<div className="w-full text-center flex flex-col items-center gap-4">
<div className="w-12 h-12 rounded-full bg-brand-500/10 flex items-center justify-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20 6L9 17L4 12"
stroke="currentColor"
@@ -12,7 +12,10 @@ async function parseErrorResponse(res: Response, fallback: string): Promise<stri
}
function wrapNetworkError(err: unknown): never {
if (err instanceof TypeError && (err.message === 'Failed to fetch' || err.message === 'Load failed')) {
if (
err instanceof TypeError &&
(err.message === 'Failed to fetch' || err.message === 'Load failed')
) {
throw new Error('Unable to connect. Please check your internet connection and try again.')
}
throw err
@@ -37,10 +40,7 @@ export async function fetchBookingInfo(
return res.json()
}
export async function fetchAvailability(
slug: string,
timezone: string
): Promise<BookingInfo> {
export async function fetchAvailability(slug: string, timezone: string): Promise<BookingInfo> {
const params = new URLSearchParams({ timezone })
let res: Response
try {
@@ -68,4 +68,11 @@ export interface BookingConfirmation {
isOffline: boolean
}
export type SchedulerStep = 'loading' | 'date-select' | 'time-select' | 'form' | 'submitting' | 'confirmed' | 'error'
export type SchedulerStep =
| 'loading'
| 'date-select'
| 'time-select'
| 'form'
| 'submitting'
| 'confirmed'
| 'error'
@@ -1,7 +1,13 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { bookMeeting, fetchBookingInfo } from './api'
import type { BookingConfirmation, BookingInfo, BookingRequest, SchedulerStep, TimeSlot } from './types'
import type {
BookingConfirmation,
BookingInfo,
BookingRequest,
SchedulerStep,
TimeSlot,
} from './types'
/** How many months to prefetch (current + 3 ahead) */
const MONTHS_TO_FETCH = 4
@@ -59,9 +65,7 @@ export function useScheduler(slug: string, enabled = true) {
setState((s) => ({ ...s, step: 'loading', error: null }))
const results = await Promise.all(
Array.from({ length: MONTHS_TO_FETCH }, (_, i) =>
fetchBookingInfo(slug, effectiveTz, i)
)
Array.from({ length: MONTHS_TO_FETCH }, (_, i) => fetchBookingInfo(slug, effectiveTz, i))
)
const cache: Record<number, BookingInfo> = {}
@@ -69,8 +73,7 @@ export function useScheduler(slug: string, enabled = true) {
cache[i] = results[i]
}
const defaultDuration =
results[0]?.customParams?.availableDurations?.[0] ?? 1800000
const defaultDuration = results[0]?.customParams?.availableDurations?.[0] ?? 1800000
setMonthCache(cache)
setState((s) => ({
@@ -134,7 +137,12 @@ export function useScheduler(slug: string, enabled = true) {
}, [])
const submitBooking = useCallback(
async (form: { firstName: string; lastName: string; email: string; guestEmails?: string[] }) => {
async (form: {
firstName: string
lastName: string
email: string
guestEmails?: string[]
}) => {
if (!state.selectedSlot) return
setState((s) => ({ ...s, step: 'submitting' }))