'use client' import { useState, useEffect } from 'react' import { BASE } from '@/lib/basepath' import type { DeliveryTier } from '@/lib/delivery' import type { TimeSlot } from '@/lib/slots' import type { HoursConfig } from '@/lib/hours-config' import CalendarPicker from './CalendarPicker' export interface DeliverySelection { date: string // YYYY-MM-DD slotISO: string // UTC ISO start time label: string // human label e.g. "10:00 AM" driveMinutes: number } interface Props { address: string tier: DeliveryTier value: DeliverySelection | null onChange: (v: DeliverySelection | null) => void } type SlotState = 'idle' | 'loading' | 'loaded' | 'error' const maxDate = new Date(Date.now() + 90 * 86400_000).toISOString().slice(0, 10) export default function DeliveryDatePicker({ address, tier, value, onChange }: Props) { const [date, setDate] = useState(value?.date ?? '') const [slots, setSlots] = useState([]) const [drive, setDrive] = useState(0) const [status, setStatus] = useState('idle') const [errMsg, setErrMsg] = useState('') const [leadTimeHours, setLeadTimeHours] = useState(48) // Dates that returned 0 slots — shown as strikethrough on the calendar const [noSlotDates, setNoSlotDates] = useState>(new Set()) // Busy dates from CalDAV — shown with an orange dot const [busyDates, setBusyDates] = useState>(new Set()) const minDate = new Date(Date.now() + leadTimeHours * 60 * 60 * 1000).toISOString().slice(0, 10) // Pre-mark all closed delivery days from admin hours config useEffect(() => { fetch(`${BASE}/api/hours`) .then(r => r.ok ? r.json() : null) .then((config: HoursConfig | null) => { if (!config) return const lead = config.leadTimeHours ?? 48 setLeadTimeHours(lead) const closed = new Set() const start = Date.now() + lead * 60 * 60 * 1000 for (let i = 0; i < 90; i++) { const dateStr = new Date(start + i * 86400_000).toISOString().slice(0, 10) const dow = new Date(`${dateStr}T12:00:00Z`).getUTCDay() if (!config.delivery[String(dow)]) closed.add(dateStr) } setNoSlotDates(closed) }) .catch(() => {/* non-fatal */}) }, []) // Fetch busy dates once on mount useEffect(() => { if (!process.env.NEXT_PUBLIC_SITE_URL && typeof window === 'undefined') return fetch(`${BASE}/api/availability?days=90`) .then(r => r.ok ? r.json() : null) .then((data) => { if (!data?.busy) return const dates = new Set( (data.busy as Array<{ start: string }>).map(b => b.start.slice(0, 10)) ) setBusyDates(dates) }) .catch(() => {/* non-fatal */}) }, []) // Fetch slots when date or address changes useEffect(() => { if (!date || !address) return setStatus('loading') setSlots([]) onChange(null) const params = new URLSearchParams({ date, address, tier }) fetch(`${BASE}/api/slots?${params}`) .then((r) => r.json()) .then((data) => { if (data.error) { setErrMsg(data.error); setStatus('error'); return } const fetched: TimeSlot[] = data.slots ?? [] setSlots(fetched) setDrive(data.driveMinutes ?? 0) setStatus('loaded') if (fetched.length === 0) { setNoSlotDates(prev => { const s = new Set(prev); s.add(date); return s }) } }) .catch(() => { setErrMsg('Could not load time slots.'); setStatus('error') }) }, [date, address, tier]) // eslint-disable-line react-hooks/exhaustive-deps const selectSlot = (slot: TimeSlot) => { onChange({ date, slotISO: slot.startISO, label: slot.label, driveMinutes: drive }) } return (

Delivery date & time

{ setDate(d); onChange(null) }} minDate={minDate} maxDate={maxDate} disabledDates={noSlotDates} busyDates={busyDates} />
{status === 'loading' && (

Checking availability…

)} {status === 'error' && (

{errMsg}{' '} Contact us {' '} to book manually.

)} {status === 'loaded' && slots.length === 0 && (

No availability on this date — please try another day, or{' '} contact us .

)} {status === 'loaded' && slots.length > 0 && ( <>

~{drive} min drive  ·  Please reserve at least 1 hour at your venue  ·  select a start time

{slots.map((slot) => { const chosen = value?.slotISO === slot.startISO return ( ) })}
)}
) }