cal.pub0.org/packages/features/bookings/Booker/config.ts

242 lines
7.5 KiB
TypeScript

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,
},
};