// 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 { useCallback, useState } 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 { BookerEvent } from "@calcom/features/bookings/types"; import type { Slots } from "@calcom/features/schedules"; import { classNames } from "@calcom/lib"; 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 { useBookerStore } from "../Booker/store"; import { getQueryParam } from "../Booker/utils/query-param"; import { useTimePreferences } from "../lib"; import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay"; import { SeatsAvailabilityText } from "./SeatsAvailabilityText"; type TOnTimeSelect = ( time: string, attendees: number, seatsPerTimeSlot?: number | null, bookingUid?: string ) => void; type AvailableTimesProps = { slots: IGetAvailableSlots["slots"][string]; onTimeSelect: TOnTimeSelect; seatsPerTimeSlot?: number | null; showAvailableSeatsCount?: boolean | null; showTimeFormatToggle?: boolean; className?: string; selectedSlots?: string[]; event: { data?: Pick | null; }; customClassNames?: string; }; const SlotItem = ({ slot, seatsPerTimeSlot, selectedSlots, onTimeSelect, showAvailableSeatsCount, event, customClassNames, }: { slot: Slots[string][number]; seatsPerTimeSlot?: number | null; selectedSlots?: string[]; onTimeSelect: TOnTimeSelect; showAvailableSeatsCount?: boolean | null; event: { data?: Pick | null; }; customClassNames?: string; }) => { const { t } = useLocale(); const overlayCalendarToggled = getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault"); const [timeFormat, timezone] = useTimePreferences((state) => [state.timeFormat, state.timezone]); const bookingData = useBookerStore((state) => state.bookingData); const layout = useBookerStore((state) => state.layout); const { data: eventData } = event; 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 { isOverlapping, overlappingTimeEnd, overlappingTimeStart } = useCheckOverlapWithOverlay({ start: computedDateWithUsersTimezone, selectedDuration: eventData?.length ?? 0, offset, }); const [overlapConfirm, setOverlapConfirm] = useState(false); const onButtonClick = useCallback(() => { if (!overlayCalendarToggled) { onTimeSelect(slot.time, slot?.attendees || 0, seatsPerTimeSlot, slot.bookingUid); return; } if (isOverlapping && overlapConfirm) { setOverlapConfirm(false); return; } if (isOverlapping && !overlapConfirm) { setOverlapConfirm(true); return; } if (!overlapConfirm) { onTimeSelect(slot.time, slot?.attendees || 0, seatsPerTimeSlot, slot.bookingUid); } }, [ overlayCalendarToggled, isOverlapping, overlapConfirm, onTimeSelect, slot.time, slot?.attendees, slot.bookingUid, seatsPerTimeSlot, ]); return (
{overlapConfirm && isOverlapping && (

Busy

{overlappingTimeStart} - {overlappingTimeEnd}

)}
); }; export const AvailableTimes = ({ slots, onTimeSelect, seatsPerTimeSlot, showAvailableSeatsCount, showTimeFormatToggle = true, className, selectedSlots, event, customClassNames, }: 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) => ( ))}
);