import { useAutoAnimate } from "@formkit/auto-animate/react"; import Link from "next/link"; import { useRouter } from "next/router"; import type { FC } from "react"; import { useEffect, useState, useCallback } from "react"; import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import { TimeFormat } from "@calcom/lib/timeFormat"; import { nameOfDay } from "@calcom/lib/weekday"; import { trpc } from "@calcom/trpc/react"; import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types"; import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui"; import classNames from "@lib/classNames"; import { timeZone } from "@lib/clock"; type AvailableTimesProps = { timeFormat: TimeFormat; onTimeFormatChange: (is24Hour: boolean) => void; eventTypeId: number; recurringCount: number | undefined; eventTypeSlug: string; date?: Dayjs; seatsPerTimeSlot?: number | null; bookingAttendees?: number | null; slots?: Slot[]; isLoading: boolean; duration: number; }; const AvailableTimes: FC = ({ slots = [], isLoading, date, eventTypeId, eventTypeSlug, recurringCount, timeFormat, onTimeFormatChange, seatsPerTimeSlot, bookingAttendees, duration, }) => { const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation(); const [slotPickerRef] = useAutoAnimate(); const { t, i18n } = useLocale(); const router = useRouter(); const { rescheduleUid } = router.query; const [brand, setBrand] = useState("#292929"); useEffect(() => { setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim()); }, []); const isMobile = useMediaQuery("(max-width: 768px)"); const ref = useCallback( (node: HTMLDivElement) => { if (isMobile) { node?.scrollIntoView({ behavior: "smooth" }); } }, [isMobile] ); const reserveSlot = (slot: Slot) => { reserveSlotMutation.mutate({ slotUtcStartDate: slot.time, eventTypeId, slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(), bookingUid: slot.bookingUid, }); }; return (
{!!date ? (
{nameOfDay(i18n.language, Number(date.format("d")), "short")} , {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
onTimeFormatChange(timeFormat === "24")} defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"} options={[ { value: "12", label: t("12_hour_short") }, { value: "24", label: t("24_hour_short") }, ]} />
{slots.length > 0 && slots.map((slot) => { type BookingURL = { pathname: string; query: Record; }; const bookingUrl: BookingURL = { pathname: router.pathname.endsWith("/embed") ? "../book" : "book", query: { ...router.query, date: dayjs.utc(slot.time).tz(timeZone()).format(), type: eventTypeId, slug: eventTypeSlug, timeFormat, /** Treat as recurring only when a count exist and it's not a rescheduling workflow */ count: recurringCount && !rescheduleUid ? recurringCount : undefined, }, }; if (rescheduleUid) { bookingUrl.query.rescheduleUid = rescheduleUid as string; } // If event already has an attendee add booking id if (slot.bookingUid) { bookingUrl.query.bookingUid = slot.bookingUid; } let slotFull, notEnoughSeats; if (slot.attendees && seatsPerTimeSlot) slotFull = slot.attendees >= seatsPerTimeSlot; if (slot.attendees && bookingAttendees && seatsPerTimeSlot) notEnoughSeats = slot.attendees + bookingAttendees > seatsPerTimeSlot; return (
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */} {/* Current there is no way to disable Next.js Links */} {seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? (
{dayjs(slot.time).tz(timeZone()).format(timeFormat)} {notEnoughSeats ? (

{t("not_enough_seats")}

) : slots ? (

{t("booking_full")}

) : null}
) : ( reserveSlot(slot)} data-testid="time" data-disabled="false"> {dayjs(slot.time).tz(timeZone()).format(timeFormat)} {!!seatsPerTimeSlot && (

= 0.8 ? "text-rose-600" : slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33 ? "text-yellow-500" : "text-emerald-400" } text-sm`}> {slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "} {seatsPerTimeSlot}{" "} {t("seats_available", { count: slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot, })}

)} )}
); })} {!isLoading && !slots.length && (

{t("all_booked_today")}

)} {isLoading && !slots.length && ( <> )}
) : null}
); }; AvailableTimes.displayName = "AvailableTimes"; export default AvailableTimes;