Files
calendar/packages/features/bookings/components/event-meta/Duration.tsx
T

138 lines
5.0 KiB
TypeScript

import type { TFunction } from "next-i18next";
import { useEffect, useRef } from "react";
import { useIsPlatform } from "@calcom/atoms/monorepo";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useShouldShowArrows, Icon } from "@calcom/ui";
/** Render X mins as X hours or X hours Y mins instead of in minutes once >= 60 minutes */
export const getDurationFormatted = (mins: number | undefined, t: TFunction) => {
if (!mins) return null;
const hours = Math.floor(mins / 60);
mins %= 60;
// format minutes string
let minStr = "";
if (mins > 0) {
minStr =
mins === 1
? t("minute_one_short", { count: 1 })
: t("multiple_duration_timeUnit_short", { count: mins, unit: "minute" });
}
// format hours string
let hourStr = "";
if (hours > 0) {
hourStr =
hours === 1
? t("hour_one_short", { count: 1 })
: t("multiple_duration_timeUnit_short", { count: hours, unit: "hour" });
}
if (hourStr && minStr) return `${hourStr} ${minStr}`;
return hourStr || minStr;
};
export const EventDuration = ({
event,
}: {
event: Pick<BookerEvent, "length" | "metadata" | "isDynamic">;
}) => {
const { t } = useLocale();
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
const isPlatform = useIsPlatform();
const [selectedDuration, setSelectedDuration, state] = useBookerStore((state) => [
state.selectedDuration,
state.setSelectedDuration,
state.state,
]);
const { ref, calculateScroll, leftVisible, rightVisible } = useShouldShowArrows();
const handleLeft = () => {
if (ref.current) {
ref.current.scrollLeft -= 100;
}
};
const handleRight = () => {
if (ref.current) {
ref.current.scrollLeft += 100;
}
};
const isDynamicEvent = "isDynamic" in event && event.isDynamic;
const isEmbed = useIsEmbed();
// Sets initial value of selected duration to the default duration.
useEffect(() => {
// Only store event duration in url if event has multiple durations.
if (!selectedDuration && (event.metadata?.multipleDuration || isDynamicEvent))
setSelectedDuration(event.length);
}, [selectedDuration, setSelectedDuration, event.metadata?.multipleDuration, event.length, isDynamicEvent]);
useEffect(() => {
const timeout = setTimeout(() => {
if (isEmbed) return;
if (selectedDuration && itemRefs.current[selectedDuration]) {
// eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- Called on !isEmbed case
itemRefs.current[selectedDuration]?.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center",
});
}
}, 100);
return () => clearTimeout(timeout);
}, [selectedDuration, isEmbed]);
if (!event?.metadata?.multipleDuration && !isDynamicEvent)
return <>{getDurationFormatted(event.length, t)}</>;
const durations = event?.metadata?.multipleDuration || [15, 30, 60, 90];
return selectedDuration ? (
<div className="border-default relative mr-5 flex flex-row items-center justify-between rounded-md border">
{leftVisible && (
<button onClick={handleLeft} className="absolute bottom-0 left-0 flex">
<div className="bg-default flex h-9 w-5 items-center justify-end rounded-md">
<Icon name="chevron-left" className="text-subtle h-4 w-4" />
</div>
<div className="to-default flex h-9 w-5 bg-gradient-to-l from-transparent" />
</button>
)}
<ul
className="bg-default no-scrollbar flex max-w-full items-center gap-0.5 overflow-x-auto rounded-md p-1"
onScroll={(e) => calculateScroll(e)}
ref={ref}>
{durations
.filter((dur) => state !== "booking" || dur === selectedDuration)
.map((duration, index) => (
<li
data-testId={`multiple-choice-${duration}mins`}
data-active={selectedDuration === duration ? "true" : "false"}
key={index}
onClick={() => setSelectedDuration(duration)}
ref={(el) => (itemRefs.current[duration] = el)}
className={classNames(
selectedDuration === duration ? "bg-emphasis" : "hover:text-emphasis",
"text-default cursor-pointer rounded-[4px] px-3 py-1.5 text-sm leading-tight transition"
)}>
<div className="w-max">{getDurationFormatted(duration, t)}</div>
</li>
))}
</ul>
{rightVisible && (
<button onClick={handleRight} className="absolute bottom-0 right-0 flex">
<div className="to-default flex h-9 w-5 bg-gradient-to-r from-transparent" />
<div className="bg-default flex h-9 w-5 items-center justify-end rounded-md">
<Icon name="chevron-right" className="text-subtle h-4 w-4" />
</div>
</button>
)}
</div>
) : null;
};