2023-02-17 19:53:31 +00:00
import { useAutoAnimate } from "@formkit/auto-animate/react" ;
2021-06-19 22:50:47 +00:00
import Link from "next/link" ;
2021-06-24 22:15:18 +00:00
import { useRouter } from "next/router" ;
2023-02-16 22:39:57 +00:00
import type { FC } from "react" ;
2023-02-24 00:19:23 +00:00
import { useEffect , useState , useCallback } from "react" ;
2021-09-22 19:52:38 +00:00
2023-02-16 22:39:57 +00:00
import type { Dayjs } from "@calcom/dayjs" ;
import dayjs from "@calcom/dayjs" ;
2022-06-15 20:54:31 +00:00
import { useLocale } from "@calcom/lib/hooks/useLocale" ;
2023-02-24 00:19:23 +00:00
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery" ;
2022-10-25 00:14:06 +00:00
import { TimeFormat } from "@calcom/lib/timeFormat" ;
2022-03-27 19:48:13 +00:00
import { nameOfDay } from "@calcom/lib/weekday" ;
2023-04-13 19:55:26 +00:00
import { trpc } from "@calcom/trpc/react" ;
2023-04-25 22:39:47 +00:00
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types" ;
2022-11-23 02:55:25 +00:00
import { SkeletonContainer , SkeletonText , ToggleGroup } from "@calcom/ui" ;
2022-03-27 19:48:13 +00:00
2021-12-15 10:26:39 +00:00
import classNames from "@lib/classNames" ;
2022-05-05 18:51:22 +00:00
import { timeZone } from "@lib/clock" ;
2021-09-22 19:52:38 +00:00
2021-09-23 17:18:29 +00:00
type AvailableTimesProps = {
2022-10-25 00:14:06 +00:00
timeFormat : TimeFormat ;
onTimeFormatChange : ( is24Hour : boolean ) = > void ;
2021-09-23 17:18:29 +00:00
eventTypeId : number ;
2022-05-05 21:16:25 +00:00
recurringCount : number | undefined ;
2022-04-06 17:20:30 +00:00
eventTypeSlug : string ;
2022-10-10 13:24:06 +00:00
date? : Dayjs ;
2022-05-24 13:19:12 +00:00
seatsPerTimeSlot? : number | null ;
2023-03-14 04:19:05 +00:00
bookingAttendees? : number | null ;
2022-06-15 20:54:31 +00:00
slots? : Slot [ ] ;
2022-06-27 21:01:46 +00:00
isLoading : boolean ;
2023-04-13 19:55:26 +00:00
duration : number ;
2021-09-23 17:18:29 +00:00
} ;
const AvailableTimes : FC < AvailableTimesProps > = ( {
2022-06-15 20:54:31 +00:00
slots = [ ] ,
2022-06-27 21:01:46 +00:00
isLoading ,
2021-07-08 21:14:29 +00:00
date ,
eventTypeId ,
2022-04-06 17:20:30 +00:00
eventTypeSlug ,
2022-05-05 21:16:25 +00:00
recurringCount ,
2021-07-08 21:14:29 +00:00
timeFormat ,
2022-10-25 00:14:06 +00:00
onTimeFormatChange ,
2022-05-24 13:19:12 +00:00
seatsPerTimeSlot ,
2023-03-14 04:19:05 +00:00
bookingAttendees ,
2023-04-13 19:55:26 +00:00
duration ,
2021-07-08 21:14:29 +00:00
} ) = > {
2023-04-13 19:55:26 +00:00
const reserveSlotMutation = trpc . viewer . public . slots . reserveSlot . useMutation ( ) ;
2023-02-17 19:53:31 +00:00
const [ slotPickerRef ] = useAutoAnimate < HTMLDivElement > ( ) ;
2021-12-20 11:55:49 +00:00
const { t , i18n } = useLocale ( ) ;
2021-06-19 22:50:47 +00:00
const router = useRouter ( ) ;
2021-06-27 22:30:11 +00:00
const { rescheduleUid } = router . query ;
2021-07-08 21:14:29 +00:00
2021-12-15 10:26:39 +00:00
const [ brand , setBrand ] = useState ( "#292929" ) ;
useEffect ( ( ) = > {
setBrand ( getComputedStyle ( document . documentElement ) . getPropertyValue ( "--brand-color" ) . trim ( ) ) ;
} , [ ] ) ;
2023-02-24 00:19:23 +00:00
const isMobile = useMediaQuery ( "(max-width: 768px)" ) ;
const ref = useCallback (
( node : HTMLDivElement ) = > {
if ( isMobile ) {
node ? . scrollIntoView ( { behavior : "smooth" } ) ;
}
} ,
[ isMobile ]
) ;
2021-12-15 10:26:39 +00:00
2023-04-13 19:55:26 +00:00
const reserveSlot = ( slot : Slot ) = > {
2023-07-11 15:11:08 +00:00
// Prevent double clicking
if ( reserveSlotMutation . isLoading || reserveSlotMutation . isSuccess ) {
return ;
}
2023-04-13 19:55:26 +00:00
reserveSlotMutation . mutate ( {
slotUtcStartDate : slot.time ,
eventTypeId ,
slotUtcEndDate : dayjs ( slot . time ) . utc ( ) . add ( duration , "minutes" ) . format ( ) ,
2023-05-22 09:30:24 +00:00
bookingUid : slot.bookingUid ,
2023-04-13 19:55:26 +00:00
} ) ;
} ;
2021-06-19 22:50:47 +00:00
return (
2023-02-17 19:53:31 +00:00
< div ref = { slotPickerRef } >
{ ! ! date ? (
2023-04-05 18:14:46 +00:00
< div className = "mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]" >
2023-03-10 13:02:48 +00:00
< div className = "mb-4 flex items-center text-left text-base" >
2023-02-17 19:53:31 +00:00
< div className = "mr-4" >
2023-04-05 18:14:46 +00:00
< span className = "text-emphasis font-semibold" >
2023-02-17 19:53:31 +00:00
{ nameOfDay ( i18n . language , Number ( date . format ( "d" ) ) , "short" ) }
< / span >
2023-04-05 18:14:46 +00:00
< span className = "text-subtle" >
2023-02-17 19:53:31 +00:00
, { date . toDate ( ) . toLocaleString ( i18n . language , { month : "short" } ) } { date . format ( " D " ) }
< / span >
< / div >
< div className = "ml-auto" >
< ToggleGroup
onValueChange = { ( timeFormat ) = > 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" ) } ,
] }
/ >
< / div >
< / div >
2023-03-10 13:02:48 +00:00
< div
ref = { ref }
className = "scroll-bar scrollbar-track-w-20 relative -mb-4 flex-grow overflow-y-auto sm:block md:h-[364px]" >
2023-02-17 19:53:31 +00:00
{ slots . length > 0 &&
slots . map ( ( slot ) = > {
type BookingURL = {
pathname : string ;
2023-03-22 10:15:16 +00:00
query : Record < string , string | number | string [ ] | undefined | TimeFormat > ;
2023-02-17 19:53:31 +00:00
} ;
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 ,
2023-03-22 10:15:16 +00:00
timeFormat ,
2023-02-17 19:53:31 +00:00
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
count : recurringCount && ! rescheduleUid ? recurringCount : undefined ,
} ,
} ;
2021-09-14 08:45:28 +00:00
2023-02-17 19:53:31 +00:00
if ( rescheduleUid ) {
bookingUrl . query . rescheduleUid = rescheduleUid as string ;
}
2021-09-14 08:45:28 +00:00
2023-02-17 19:53:31 +00:00
// If event already has an attendee add booking id
if ( slot . bookingUid ) {
bookingUrl . query . bookingUid = slot . bookingUid ;
}
2022-05-24 13:19:12 +00:00
2023-03-14 04:19:05 +00:00
let slotFull , notEnoughSeats ;
if ( slot . attendees && seatsPerTimeSlot ) slotFull = slot . attendees >= seatsPerTimeSlot ;
2023-07-11 15:11:08 +00:00
if ( slot . attendees && bookingAttendees && seatsPerTimeSlot ) {
2023-03-14 04:19:05 +00:00
notEnoughSeats = slot . attendees + bookingAttendees > seatsPerTimeSlot ;
2023-07-11 15:11:08 +00:00
}
const isHalfFull =
slot . attendees && seatsPerTimeSlot && slot . attendees / seatsPerTimeSlot >= 0.5 ;
const isNearlyFull =
slot . attendees && seatsPerTimeSlot && slot . attendees / seatsPerTimeSlot >= 0.83 ;
const colorClass = isNearlyFull
? "text-rose-600"
: isHalfFull
? "text-yellow-500"
: "text-emerald-400" ;
2023-03-14 04:19:05 +00:00
2023-02-17 19:53:31 +00:00
return (
< div data - slot - owner = { ( slot . userIds || [ ] ) . join ( "," ) } key = { ` ${ dayjs ( slot . time ) . format ( ) } ` } >
{ /* ^ 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 */ }
2023-03-14 04:19:05 +00:00
{ seatsPerTimeSlot && slot . attendees && ( slotFull || notEnoughSeats ) ? (
2023-02-17 19:53:31 +00:00
< div
className = { classNames (
2023-04-05 18:14:46 +00:00
"text-default bg-default border-subtle mb-2 block rounded-sm border py-2 font-medium opacity-25" ,
2023-02-17 19:53:31 +00:00
brand === "#fff" || brand === "#ffffff" ? "" : ""
2023-07-11 15:11:08 +00:00
) }
data - testid = "time"
data - disabled = "true" >
2023-02-17 19:53:31 +00:00
{ dayjs ( slot . time ) . tz ( timeZone ( ) ) . format ( timeFormat ) }
2023-03-14 04:19:05 +00:00
{ notEnoughSeats ? (
< p className = "text-sm" > { t ( "not_enough_seats" ) } < / p >
) : slots ? (
< p className = "text-sm" > { t ( "booking_full" ) } < / p >
) : null }
2023-02-17 19:53:31 +00:00
< / div >
) : (
< Link
href = { bookingUrl }
prefetch = { false }
className = { classNames (
2023-04-05 18:14:46 +00:00
" bg-default dark:bg-muted border-default hover:bg-subtle hover:border-brand-default text-emphasis mb-2 block rounded-md border py-2 text-sm font-medium" ,
2023-02-17 19:53:31 +00:00
brand === "#fff" || brand === "#ffffff" ? "" : ""
) }
2023-04-13 19:55:26 +00:00
onClick = { ( ) = > reserveSlot ( slot ) }
2023-06-10 03:47:50 +00:00
data - testid = "time"
data - disabled = "false" >
2023-02-17 19:53:31 +00:00
{ dayjs ( slot . time ) . tz ( timeZone ( ) ) . format ( timeFormat ) }
{ ! ! seatsPerTimeSlot && (
2023-07-11 15:11:08 +00:00
< p className = { ` ${ colorClass } text-sm ` } >
2023-02-17 19:53:31 +00:00
{ slot . attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot } / { " " }
2023-05-24 09:29:49 +00:00
{ seatsPerTimeSlot } { " " }
{ t ( "seats_available" , {
count : slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot ,
} ) }
2023-02-17 19:53:31 +00:00
< / p >
) }
< / Link >
2023-01-06 12:13:56 +00:00
) }
2023-02-17 19:53:31 +00:00
< / div >
) ;
} ) }
{ ! isLoading && ! slots . length && (
< div className = "-mt-4 flex h-full w-full flex-col content-center items-center justify-center" >
2023-04-05 18:14:46 +00:00
< h1 className = "text-emphasis my-6 text-xl" > { t ( "all_booked_today" ) } < / h1 >
2021-09-22 14:44:38 +00:00
< / div >
2023-02-17 19:53:31 +00:00
) }
2022-06-27 21:01:46 +00:00
2023-02-17 19:53:31 +00:00
{ isLoading && ! slots . length && (
< >
< SkeletonContainer className = "mb-2" >
< SkeletonText className = "h-5 w-full" / >
< / SkeletonContainer >
< SkeletonContainer className = "mb-2" >
< SkeletonText className = "h-5 w-full" / >
< / SkeletonContainer >
< SkeletonContainer className = "mb-2" >
< SkeletonText className = "h-5 w-full" / >
< / SkeletonContainer >
< / >
) }
2021-09-22 14:44:38 +00:00
< / div >
2023-02-17 19:53:31 +00:00
< / div >
) : null }
2021-06-19 22:50:47 +00:00
< / div >
) ;
2021-06-24 22:15:18 +00:00
} ;
2021-06-19 22:50:47 +00:00
2022-10-10 13:24:06 +00:00
AvailableTimes . displayName = "AvailableTimes" ;
2021-06-22 15:19:28 +00:00
export default AvailableTimes ;