242 lines
7.5 KiB
TypeScript
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,
|
|
},
|
|
};
|