// 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 { useIsPlatform } from "@calcom/atoms/monorepo"; import type { IOutOfOfficeData } from "@calcom/core/getUserAvailability"; import dayjs from "@calcom/dayjs"; import { OutOfOfficeInSlots } from "@calcom/features/bookings/Booker/components/OutOfOfficeInSlots"; import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings"; import type { BookerEvent } from "@calcom/features/bookings/types"; import type { Slot } from "@calcom/features/schedules"; import { classNames } from "@calcom/lib"; import { getPaymentAppData } from "@calcom/lib/getPaymentAppData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { localStorage } from "@calcom/lib/webstorage"; import type { IGetAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util"; import { Button, Icon, SkeletonText } from "@calcom/ui"; import { useBookerTime } from "../Booker/components/hooks/useBookerTime"; import { useBookerStore } from "../Booker/store"; import { getQueryParam } from "../Booker/utils/query-param"; import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay"; import { SeatsAvailabilityText } from "./SeatsAvailabilityText"; type TOnTimeSelect = ( time: string, attendees: number, seatsPerTimeSlot?: number | null, bookingUid?: string ) => void; export type AvailableTimesProps = { slots: IGetAvailableSlots["slots"][string]; showTimeFormatToggle?: boolean; className?: string; } & Omit; type SlotItemProps = { slot: Slot; seatsPerTimeSlot?: number | null; selectedSlots?: string[]; onTimeSelect: TOnTimeSelect; showAvailableSeatsCount?: boolean | null; event: { data?: Pick | null; }; customClassNames?: string; loadingStates?: IUseBookingLoadingStates; isVerificationCodeSending?: boolean; renderConfirmNotVerifyEmailButtonCond?: boolean; skipConfirmStep?: boolean; shouldRenderCaptcha?: boolean; watchedCfToken?: string; handleSlotClick?: (slot: Slot, isOverlapping: boolean) => void; }; const SlotItem = ({ slot, seatsPerTimeSlot, selectedSlots, onTimeSelect, showAvailableSeatsCount, event, customClassNames, loadingStates, renderConfirmNotVerifyEmailButtonCond, isVerificationCodeSending, skipConfirmStep, shouldRenderCaptcha, watchedCfToken, handleSlotClick, }: 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 = useBookerStore((state) => state.bookingData); const layout = useBookerStore((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 = useBookerStore((state) => state.selectedTimeslot); const { isOverlapping, overlappingTimeEnd, overlappingTimeStart } = useCheckOverlapWithOverlay({ start: computedDateWithUsersTimezone, selectedDuration: eventData?.length ?? 0, offset, }); 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; time?: string; className?: string; } const OOOSlot: React.FC = (props) => { const isPlatform = useIsPlatform(); const { fromUser, toUser, reason, emoji, 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) => ( ))}
);