mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 14:19:11 +03:00
custom schedule ui
This commit is contained in:
1 parent
6af66f3c28
commit
fec3638bc4
4 files changed
+74
-16
No files matched your search
@@ -37,5 +37,27 @@ export async function GET(
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
// HubSpot returns `durations` but our types expect `availableDurations`
|
||||
if (data.customParams?.durations && !data.customParams.availableDurations) {
|
||||
data.customParams.availableDurations = data.customParams.durations
|
||||
}
|
||||
|
||||
// Transform linkAvailability from HubSpot's nested structure to a flat Record<string, TimeSlot[]>
|
||||
// HubSpot returns: { linkAvailabilityByDuration: { "1800000": { availabilities: [...] } } }
|
||||
// We need: { "1800000": [...] }
|
||||
if (data.linkAvailability?.linkAvailabilityByDuration) {
|
||||
const flat: Record<string, Array<{ startMillisUtc: number; endMillisUtc: number }>> = {}
|
||||
for (const [duration, value] of Object.entries(
|
||||
data.linkAvailability.linkAvailabilityByDuration as Record<
|
||||
string,
|
||||
{ availabilities?: Array<{ startMillisUtc: number; endMillisUtc: number }> }
|
||||
>
|
||||
)) {
|
||||
flat[duration] = value.availabilities ?? []
|
||||
}
|
||||
data.linkAvailability = flat
|
||||
}
|
||||
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export default function HubSpotMeetingSection({ section }: { section: GoHubSpotM
|
||||
timezone,
|
||||
availableDates,
|
||||
slotsForSelectedDate,
|
||||
maxMonthOffset,
|
||||
availableMonthOffsets,
|
||||
selectDate,
|
||||
selectSlot,
|
||||
goBackToDate,
|
||||
@@ -120,7 +120,7 @@ export default function HubSpotMeetingSection({ section }: { section: GoHubSpotM
|
||||
{state.step === 'date-select' && (
|
||||
<CalendarPicker
|
||||
monthOffset={state.monthOffset}
|
||||
maxMonthOffset={maxMonthOffset}
|
||||
availableMonthOffsets={availableMonthOffsets}
|
||||
timezone={timezone}
|
||||
availableDates={availableDates}
|
||||
selectedDate={state.selectedDate}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cn } from 'ui'
|
||||
|
||||
interface CalendarPickerProps {
|
||||
monthOffset: number
|
||||
maxMonthOffset: number
|
||||
availableMonthOffsets: number[]
|
||||
timezone: string
|
||||
availableDates: Set<string>
|
||||
selectedDate: string | null
|
||||
@@ -15,7 +15,7 @@ const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
|
||||
export default function CalendarPicker({
|
||||
monthOffset,
|
||||
maxMonthOffset,
|
||||
availableMonthOffsets,
|
||||
timezone,
|
||||
availableDates,
|
||||
selectedDate,
|
||||
@@ -55,16 +55,19 @@ export default function CalendarPicker({
|
||||
|
||||
const today = new Date().toLocaleDateString('en-CA', { timeZone: timezone })
|
||||
|
||||
const hasPrev = availableMonthOffsets.some((o) => o < monthOffset)
|
||||
const hasNext = availableMonthOffsets.some((o) => o > monthOffset)
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChangeMonth(monthOffset - 1)}
|
||||
disabled={monthOffset <= 0}
|
||||
disabled={!hasPrev}
|
||||
className={cn(
|
||||
'p-2 rounded-md text-foreground-lighter transition-colors',
|
||||
monthOffset > 0 ? '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"
|
||||
>
|
||||
@@ -74,10 +77,10 @@ export default function CalendarPicker({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChangeMonth(monthOffset + 1)}
|
||||
disabled={monthOffset >= maxMonthOffset}
|
||||
disabled={!hasNext}
|
||||
className={cn(
|
||||
'p-2 rounded-md text-foreground-lighter transition-colors',
|
||||
monthOffset < maxMonthOffset ? '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"
|
||||
>
|
||||
|
||||
@@ -121,11 +121,6 @@ export function useScheduler(slug: string, enabled = true) {
|
||||
setState((s) => ({ ...s, selectedSlot: null, step: 'time-select' }))
|
||||
}, [])
|
||||
|
||||
const changeMonth = useCallback((offset: number) => {
|
||||
const clamped = Math.max(0, Math.min(offset, MONTHS_TO_FETCH - 1))
|
||||
setState((s) => ({ ...s, monthOffset: clamped }))
|
||||
}, [])
|
||||
|
||||
const retry = useCallback(() => {
|
||||
loadAllMonths()
|
||||
}, [loadAllMonths])
|
||||
@@ -203,8 +198,46 @@ export function useScheduler(slug: string, enabled = true) {
|
||||
return dates
|
||||
}, [allSlots, timezone])
|
||||
|
||||
/** Max month offset the user can navigate to */
|
||||
const maxMonthOffset = MONTHS_TO_FETCH - 1
|
||||
/** Month offsets that have at least one available slot */
|
||||
const availableMonthOffsets = useMemo(() => {
|
||||
const now = new Date()
|
||||
const offsets = new Set<number>()
|
||||
for (const slot of allSlots) {
|
||||
const date = new Date(slot.startMillisUtc)
|
||||
const localDateStr = date.toLocaleDateString('en-CA', { timeZone: timezone })
|
||||
const [y, m] = localDateStr.split('-').map(Number)
|
||||
const offset = (y - now.getFullYear()) * 12 + (m - 1 - now.getMonth())
|
||||
if (offset >= 0 && offset < MONTHS_TO_FETCH) {
|
||||
offsets.add(offset)
|
||||
}
|
||||
}
|
||||
return Array.from(offsets).sort((a, b) => a - b)
|
||||
}, [allSlots, timezone])
|
||||
|
||||
/** Auto-navigate to first month with availability */
|
||||
const hasSetInitialMonth = useRef(false)
|
||||
useEffect(() => {
|
||||
if (hasSetInitialMonth.current) return
|
||||
if (availableMonthOffsets.length > 0 && state.step === 'date-select') {
|
||||
hasSetInitialMonth.current = true
|
||||
setState((s) => ({ ...s, monthOffset: availableMonthOffsets[0] }))
|
||||
}
|
||||
}, [availableMonthOffsets, state.step])
|
||||
|
||||
const changeMonth = useCallback(
|
||||
(offset: number) => {
|
||||
if (availableMonthOffsets.length === 0) return
|
||||
const current = state.monthOffset
|
||||
if (offset > current) {
|
||||
const next = availableMonthOffsets.find((o) => o > current)
|
||||
if (next !== undefined) setState((s) => ({ ...s, monthOffset: next }))
|
||||
} else if (offset < current) {
|
||||
const prev = [...availableMonthOffsets].reverse().find((o) => o < current)
|
||||
if (prev !== undefined) setState((s) => ({ ...s, monthOffset: prev }))
|
||||
}
|
||||
},
|
||||
[availableMonthOffsets, state.monthOffset]
|
||||
)
|
||||
|
||||
return {
|
||||
state,
|
||||
@@ -212,7 +245,7 @@ export function useScheduler(slug: string, enabled = true) {
|
||||
locale,
|
||||
availableDates,
|
||||
slotsForSelectedDate,
|
||||
maxMonthOffset,
|
||||
availableMonthOffsets,
|
||||
selectDate,
|
||||
selectSlot,
|
||||
goBackToDate,
|
||||
|
||||
Reference in new issue
Block a user