import { useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import type { Service } from "@groombook/types"; // ─── Types ─────────────────────────────────────────────────────────────────── interface BookingBody { serviceId: string; startTime: string; clientName: string; clientEmail: string; clientPhone: string; petName: string; petSpecies: string; petBreed: string; notes: string; } interface BookingResult { appointment: { id: string; startTime: string; endTime: string }; client: { id: string; name: string; email: string | null }; pet: { id: string; name: string }; } // ─── Helpers ───────────────────────────────────────────────────────────────── function fmtPrice(cents: number): string { return `$${(cents / 100).toFixed(2)}`; } function fmtDuration(minutes: number): string { if (minutes < 60) return `${minutes} min`; const h = Math.floor(minutes / 60); const m = minutes % 60; return m > 0 ? `${h}h ${m}m` : `${h}h`; } function fmtTime(iso: string): string { return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } function fmtDateLong(isoDate: string): string { const d = new Date(isoDate + "T12:00:00Z"); return d.toLocaleDateString([], { weekday: "long", year: "numeric", month: "long", day: "numeric" }); } function todayIso(): string { return new Date().toISOString().slice(0, 10); } // ─── Sub-components ─────────────────────────────────────────────────────────── function StepIndicator({ step }: { step: number }) { const steps = ["Service", "Date & Time", "Your Info", "Confirm"]; return (
{steps.map((label, i) => { const idx = i + 1; const active = idx === step; const done = idx < step; return (
{done ? "✓" : idx} {label}
); })}
); } // ─── Main Component ─────────────────────────────────────────────────────────── export function BookPage() { const [step, setStep] = useState(1); // Step 1 — service const [services, setServices] = useState([]); const [servicesLoading, setServicesLoading] = useState(true); const [selectedService, setSelectedService] = useState(null); // Step 2 — date & time const [date, setDate] = useState(todayIso()); const [dateError, setDateError] = useState(null); const [slots, setSlots] = useState([]); const [slotsLoading, setSlotsLoading] = useState(false); const [selectedSlot, setSelectedSlot] = useState(null); // Step 3 — contact info const [form, setForm] = useState({ serviceId: "", startTime: "", clientName: "", clientEmail: "", clientPhone: "", petName: "", petSpecies: "", petBreed: "", notes: "", }); const [formError, setFormError] = useState(null); // Pre-fill form from URL params (e.g., ?clientName=Jane&clientEmail=jane@example.com) const [searchParams] = useSearchParams(); useEffect(() => { const clientName = searchParams.get("clientName"); const clientEmail = searchParams.get("clientEmail"); const clientPhone = searchParams.get("clientPhone"); const petName = searchParams.get("petName"); const petSpecies = searchParams.get("petSpecies"); const petBreed = searchParams.get("petBreed"); if (clientName || clientEmail || clientPhone || petName || petSpecies || petBreed) { setForm((f) => ({ ...f, ...(clientName && { clientName }), ...(clientEmail && { clientEmail }), ...(clientPhone && { clientPhone }), ...(petName && { petName }), ...(petSpecies && { petSpecies }), ...(petBreed && { petBreed }), })); } }, [searchParams]); // Step 4 — result const [submitting, setSubmitting] = useState(false); const [result, setResult] = useState(null); const [submitError, setSubmitError] = useState(null); // Load services on mount useEffect(() => { fetch("/api/book/services") .then((r) => r.json() as Promise) .then(setServices) .catch(() => setServices([])) .finally(() => setServicesLoading(false)); }, []); // Load slots when service or date changes (step 2) useEffect(() => { if (!selectedService || !date) return; setSlotsLoading(true); setSelectedSlot(null); fetch( `/api/book/availability?serviceId=${encodeURIComponent(selectedService.id)}&date=${encodeURIComponent(date)}` ) .then((r) => r.json() as Promise) .then(setSlots) .catch(() => setSlots([])) .finally(() => setSlotsLoading(false)); }, [selectedService, date]); function goToStep2(svc: Service) { setSelectedService(svc); setForm((f) => ({ ...f, serviceId: svc.id })); setStep(2); } function goToStep3() { if (!selectedSlot) return; setForm((f) => ({ ...f, startTime: selectedSlot })); setStep(3); } function goToStep4() { if (!form.clientName.trim() || !form.clientEmail.trim() || !form.petName.trim() || !form.petSpecies.trim()) { setFormError("Please fill in all required fields."); return; } setFormError(null); setStep(4); } async function submitBooking() { setSubmitting(true); setSubmitError(null); try { const res = await fetch("/api/book/appointments", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ serviceId: form.serviceId, startTime: form.startTime, clientName: form.clientName, clientEmail: form.clientEmail, clientPhone: form.clientPhone || undefined, petName: form.petName, petSpecies: form.petSpecies, petBreed: form.petBreed || undefined, notes: form.notes || undefined, }), }); if (!res.ok) { const body = (await res.json()) as { error?: string }; throw new Error(body.error ?? `HTTP ${res.status}`); } const data = (await res.json()) as BookingResult; setResult(data); setStep(5); } catch (e: unknown) { setSubmitError(e instanceof Error ? e.message : "Something went wrong. Please try again."); } finally { setSubmitting(false); } } // ── Styles ── const card: React.CSSProperties = { background: "#fff", border: "1px solid #e5e7eb", borderRadius: 8, padding: "1rem", cursor: "pointer", }; const selectedCard: React.CSSProperties = { ...card, border: "2px solid var(--color-primary)", background: "#f0faf5", }; const input: React.CSSProperties = { width: "100%", padding: "0.5rem 0.75rem", border: "1px solid #d1d5db", borderRadius: 6, fontSize: 14, boxSizing: "border-box", }; const label: React.CSSProperties = { display: "block", fontSize: 13, fontWeight: 600, color: "#374151", marginBottom: 4, }; const btn: React.CSSProperties = { padding: "0.6rem 1.25rem", borderRadius: 6, border: "none", cursor: "pointer", fontSize: 14, fontWeight: 600, }; const primaryBtn: React.CSSProperties = { ...btn, background: "var(--color-primary)", color: "#fff", }; const secondaryBtn: React.CSSProperties = { ...btn, background: "#f3f4f6", color: "#374151", }; return (

