feat: CAL-1674 Added new header layout for new booker. (#9227)
* CAL-1674: Added new header layout for new booker. * Removed unneccesary bg style for toggle group, since it's already the default bg color. * Fixed types * Gave minwidth to booker header date title, to prevent arrows for switching weeks from jumping around. * Push small calendar down * Fixed overflow in small calendar view * Added booker modal padding underneath badges * Fixed grid area name causing broken animation. Also use setstyle directly instead of using animate for non animated properties. Performs better and removes some errors by framer motion for non animatable units like 'auto'. --------- Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
co-authored by
sean-brydon
parent
8ccf3b589e
commit
2362754356
@@ -1,26 +1,24 @@
|
||||
import { LazyMotion, domAnimation, m, AnimatePresence } from "framer-motion";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import StickyBox from "react-sticky-box";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import { ToggleGroup } from "@calcom/ui";
|
||||
import { Calendar, Columns, Grid } from "@calcom/ui/components/icon";
|
||||
|
||||
import { AvailableTimeSlots } from "./components/AvailableTimeSlots";
|
||||
import { BookEventForm } from "./components/BookEventForm";
|
||||
import { BookFormAsModal } from "./components/BookEventForm/BookFormAsModal";
|
||||
import { EventMeta } from "./components/EventMeta";
|
||||
import { Header } from "./components/Header";
|
||||
import { LargeCalendar } from "./components/LargeCalendar";
|
||||
import { LargeViewHeader } from "./components/LargeViewHeader";
|
||||
import { BookerSection } from "./components/Section";
|
||||
import { Away, NotFound } from "./components/Unavailable";
|
||||
import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
|
||||
import { useBookerStore, useInitializeBookerStore } from "./store";
|
||||
import type { BookerLayout, BookerProps } from "./types";
|
||||
import type { BookerProps } from "./types";
|
||||
import { useEvent } from "./utils/event";
|
||||
import { useBrandColors } from "./utils/use-brand-colors";
|
||||
|
||||
@@ -52,7 +50,6 @@ const BookerComponent = ({
|
||||
shallow
|
||||
);
|
||||
const extraDays = layout === "large_timeslots" ? (isTablet ? 2 : 4) : 0;
|
||||
const onLayoutToggle = useCallback((newLayout: BookerLayout) => setLayout(newLayout), [setLayout]);
|
||||
|
||||
const animationScope = useBookerResizeAnimation(layout, bookerState);
|
||||
|
||||
@@ -98,46 +95,22 @@ const BookerComponent = ({
|
||||
|
||||
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-default dark:[&>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
|
||||
ref={animationScope}
|
||||
className={classNames(
|
||||
// Sets booker size css variables for the size of all the columns.
|
||||
...getBookerSizeClassNames(layout, bookerState),
|
||||
"bg-default dark:bg-muted grid max-w-full auto-rows-fr items-start overflow-clip dark:[color-scheme:dark] sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row",
|
||||
"bg-default dark:bg-muted grid max-w-full items-start overflow-clip dark:[color-scheme:dark] sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row",
|
||||
layout === "small_calendar" && "border-subtle rounded-md border"
|
||||
)}>
|
||||
<AnimatePresence>
|
||||
<StickyOnDesktop key="meta" className="relative z-10 flex min-h-full">
|
||||
<BookerSection area="header">
|
||||
<Header extraDays={extraDays} isMobile={isMobile} />
|
||||
</BookerSection>
|
||||
<StickyOnDesktop
|
||||
key="meta"
|
||||
className={classNames("relative z-10 flex", layout !== "small_calendar" && "sm:min-h-screen")}>
|
||||
<BookerSection
|
||||
area="meta"
|
||||
className="max-w-screen flex w-full flex-col md:w-[var(--booker-meta-width)]">
|
||||
@@ -196,7 +169,6 @@ const BookerComponent = ({
|
||||
)}
|
||||
ref={timeslotsRef}
|
||||
{...fadeInLeft}>
|
||||
{layout === "large_timeslots" && <LargeViewHeader extraDays={extraDays} />}
|
||||
<AvailableTimeSlots
|
||||
extraDays={extraDays}
|
||||
limitHeight={layout === "small_calendar"}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function BookFormAsModal({ visible, onCancel }: { visible: boolean; onCan
|
||||
enableOverflow
|
||||
className="[&_.modalsticky]:border-t-subtle [&_.modalsticky]:bg-default dark:[&_.modalsticky]:bg-muted max-h-[80vh] pt-6 pb-0 [&_.modalsticky]:sticky [&_.modalsticky]:bottom-0 [&_.modalsticky]:left-0 [&_.modalsticky]:right-0 [&_.modalsticky]:-mx-8 [&_.modalsticky]:border-t [&_.modalsticky]:px-6 [&_.modalsticky]:py-4">
|
||||
<h1 className="font-cal text-emphasis text-xl leading-5">{t("confirm_your_details")} </h1>
|
||||
<div className="mt-4 flex space-x-2 rounded-md leading-none">
|
||||
<div className="my-4 flex space-x-2 rounded-md leading-none">
|
||||
<Badge variant="grayWithoutHover" startIcon={Calendar} size="lg">
|
||||
<span>{parsedSelectedTimeslot.format("LLL")}</span>
|
||||
</Badge>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { useCallback } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, ButtonGroup, ToggleGroup } from "@calcom/ui";
|
||||
import { Calendar, Columns, Grid } from "@calcom/ui/components/icon";
|
||||
|
||||
import { TimeFormatToggle } from "../../components/TimeFormatToggle";
|
||||
import { useBookerStore } from "../store";
|
||||
import type { BookerLayout } from "../types";
|
||||
|
||||
export function Header({ extraDays, isMobile }: { extraDays: number; isMobile: boolean }) {
|
||||
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
|
||||
const selectedDateString = useBookerStore((state) => state.selectedDate);
|
||||
const addToSelectedDate = useBookerStore((state) => state.addToSelectedDate);
|
||||
const isSmallCalendar = layout === "small_calendar";
|
||||
const selectedDate = dayjs(selectedDateString);
|
||||
|
||||
const onLayoutToggle = useCallback(
|
||||
(newLayout: string) => setLayout(newLayout as BookerLayout),
|
||||
[setLayout]
|
||||
);
|
||||
|
||||
if (isMobile) return null;
|
||||
|
||||
// In month view we only show the layout toggle.
|
||||
if (isSmallCalendar) {
|
||||
return (
|
||||
<div className="fixed top-3 right-3 z-10">
|
||||
<LayoutToggle onLayoutToggle={onLayoutToggle} layout={layout} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-subtle relative z-10 flex border-l border-b p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="min-w-[150px] text-base font-semibold leading-4">
|
||||
{selectedDate.format("MMM D")}-{selectedDate.add(extraDays, "days").format("D")},{" "}
|
||||
<span className="text-subtle">{selectedDate.format("YYYY")}</span>
|
||||
</h3>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="icon"
|
||||
color="minimal"
|
||||
StartIcon={ChevronLeft}
|
||||
aria-label="Previous Day"
|
||||
onClick={() => addToSelectedDate(-extraDays - 1)}
|
||||
/>
|
||||
<Button
|
||||
variant="icon"
|
||||
color="minimal"
|
||||
StartIcon={ChevronRight}
|
||||
aria-label="Next Day"
|
||||
onClick={() => addToSelectedDate(extraDays + 1)}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className="ml-auto flex gap-3">
|
||||
<TimeFormatToggle />
|
||||
<div className="fixed top-4 right-4">
|
||||
<LayoutToggle onLayoutToggle={onLayoutToggle} layout={layout} />
|
||||
</div>
|
||||
{/*
|
||||
This second layout toggle is hidden, but needed to reserve the correct spot in the DIV
|
||||
for the fixed toggle above to fit into. If we wouldn't make it fixed in this view, the transition
|
||||
would be really weird, because the element is positioned fixed in the month view, and then
|
||||
when switching layouts wouldn't anymmore, causing it to animate from the center to the top right,
|
||||
while it actuall already was on place. That's why we have this element twice.
|
||||
*/}
|
||||
<div className="pointer-events-none opacity-0" aria-hidden>
|
||||
<LayoutToggle onLayoutToggle={onLayoutToggle} layout={layout} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const LayoutToggle = ({
|
||||
onLayoutToggle,
|
||||
layout,
|
||||
}: {
|
||||
onLayoutToggle: (layout: string) => void;
|
||||
layout: string;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<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"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { Button, ButtonGroup } from "@calcom/ui";
|
||||
|
||||
import { useBookerStore } from "../store";
|
||||
|
||||
export function LargeViewHeader({ extraDays }: { extraDays: number }) {
|
||||
const layout = useBookerStore((state) => state.layout);
|
||||
const selectedDateString = useBookerStore((state) => state.selectedDate);
|
||||
const addToSelectedDate = useBookerStore((state) => state.addToSelectedDate);
|
||||
const isLargeTimeslots = layout === "large_timeslots";
|
||||
const selectedDate = dayjs(selectedDateString);
|
||||
|
||||
if (!isLargeTimeslots) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-8 mb-4 flex py-2.5">
|
||||
<h3 className="text-base font-semibold leading-4">
|
||||
{selectedDate.format("MMM D")}-{selectedDate.add(extraDays, "days").format("D")},{" "}
|
||||
<span className="text-subtle">{selectedDate.format("YYYY")}</span>
|
||||
</h3>
|
||||
<div className="ml-auto">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="icon"
|
||||
color="minimal"
|
||||
StartIcon={ChevronLeft}
|
||||
aria-label="Previous Day"
|
||||
onClick={() => addToSelectedDate(-extraDays - 1)}
|
||||
/>
|
||||
<Button
|
||||
variant="icon"
|
||||
color="minimal"
|
||||
StartIcon={ChevronRight}
|
||||
aria-label="Next Day"
|
||||
onClick={() => addToSelectedDate(extraDays + 1)}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,7 @@ const gridAreaClassNameMap: { [key in BookerAreas]: string } = {
|
||||
main: "[grid-area:main]",
|
||||
meta: "[grid-area:meta]",
|
||||
timeslots: "[grid-area:timeslots]",
|
||||
header: "[grid-area:header]",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -50,10 +50,12 @@ export const resizeAnimationConfig: ResizeAnimationConfig = {
|
||||
minHeight: "0px",
|
||||
gridTemplateAreas: `
|
||||
"meta"
|
||||
"header"
|
||||
"main"
|
||||
"timeslots"
|
||||
`,
|
||||
gridTemplateColumns: "100%",
|
||||
gridTemplateRows: "auto auto auto auto",
|
||||
},
|
||||
},
|
||||
small_calendar: {
|
||||
@@ -61,31 +63,49 @@ export const resizeAnimationConfig: ResizeAnimationConfig = {
|
||||
width: "calc(var(--booker-meta-width) + var(--booker-main-width))",
|
||||
minHeight: "450px",
|
||||
height: "auto",
|
||||
gridTemplateAreas: `"meta main"`,
|
||||
gridTemplateAreas: `
|
||||
"meta main main"
|
||||
"meta main main"
|
||||
`,
|
||||
gridTemplateColumns: "var(--booker-meta-width) var(--booker-main-width)",
|
||||
gridTemplateRows: "auto",
|
||||
},
|
||||
selecting_time: {
|
||||
width: "calc(var(--booker-meta-width) + var(--booker-main-width) + var(--booker-timeslots-width))",
|
||||
minHeight: "450px",
|
||||
height: "auto",
|
||||
gridTemplateAreas: `"meta main timeslots"`,
|
||||
gridTemplateAreas: `
|
||||
"meta main timeslots"
|
||||
"meta main timeslots"
|
||||
`,
|
||||
gridTemplateColumns: "var(--booker-meta-width) 1fr var(--booker-timeslots-width)",
|
||||
gridTemplateRows: "auto",
|
||||
},
|
||||
},
|
||||
large_calendar: {
|
||||
default: {
|
||||
width: "100vw",
|
||||
minHeight: "450px",
|
||||
height: "100vh",
|
||||
gridTemplateAreas: `"meta main"`,
|
||||
gridTemplateAreas: `
|
||||
"meta header header"
|
||||
"meta main main"
|
||||
`,
|
||||
gridTemplateColumns: "var(--booker-meta-width) 1fr",
|
||||
gridTemplateRows: "70px auto",
|
||||
},
|
||||
},
|
||||
large_timeslots: {
|
||||
default: {
|
||||
width: "100vw",
|
||||
minHeight: "450px",
|
||||
height: "100vh",
|
||||
gridTemplateAreas: `"meta main"`,
|
||||
gridTemplateAreas: `
|
||||
"meta header header"
|
||||
"meta main main"
|
||||
`,
|
||||
gridTemplateColumns: "var(--booker-meta-width) 1fr",
|
||||
gridTemplateRows: "70px auto",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -134,28 +154,24 @@ export const useBookerResizeAnimation = (layout: BookerLayout, state: BookerStat
|
||||
// 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.
|
||||
width: animationConfig?.width || "auto",
|
||||
gridTemplateAreas: animationConfig?.gridTemplateAreas,
|
||||
width: animationConfig?.width || "auto",
|
||||
gridTemplateColumns: animationConfig?.gridTemplateColumns,
|
||||
gridTemplateRows: animationConfig?.gridTemplateRows,
|
||||
minHeight: animationConfig?.minHeight,
|
||||
};
|
||||
|
||||
// We don't animate if users has set prefers-reduced-motion,
|
||||
// or when the layout is mobile.
|
||||
if (prefersReducedMotion || layout === "mobile") {
|
||||
animate(
|
||||
animationScope.current,
|
||||
{
|
||||
...animatedProperties,
|
||||
...nonAnimatedProperties,
|
||||
},
|
||||
{
|
||||
duration: 0,
|
||||
}
|
||||
);
|
||||
const styles = { ...nonAnimatedProperties, ...animatedProperties };
|
||||
Object.keys(styles).forEach((property) => {
|
||||
animationScope.current.style[property] = styles[property as keyof typeof styles];
|
||||
});
|
||||
} else {
|
||||
animate(animationScope.current, nonAnimatedProperties, {
|
||||
duration: 0,
|
||||
Object.keys(nonAnimatedProperties).forEach((property) => {
|
||||
animationScope.current.style[property] =
|
||||
nonAnimatedProperties[property as keyof typeof nonAnimatedProperties];
|
||||
});
|
||||
animate(animationScope.current, animatedProperties, {
|
||||
duration: 0.5,
|
||||
|
||||
@@ -43,4 +43,4 @@ export interface BookerProps {
|
||||
|
||||
export type BookerState = "loading" | "selecting_date" | "selecting_time" | "booking";
|
||||
export type BookerLayout = "small_calendar" | "large_timeslots" | "large_calendar" | "mobile";
|
||||
export type BookerAreas = "calendar" | "timeslots" | "main" | "meta";
|
||||
export type BookerAreas = "calendar" | "timeslots" | "main" | "meta" | "header";
|
||||
|
||||
Reference in New Issue
Block a user