// We do not need to worry about importing framer-motion here as it is lazy imported in Booker. import * as HoverCard from "@radix-ui/react-hover-card"; import { AnimatePresence, m } from "framer-motion"; import { useMemo } from "react"; import { getPaymentAppData } from "@calcom/app-store/_utils/payments/getPaymentAppData"; import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; import dayjs from "@calcom/dayjs"; import type { IOutOfOfficeData } from "@calcom/features/availability/lib/getUserAvailability"; import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider"; import { OutOfOfficeInSlots } from "./OutOfOfficeInSlots"; import type { IUseBookingLoadingStates } from "../hooks/useBookings"; import type { BookerEvent } from "@calcom/features/bookings/types"; import type { Slot } from "~/schedules/lib/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { localStorage } from "@calcom/lib/webstorage"; import classNames from "@calcom/ui/classNames"; import { Button } from "@calcom/ui/components/button"; import { SkeletonText } from "@calcom/ui/components/skeleton"; import { CalendarX2Icon } from "@coss/ui/icons"; import { useBookerTime } from "@calcom/features/bookings/Booker/hooks/useBookerTime"; import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param"; import { useCheckOverlapWithOverlay } from "@calcom/features/bookings/lib/useCheckOverlapWithOverlay"; import type { Slots } from "@calcom/features/bookings/types"; import { SeatsAvailabilityText } from "./SeatsAvailabilityText"; type TOnTimeSelect = ( time: string, attendees: number, seatsPerTimeSlot?: number | null, bookingUid?: string ) => void; type TOnTentativeTimeSelect = ({ time, attendees, seatsPerTimeSlot, bookingUid, }: { time: string; attendees: number; seatsPerTimeSlot?: number | null; bookingUid?: string; }) => void; export type AvailableTimesProps = { slots: Slots[string]; showTimeFormatToggle?: boolean; className?: string; // It is called when a slot is selected, but it is not a confirmation and a confirm button will be shown besides it. onTentativeTimeSelect?: TOnTentativeTimeSelect; unavailableTimeSlots?: string[]; } & Omit; type SlotItemProps = { slot: Slot; seatsPerTimeSlot?: number | null; selectedSlots?: string[]; onTimeSelect?: TOnTimeSelect; onTentativeTimeSelect?: TOnTentativeTimeSelect; showAvailableSeatsCount?: boolean | null; event: { data?: Pick | null; }; customClassNames?: string; confirmStepClassNames?: { confirmButton?: string; }; loadingStates?: IUseBookingLoadingStates; isVerificationCodeSending?: boolean; renderConfirmNotVerifyEmailButtonCond?: boolean; skipConfirmStep?: boolean; shouldRenderCaptcha?: boolean; watchedCfToken?: string; unavailableTimeSlots?: string[]; confirmButtonDisabled?: boolean; handleSlotClick?: (slot: Slot, isOverlapping: boolean) => void; }; const SlotItem = ({ slot, seatsPerTimeSlot, selectedSlots, onTimeSelect, showAvailableSeatsCount, event, customClassNames, loadingStates, renderConfirmNotVerifyEmailButtonCond, isVerificationCodeSending, skipConfirmStep, shouldRenderCaptcha, watchedCfToken, handleSlotClick, onTentativeTimeSelect, unavailableTimeSlots = [], confirmButtonDisabled, confirmStepClassNames, }: SlotItemProps) => { const { t } = useLocale(); const { data: eventData } = event; const isPaidEvent = useMemo(() => { if (!eventData?.price) return false; const paymentAppData = getPaymentAppData(eventData); return eventData?.price > 0 && !Number.isNaN(paymentAppData.price) && paymentAppData.price > 0; }, [eventData]); const overlayCalendarToggled = getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault"); const { timeFormat, timezone } = useBookerTime(); const bookingData = useBookerStoreContext((state) => state.bookingData); const layout = useBookerStoreContext((state) => state.layout); const hasTimeSlots = !!seatsPerTimeSlot; const computedDateWithUsersTimezone = dayjs.utc(slot.time).tz(timezone); const bookingFull = !!(hasTimeSlots && slot.attendees && slot.attendees >= seatsPerTimeSlot); const isHalfFull = slot.attendees && seatsPerTimeSlot && slot.attendees / seatsPerTimeSlot >= 0.5; const isNearlyFull = slot.attendees && seatsPerTimeSlot && slot.attendees / seatsPerTimeSlot >= 0.83; const colorClass = isNearlyFull ? "bg-rose-600" : isHalfFull ? "bg-yellow-500" : "bg-emerald-400"; const nowDate = dayjs(); const usersTimezoneDate = nowDate.tz(timezone); const offset = (usersTimezoneDate.utcOffset() - nowDate.utcOffset()) / 60; const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot); const { isOverlapping, overlappingTimeEnd, overlappingTimeStart } = useCheckOverlapWithOverlay({ start: computedDateWithUsersTimezone, selectedDuration: eventData?.length ?? 0, offset, }); const onButtonClick = () => { if (handleSlotClick) { handleSlotClick(slot, isOverlapping); } if (onTentativeTimeSelect) { onTentativeTimeSelect({ time: slot.time, attendees: slot.attendees || 0, seatsPerTimeSlot, }); } }; const isTimeslotUnavailable = unavailableTimeSlots.includes(slot.time); return (
{!!slot.showConfirmButton && ( {isOverlapping && (

Busy

{overlappingTimeStart} - {overlappingTimeEnd}

)}
)}
); }; export const AvailableTimes = ({ slots, showTimeFormatToggle = true, className, ...props }: AvailableTimesProps) => { const { t } = useLocale(); const oooAllDay = slots.every((slot) => slot.away); if (oooAllDay) { return ; } // Display ooo in slots once but after or before slots const oooBeforeSlots = slots[0] && slots[0].away; const oooAfterSlots = slots[slots.length - 1] && slots[slots.length - 1].away; return (
{!slots.length && (

{t("all_booked_today")}

)} {oooBeforeSlots && !oooAfterSlots && } {slots.map((slot) => { if (slot.away) return null; return ; })} {oooAfterSlots && !oooBeforeSlots && }
); }; interface IOOOSlotProps { fromUser?: IOutOfOfficeData["anyDate"]["fromUser"]; toUser?: IOutOfOfficeData["anyDate"]["toUser"]; reason?: string; emoji?: string; notes?: string | null; showNotePublicly?: boolean; time?: string; className?: string; } const OOOSlot: React.FC = (props) => { const isPlatform = useIsPlatform(); const { fromUser, toUser, reason, emoji, notes, showNotePublicly, time, className = "" } = props; if (isPlatform) return <>; return ( ); }; export const AvailableTimesSkeleton = () => (
{/* Random number of elements between 1 and 6. */} {Array.from({ length: Math.floor(Math.random() * 6) + 1 }).map((_, i) => ( ))}
);