cal.pub0.org/packages/features/bookings/Booker/Booker.tsx

236 lines
9.3 KiB
TypeScript

import { LazyMotion, domAnimation, m, AnimatePresence } from "framer-motion";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useRef } from "react";
import StickyBox from "react-sticky-box";
import { shallow } from "zustand/shallow";
import classNames from "@calcom/lib/classNames";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { ToggleGroup, useCalcomTheme } from "@calcom/ui";
import { Calendar, Columns, Grid } from "@calcom/ui/components/icon";
import { AvailableTimeSlots } from "./components/AvailableTimeSlots";
import { Away } from "./components/Away";
import { BookEventForm } from "./components/BookEventForm";
import { BookFormAsModal } from "./components/BookEventForm/BookFormAsModal";
import { DatePicker } from "./components/DatePicker";
import { EventMeta } from "./components/EventMeta";
import { LargeCalendar } from "./components/LargeCalendar";
import { LargeViewHeader } from "./components/LargeViewHeader";
import { BookerSection } from "./components/Section";
import { fadeInLeft, resizeAnimationConfig } from "./config";
import { useBookerStore, useInitializeBookerStore } from "./store";
import type { BookerLayout, BookerProps } from "./types";
import { useEvent } from "./utils/event";
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor?: string; darkBrandColor?: string }) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
};
const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: BookerProps) => {
const { t } = useLocale();
const isMobile = useMediaQuery("(max-width: 768px)");
const isTablet = useMediaQuery("(max-width: 1024px)");
const timeslotsRef = useRef<HTMLDivElement>(null);
const StickyOnDesktop = isMobile ? "div" : StickyBox;
const rescheduleUid =
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduleUid") : null;
const event = useEvent();
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
const selectedDate = useBookerStore((state) => state.selectedDate);
const [selectedTimeslot, setSelectedTimeslot] = useBookerStore(
(state) => [state.selectedTimeslot, state.setSelectedTimeslot],
shallow
);
const extraDays = layout === "large_timeslots" ? (isTablet ? 2 : 4) : 0;
const onLayoutToggle = useCallback((newLayout: BookerLayout) => setLayout(newLayout), [setLayout]);
useBrandColors({
brandColor: event.data?.profile.brandColor,
darkBrandColor: event.data?.profile.darkBrandColor,
});
useInitializeBookerStore({
username,
eventSlug,
month,
eventId: event?.data?.id,
rescheduleUid,
rescheduleBooking,
});
useEffect(() => {
if (isMobile && layout !== "mobile") {
setLayout("mobile");
} else if (!isMobile && layout === "mobile") {
setLayout("small_calendar");
}
}, [isMobile, setLayout, layout]);
useEffect(() => {
if (event.isLoading) return setBookerState("loading");
if (!selectedDate) return setBookerState("selecting_date");
if (!selectedTimeslot) return setBookerState("selecting_time");
return setBookerState("booking");
}, [event, selectedDate, selectedTimeslot, setBookerState]);
useEffect(() => {
if (layout === "mobile") {
timeslotsRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [layout]);
return (
<>
{/*
If we would render this on mobile, it would unset the mobile variant,
since that's not a valid option, so it would set the layout to null.
*/}
{!isMobile && (
<div className="[&>div]:bg-muted fixed top-2 right-3 z-10">
<ToggleGroup
onValueChange={onLayoutToggle}
defaultValue={layout}
options={[
{
value: "small_calendar",
label: <Calendar width="16" height="16" />,
tooltip: t("switch_monthly"),
},
{
value: "large_calendar",
label: <Grid width="16" height="16" />,
tooltip: t("switch_weekly"),
},
{
value: "large_timeslots",
label: <Columns width="16" height="16" />,
tooltip: t("switch_multiday"),
},
]}
/>
</div>
)}
<div className="flex h-full w-full flex-col items-center">
<div
style={resizeAnimationConfig[layout]?.[bookerState] || resizeAnimationConfig[layout].default}
className={classNames(
// Size settings are abstracted on their own lines purely for readbility.
// General sizes:
"[--booker-main-width:480px] [--booker-timeslots-width:240px] lg:[--booker-timeslots-width:280px]",
// Sizes only applicable for small_calendar
layout === "small_calendar" &&
"w-[calc(var(--booker-meta-width)+var(--booker-main-width))] [--booker-meta-width:240px] lg:[--booker-meta-width:280px]",
// Sizes for fullscreen layouts:
layout !== "small_calendar" && "[--booker-meta-width:340px] lg:[--booker-meta-width:424px]",
// Other styles
"bg-muted grid max-w-full items-start overflow-clip dark:[color-scheme:dark] md:flex-row",
layout === "small_calendar" && "border-subtle mt-20 min-h-[450px] rounded-md border",
layout !== "small_calendar" && "h-auto min-h-screen w-screen",
"transition-[width,max-width] duration-300"
)}>
<AnimatePresence>
<StickyOnDesktop key="meta" className="relative z-10">
<BookerSection area="meta" className="max-w-screen w-full md:w-[var(--booker-meta-width)]">
<EventMeta />
{layout !== "small_calendar" && !(layout === "mobile" && bookerState === "booking") && (
<div className=" mt-auto p-5">
<DatePicker />
</div>
)}
</BookerSection>
</StickyOnDesktop>
<BookerSection
key="book-event-form"
area="main"
className="border-subtle sticky top-0 ml-[-1px] h-full p-5 md:w-[var(--booker-main-width)] md:border-l"
{...fadeInLeft}
visible={bookerState === "booking" && layout !== "large_timeslots"}>
<BookEventForm onCancel={() => setSelectedTimeslot(null)} />
</BookerSection>
<BookerSection
key="datepicker"
area="main"
visible={bookerState !== "booking" && layout === "small_calendar"}
{...fadeInLeft}
initial="visible"
className="md:border-subtle ml-[-1px] h-full flex-shrink p-5 md:border-l lg:w-[var(--booker-main-width)]">
<DatePicker />
</BookerSection>
<BookerSection
key="large-calendar"
area="main"
visible={
layout === "large_calendar" &&
(bookerState === "selecting_date" || bookerState === "selecting_time")
}
className="border-muted sticky top-0 ml-[-1px] h-full md:border-l"
{...fadeInLeft}>
<LargeCalendar />
</BookerSection>
<BookerSection
key="timeslots"
area={{ default: "main", small_calendar: "timeslots" }}
visible={
(layout !== "large_calendar" && bookerState === "selecting_time") ||
layout === "large_timeslots"
}
className={classNames(
"border-subtle flex h-full w-full flex-col p-5 pb-0 md:border-l",
layout === "small_calendar" &&
"scroll-bar h-full overflow-auto md:w-[var(--booker-timeslots-width)]",
layout !== "small_calendar" && "sticky top-0"
)}
ref={timeslotsRef}
{...fadeInLeft}>
{layout === "large_timeslots" && <LargeViewHeader extraDays={extraDays} />}
<AvailableTimeSlots
extraDays={extraDays}
limitHeight={layout === "small_calendar"}
seatsPerTimeslot={event.data?.seatsPerTimeSlot}
/>
</BookerSection>
</AnimatePresence>
</div>
<m.span
key="logo"
className={classNames(
"mt-auto mb-6 pt-6 [&_img]:h-[15px]",
layout === "small_calendar" ? "block" : "hidden"
)}>
<PoweredBy logoOnly />
</m.span>
</div>
<BookFormAsModal
visible={layout === "large_timeslots" && bookerState === "booking"}
onCancel={() => setSelectedTimeslot(null)}
/>
</>
);
};
export const Booker = (props: BookerProps) => {
if (props.isAway) return <Away />;
return (
<LazyMotion features={domAnimation}>
<BookerComponent {...props} />
</LazyMotion>
);
};