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:
Jeroen Reumkens
2023-06-06 13:55:27 +00:00
committed by GitHub
co-authored by sean-brydon
parent 8ccf3b589e
commit 2362754356
7 changed files with 159 additions and 100 deletions
+10 -38
View File
@@ -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]",
};
/**
+33 -17
View File
@@ -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,
+1 -1
View File
@@ -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";