Book an Appointment

Schedule a grooming appointment for your pet in minutes.

{step < 5 && } {/* ── Step 1: Select Service ── */} {step === 1 && (

Choose a service

{servicesLoading &&

Loading services…

} {!servicesLoading && services.length === 0 && (

No services available. Please contact us to book.

)}
{services.map((svc) => (
goToStep2(svc)} role="button" tabIndex={0} onKeyDown={(e) => e.key === "Enter" && goToStep2(svc)} >
{svc.name}
{svc.description && (
{svc.description}
)}
{fmtPrice(svc.basePriceCents)}
{fmtDuration(svc.durationMinutes)}
))}
)} {/* ── Step 2: Date & Time ── */} {step === 2 && selectedService && (

Choose a date and time

{selectedService.name} — {fmtDuration(selectedService.durationMinutes)} — {fmtPrice(selectedService.basePriceCents)}

{ const val = e.target.value; // HTML5 date input enforces yyyy-MM-dd; empty value means invalid format if (!val) { setDateError("Please enter a valid date (YYYY-MM-DD)."); setDate(""); } else { setDateError(null); setDate(val); } }} /> {dateError && (

{dateError}

)}
{slotsLoading &&

Checking availability…

} {!slotsLoading && slots.length === 0 && (

No available slots on this date. Please try another day.

)} {!slotsLoading && slots.length > 0 && (
{slots.map((slot) => ( ))}
)}
)} {/* ── Step 3: Contact Info ── */} {step === 3 && (

Your information

Contact details
setForm((f) => ({ ...f, clientName: e.target.value }))} placeholder="Jane Smith" />
setForm((f) => ({ ...f, clientEmail: e.target.value }))} placeholder="jane@example.com" />
setForm((f) => ({ ...f, clientPhone: e.target.value }))} placeholder="(555) 000-1234" />
Pet details
setForm((f) => ({ ...f, petName: e.target.value }))} placeholder="Buddy" />
setForm((f) => ({ ...f, petBreed: e.target.value }))} placeholder="Golden Retriever" />