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 (
);
}
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 && (
)}
);
}