import { LanguagePicker, useT } from "@agent-native/core/client/i18n"; import { OpenSourceBadge, PoweredByBadge, StarfieldBackground, } from "@agent-native/core/client/ui"; import type { Booking } from "@shared/api"; import { IconAlertTriangle, IconCalendar } from "@tabler/icons-react"; import { addMinutes, endOfMonth, format, parseISO, startOfMonth, } from "date-fns"; import { useEffect, useState, type ReactNode } from "react"; import { useNavigate, useParams } from "react-router"; import { toast } from "sonner"; import { BookingConfirmation } from "@/components/booking/BookingConfirmation"; import { BookingForm, type BookingFormValue, } from "@/components/booking/BookingForm"; import { DatePicker } from "@/components/booking/DatePicker"; import { TimeSlotPicker } from "@/components/booking/TimeSlotPicker"; import { ThemeToggle } from "@/components/ThemeToggle"; import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import { useAvailableDays, useAvailableSlots, useCreateBooking, } from "@/hooks/use-bookings"; import { usePublicSettings, usePublicAvailability, usePublicBookingLink, } from "@/hooks/use-public-data"; import { cn } from "@/lib/utils"; type Step = "duration" | "date" | "time" | "info" | "confirmed"; const BRAND_LINK_CLASS = "font-semibold text-[#00B5FF] hover:text-[#33C4FF]"; function BookingPageShell({ children, className, }: { children: ReactNode; className?: string; }) { return (
{children}
); } export default function BookingPage() { const t = useT(); const { slug, username } = useParams<{ slug: string; username?: string }>(); const navigate = useNavigate(); const { data: settings, isLoading: settingsLoading } = usePublicSettings(); const { data: availability, isLoading: availabilityLoading } = usePublicAvailability(slug); const { data: bookingLink, isLoading: bookingLinkLoading, isError: bookingLinkError, } = usePublicBookingLink(slug, username); const isRedirecting = !!bookingLink && (!!bookingLink.redirectPath || !!bookingLink.redirect); // Handle slug redirects (old URL → new URL) useEffect(() => { if (bookingLink?.redirectPath) { navigate(bookingLink.redirectPath, { replace: true }); return; } if (!bookingLink?.redirect) return; const newSlug = bookingLink.redirect; const path = username ? `/book/${username}/${newSlug}` : `/book/${newSlug}`; navigate(path, { replace: true }); }, [bookingLink?.redirect, bookingLink?.redirectPath, username, navigate]); const [step, setStep] = useState("date"); const [selectedDate, setSelectedDate] = useState(null); const [selectedSlot, setSelectedSlot] = useState(null); const [confirmedBooking, setConfirmedBooking] = useState( null, ); const [selectedDuration, setSelectedDuration] = useState(null); const [viewMonth, setViewMonth] = useState(() => new Date()); const [bookingForm, setBookingForm] = useState({ name: "", email: "", notes: "", fieldResponses: {}, }); const dateStr = selectedDate ? format(selectedDate, "yyyy-MM-dd") : ""; const durationOptions = bookingLink?.durations && bookingLink.durations.length > 0 ? bookingLink.durations : null; const hasDurationChoice = !!durationOptions && durationOptions.length > 1; const bookingLinkDuration = durationOptions && durationOptions.length === 1 ? durationOptions[0] : bookingLink?.duration; const duration = selectedDuration ?? bookingLinkDuration ?? availability?.slotDurationMinutes ?? settings?.defaultEventDuration ?? 30; const { data: slots = [], isLoading: slotsLoading, error: slotsError, } = useAvailableSlots(dateStr, duration, slug); const monthStart = format(startOfMonth(viewMonth), "yyyy-MM-dd"); const monthEnd = format(endOfMonth(viewMonth), "yyyy-MM-dd"); const { data: availableDates = [], isLoading: availableDatesLoading, error: availableDatesError, } = useAvailableDays( monthStart, monthEnd, duration, slug, step === "date" && !!availability && (!hasDurationChoice || selectedDuration !== null), ); const createBooking = useCreateBooking(); const selectedSlotRange = selectedSlot ? { start: selectedSlot, end: slots.find((slot) => slot.start === selectedSlot)?.end ?? addMinutes(parseISO(selectedSlot), duration).toISOString(), } : null; function handleDateSelect(date: Date) { setSelectedDate(date); setSelectedSlot(null); setStep("time"); } function handleSlotSelect(start: string) { setSelectedSlot(start); setStep("info"); } function handleBookingSubmit(data: { name: string; email: string; notes?: string; captchaToken?: string; fieldResponses?: Record; }) { if (!selectedSlot || !slug) return; const slot = slots.find((s) => s.start === selectedSlot); if (!slot) return; createBooking.mutate( { name: data.name, email: data.email, notes: data.notes, captchaToken: data.captchaToken, fieldResponses: data.fieldResponses, start: slot.start, end: slot.end, slug, }, { onSuccess: (booking: Booking) => { setConfirmedBooking(booking); setStep("confirmed"); }, onError: (error) => toast.error( error instanceof Error ? error.message : t("bookingLinks.failedToCreateBooking"), ), }, ); } function handleReset() { setStep(hasDurationChoice ? "duration" : "date"); setSelectedDate(null); setSelectedSlot(null); setSelectedDuration(null); setConfirmedBooking(null); setBookingForm({ name: "", email: "", notes: "", fieldResponses: {} }); } function handleStepNavigation(target: Step) { if (target === step) return; if (target === "duration") { setSelectedDate(null); setSelectedSlot(null); setStep("duration"); return; } if (target === "date") { setSelectedSlot(null); setStep("date"); return; } if (target === "time" && selectedDate) { setSelectedSlot(null); setStep("time"); } } const title = settings?.bookingPageTitle || t("bookingLinks.bookAMeeting"); const description = settings?.bookingPageDescription || t("bookingLinks.defaultDescription"); const isLegacyBookingPage = !!slug && availability?.bookingPageSlug === slug; const pageTitle = bookingLink?.title || title; const pageDescription = bookingLink?.description || description; const requiredHostCount = (bookingLink?.hosts?.length ?? 0) + 1; const availabilityErrorMessage = t("bookingLinks.availabilityUnavailable"); useEffect(() => { if (hasDurationChoice && step === "date" && selectedDuration === null) { setStep("duration"); } else if (!hasDurationChoice && step === "duration") { setStep("date"); } }, [hasDurationChoice, selectedDuration, step]); if ( bookingLinkLoading || settingsLoading || availabilityLoading || isRedirecting ) { return (
); } if ((bookingLinkError || !bookingLink) && !isLegacyBookingPage) { return (

{t("bookingLinks.bookingLinkNotFound")}

{t("bookingLinks.meetingTypeUnavailable")}

); } return (
{/* Header */}

{pageTitle}

{pageDescription}

{(!hasDurationChoice || requiredHostCount > 1) && (
{!hasDurationChoice && ( {t("bookingLinks.minuteMeeting", { count: duration })} )} {requiredHostCount > 1 && ( {t("bookingLinks.requiredHostsCount", { count: requiredHostCount, })} )}
)}
{/* Steps */}
{/* Step indicators */} {step !== "confirmed" && (() => { const steps = hasDurationChoice ? (["duration", "date", "time", "info"] as const) : (["date", "time", "info"] as const); const currentStepIndex = (steps as readonly string[]).indexOf( step, ); const stepLabels: Record = { duration: t("bookingLinks.durationSelection"), date: t("bookingLinks.dateSelection"), time: t("bookingLinks.timeSelection"), info: t("bookingLinks.yourInformation"), confirmed: t("bookingLinks.confirmation"), }; return (
{steps.map((s, i) => { const isCurrent = step === s; const isPrevious = currentStepIndex > i; const isReachable = !isCurrent && (isPrevious || (s === "date" && !!selectedDuration) || (s === "time" && !!selectedDate) || (s === "info" && !!selectedSlot)); const circleClass = cn( "flex h-7 w-7 items-center justify-center rounded-full text-xs font-medium transition-colors", isCurrent ? "bg-primary text-primary-foreground" : isPrevious ? "bg-primary/20 text-primary" : "bg-muted text-muted-foreground", isReachable && "cursor-pointer hover:bg-primary/30 hover:text-primary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", ); return (
{isReachable ? ( ) : (
{i + 1}
)} {i < steps.length - 1 && (
)}
); })}
); })()} {step === "duration" && durationOptions && (

{t("bookingLinks.chooseDuration")}

{durationOptions.map((mins) => ( ))}
)} {step === "date" && availability && (

{t("bookingLinks.selectDate")}

{availableDatesError ? (

{availabilityErrorMessage}

) : (
)}
)} {step === "time" && (

{t("bookingLinks.selectTime")}

{selectedDate && (

{format(selectedDate, "EEEE, MMMM d, yyyy")}

)}
)} {step === "info" && (

{t("bookingLinks.yourInformation")}

{selectedSlotRange && (
{t("bookingLinks.confirming")}
{format(parseISO(selectedSlotRange.start), "EEEE, MMMM d")}
{format(parseISO(selectedSlotRange.start), "h:mm a")} -{" "} {format(parseISO(selectedSlotRange.end), "h:mm a")}
)}
)} {step === "confirmed" && confirmedBooking && ( )}
); }