import { cubicBezier, useAnimate } from "framer-motion"; import { useReducedMotion } from "framer-motion"; import { useEffect } from "react"; import { BookerLayouts } from "@calcom/prisma/zod-utils"; import type { BookerLayout, BookerState } from "./types"; // Framer motion fade in animation configs. export const fadeInLeft = { variants: { visible: { opacity: 1, x: 0 }, hidden: { opacity: 0, x: 20 }, }, initial: "hidden", exit: "hidden", animate: "visible", transition: { ease: "easeInOut", delay: 0.1 }, }; export const fadeInUp = { variants: { visible: { opacity: 1, y: 0 }, hidden: { opacity: 0, y: 20 }, }, initial: "hidden", exit: "hidden", animate: "visible", transition: { ease: "easeInOut", delay: 0.1 }, }; export const fadeInRight = { variants: { visible: { opacity: 1, x: 0 }, hidden: { opacity: 0, x: -20 }, }, initial: "hidden", exit: "hidden", animate: "visible", transition: { ease: "easeInOut", delay: 0.1 }, }; type ResizeAnimationConfig = { [key in BookerLayout]: { [key in BookerState | "default"]?: React.CSSProperties; }; }; /** * This configuration is used to animate the grid container for the booker. * The object is structured as following: * * The root property of the object: is the name of the layout * (mobile, month_view, week_view, column_view) * * The values of these properties are objects that define the animation for each state of the booker. * The animation have the same properties as you could pass to the animate prop of framer-motion: * @see: https://www.framer.com/motion/animation/ */ export const resizeAnimationConfig: ResizeAnimationConfig = { mobile: { default: { width: "100%", minHeight: "0px", gridTemplateAreas: ` "meta" "header" "main" "timeslots" `, gridTemplateColumns: "100%", gridTemplateRows: "minmax(min-content,max-content) 1fr", }, }, month_view: { default: { width: "calc(var(--booker-meta-width) + var(--booker-main-width))", minHeight: "450px", height: "auto", gridTemplateAreas: ` "meta main main" "meta main main" `, gridTemplateColumns: "var(--booker-meta-width) var(--booker-main-width)", gridTemplateRows: "1fr 0fr", }, selecting_time: { width: "calc(var(--booker-meta-width) + var(--booker-main-width) + var(--booker-timeslots-width))", minHeight: "450px", height: "auto", gridTemplateAreas: ` "meta main timeslots" "meta main timeslots" `, gridTemplateColumns: "var(--booker-meta-width) 1fr var(--booker-timeslots-width)", gridTemplateRows: "1fr 0fr", }, }, week_view: { default: { width: "100vw", minHeight: "100vh", height: "auto", gridTemplateAreas: ` "meta header header" "meta main main" `, gridTemplateColumns: "var(--booker-meta-width) 1fr", gridTemplateRows: "70px auto", }, }, column_view: { default: { width: "100vw", minHeight: "100vh", height: "auto", gridTemplateAreas: ` "meta header header" "meta main main" `, gridTemplateColumns: "var(--booker-meta-width) 1fr", gridTemplateRows: "70px auto", }, }, }; export const getBookerSizeClassNames = ( layout: BookerLayout, bookerState: BookerState, hideEventTypeDetails = false ) => { const getBookerMetaClass = (className: string) => { if (hideEventTypeDetails) { return ""; } return className; }; return [ // Size settings are abstracted on their own lines purely for readability. // General sizes, used always "[--booker-timeslots-width:240px] lg:[--booker-timeslots-width:280px]", // Small calendar defaults layout === BookerLayouts.MONTH_VIEW && getBookerMetaClass("[--booker-meta-width:240px]"), // Meta column get's wider in booking view to fit the full date on a single row in case // of a multi occurance event. Also makes form less wide, which also looks better. layout === BookerLayouts.MONTH_VIEW && bookerState === "booking" && `[--booker-main-width:420px] ${getBookerMetaClass("lg:[--booker-meta-width:340px]")}`, // Smaller meta when not in booking view. layout === BookerLayouts.MONTH_VIEW && bookerState !== "booking" && `[--booker-main-width:480px] ${getBookerMetaClass("lg:[--booker-meta-width:280px]")}`, // Fullscreen view settings. layout !== BookerLayouts.MONTH_VIEW && `[--booker-main-width:480px] [--booker-meta-width:340px] ${getBookerMetaClass( "lg:[--booker-meta-width:424px]" )}`, ]; }; /** * This hook returns a ref that should be set on the booker element. * Based on that ref this hook animates the size of the booker element with framer motion. * It also takes into account the prefers-reduced-motion setting, to not animate when that's set. */ export const useBookerResizeAnimation = (layout: BookerLayout, state: BookerState) => { const prefersReducedMotion = useReducedMotion(); const [animationScope, animate] = useAnimate(); const isEmbed = typeof window !== "undefined" && window?.isEmbed?.(); ``; useEffect(() => { const animationConfig = resizeAnimationConfig[layout][state] || resizeAnimationConfig[layout].default; if (!animationScope.current) return; const animatedProperties = { height: animationConfig?.height || "auto", }; const nonAnimatedProperties = { // Width is animated by the css class instead of via framer motion, // because css is better at animating the calcs, framer motion might // make some mistakes in that. gridTemplateAreas: animationConfig?.gridTemplateAreas, width: animationConfig?.width || "auto", gridTemplateColumns: animationConfig?.gridTemplateColumns, gridTemplateRows: animationConfig?.gridTemplateRows, minHeight: animationConfig?.minHeight, }; // In this cases we don't animate the booker at all. if (prefersReducedMotion || layout === "mobile" || isEmbed) { const styles = { ...nonAnimatedProperties, ...animatedProperties }; Object.keys(styles).forEach((property) => { if (property === "height") { // Change 100vh to 100% in embed, since 100vh in iframe will behave weird, because // the iframe will constantly grow. 100% will simply make sure it grows with the iframe. animationScope.current.style.height = animatedProperties.height === "100vh" && isEmbed ? "100%" : animatedProperties.height; } else { animationScope.current.style[property] = styles[property as keyof typeof styles]; } }); } else { Object.keys(nonAnimatedProperties).forEach((property) => { animationScope.current.style[property] = nonAnimatedProperties[property as keyof typeof nonAnimatedProperties]; }); animate(animationScope.current, animatedProperties, { duration: 0.5, ease: cubicBezier(0.4, 0, 0.2, 1), }); } }, [animate, isEmbed, animationScope, layout, prefersReducedMotion, state]); return animationScope; }; /** * These configures the amount of days that are shown on top of the selected date. */ export const extraDaysConfig = { mobile: { // Desktop tablet feels weird on mobile layout, // but this is simply here to make the types a lot easier.. desktop: 0, tablet: 0, }, [BookerLayouts.MONTH_VIEW]: { desktop: 0, tablet: 0, }, [BookerLayouts.WEEK_VIEW]: { desktop: 7, tablet: 4, }, [BookerLayouts.COLUMN_VIEW]: { desktop: 6, tablet: 2, }, };