feat: weekly view for new booker. (#9389)

* WIP on weekly view for new booker.

* Set responsive grid height for booker weekly view.

* Fixed types

* Ensure you can't click on disabled dates in weekly calendar

* CAL-1916: Improvements for blocked out parts background blocks, made the lines connect by using a shared background.

* Margin, padding and color tweaks for new booker CAL-1911

* Made week view have more days visible by default

* Fix to prevent layout switch from breaking when clicking same layout twice.
This commit is contained in:
Jeroen Reumkens authored and GitHub committed 2023-06-09 09:38:18 +00:00
1 parent a33dbd8922
commit ca47a589ba
17 files changed
+236 -98

No files matched your search

+12 -15
View File
@@ -16,7 +16,7 @@ import { Header } from "./components/Header";
import { LargeCalendar } from "./components/LargeCalendar";
import { BookerSection } from "./components/Section";
import { Away, NotFound } from "./components/Unavailable";
import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
import { extraDaysConfig, fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
import { useBookerStore, useInitializeBookerStore } from "./store";
import type { BookerProps } from "./types";
import { useEvent } from "./utils/event";
@@ -48,9 +48,9 @@ const BookerComponent = ({
(state) => [state.selectedTimeslot, state.setSelectedTimeslot],
shallow
);
const extraDays = layout === BookerLayouts.COLUMN_VIEW ? (isTablet ? 2 : 4) : 0;
const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings;
const extraDays = isTablet ? extraDaysConfig[layout].tablet : extraDaysConfig[layout].desktop;
const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings;
const animationScope = useBookerResizeAnimation(layout, bookerState);
useBrandColors({
@@ -125,7 +125,7 @@ const BookerComponent = ({
<EventMeta />
{layout !== BookerLayouts.MONTH_VIEW &&
!(layout === "mobile" && bookerState === "booking") && (
<div className=" mt-auto p-5">
<div className=" mt-auto px-5 py-3">
<DatePicker />
</div>
)}
@@ -135,9 +135,9 @@ const BookerComponent = ({
<BookerSection
key="book-event-form"
area="main"
className="border-subtle sticky top-0 ml-[-1px] h-full p-5 md:w-[var(--booker-main-width)] md:border-l"
className="border-subtle sticky top-0 ml-[-1px] h-full px-5 py-3 md:w-[var(--booker-main-width)] md:border-l"
{...fadeInLeft}
visible={bookerState === "booking" && layout !== BookerLayouts.COLUMN_VIEW}>
visible={bookerState === "booking" && layout === BookerLayouts.MONTH_VIEW}>
<BookEventForm onCancel={() => setSelectedTimeslot(null)} />
</BookerSection>
@@ -147,20 +147,17 @@ const BookerComponent = ({
visible={bookerState !== "booking" && layout === BookerLayouts.MONTH_VIEW}
{...fadeInLeft}
initial="visible"
className="md:border-subtle ml-[-1px] h-full flex-shrink p-5 md:border-l lg:w-[var(--booker-main-width)]">
className="md:border-subtle ml-[-1px] h-full flex-shrink px-5 py-3 md:border-l lg:w-[var(--booker-main-width)]">
<DatePicker />
</BookerSection>
<BookerSection
key="large-calendar"
area="main"
visible={
layout === BookerLayouts.WEEK_VIEW &&
(bookerState === "selecting_date" || bookerState === "selecting_time")
}
className="border-muted sticky top-0 ml-[-1px] h-full md:border-l"
visible={layout === BookerLayouts.WEEK_VIEW}
className="border-subtle sticky top-0 ml-[-1px] h-full md:border-l"
{...fadeInLeft}>
<LargeCalendar />
<LargeCalendar extraDays={extraDays} />
</BookerSection>
<BookerSection
@@ -171,7 +168,7 @@ const BookerComponent = ({
layout === BookerLayouts.COLUMN_VIEW
}
className={classNames(
"border-subtle flex h-full w-full flex-col p-5 pb-0 md:border-l",
"border-subtle flex h-full w-full flex-col px-5 py-3 pb-0 md:border-l",
layout === BookerLayouts.MONTH_VIEW &&
"scroll-bar h-full overflow-auto md:w-[var(--booker-timeslots-width)]",
layout !== BookerLayouts.MONTH_VIEW && "sticky top-0"
@@ -198,7 +195,7 @@ const BookerComponent = ({
</div>
<BookFormAsModal
visible={layout === BookerLayouts.COLUMN_VIEW && bookerState === "booking"}
visible={layout !== BookerLayouts.MONTH_VIEW && bookerState === "booking"}
onCancel={() => setSelectedTimeslot(null)}
/>
</>
@@ -28,8 +28,11 @@ export function Header({
const selectedDate = dayjs(selectedDateString);
const onLayoutToggle = useCallback(
(newLayout: string) => setLayout(newLayout as BookerLayout),
[setLayout]
(newLayout: string) => {
if (layout === newLayout || !newLayout) return;
setLayout(newLayout as BookerLayout);
},
[setLayout, layout]
);
if (isMobile || !enabledLayouts) return null;
@@ -51,7 +54,7 @@ export function Header({
}
return (
<div className="border-subtle relative z-10 flex border-l border-b p-4">
<div className="border-subtle relative z-10 flex border-l border-b px-5 py-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")},{" "}
@@ -74,24 +77,22 @@ export function Header({
/>
</ButtonGroup>
</div>
{enabledLayouts.length > 1 && (
<div className="ml-auto flex gap-3">
<TimeFormatToggle />
<div className="fixed top-4 right-4">
<LayoutToggleWithData />
</div>
{/*
<div className="ml-auto flex gap-2">
<TimeFormatToggle />
<div className="fixed top-4 right-4">
<LayoutToggleWithData />
</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>
<LayoutToggleWithData />
</div>
<div className="pointer-events-none opacity-0" aria-hidden>
<LayoutToggleWithData />
</div>
)}
</div>
</div>
);
}
@@ -1,30 +1,48 @@
import { shallow } from "zustand/shallow";
import { useMemo } from "react";
import dayjs from "@calcom/dayjs";
import { Calendar } from "@calcom/features/calendars/weeklyview";
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
import { useBookerStore } from "../store";
import { useScheduleForEvent } from "../utils/event";
export const LargeCalendar = () => {
const [setSelectedDate, setSelectedTimeslot] = useBookerStore(
(state) => [state.setSelectedDate, state.setSelectedTimeslot],
shallow
);
export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
const selectedDate = useBookerStore((state) => state.selectedDate);
const date = selectedDate || dayjs().format("YYYY-MM-DD");
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
const schedule = useScheduleForEvent({
prefetchNextMonth: !!extraDays && dayjs(date).month() !== dayjs(date).add(extraDays, "day").month(),
});
const availableSlots = useMemo(() => {
const availableTimeslots: CalendarAvailableTimeslots = {};
if (!schedule.data) return availableTimeslots;
if (!schedule.data.slots) return availableTimeslots;
for (const day in schedule.data.slots) {
availableTimeslots[day] = schedule.data.slots[day].map((slot) => ({
start: dayjs(slot.time).toDate(),
end: dayjs(slot.time).add(30, "minutes").toDate(),
}));
}
return availableTimeslots;
}, [schedule]);
return (
<div className="bg-default dark:bg-muted flex h-full w-full flex-col items-center justify-center">
Something big is coming...
<br />
<button
className="max-w-[300px] underline"
type="button"
onClick={(ev) => {
ev.preventDefault();
setSelectedDate(dayjs().format("YYYY-MM-DD"));
setSelectedTimeslot(dayjs().format());
}}>
Click this button to set date + time in one go just like the big thing that is coming here would do.
:)
</button>
<div className="h-full">
<Calendar
availableTimeslots={availableSlots}
startHour={8}
endHour={18}
events={[]}
startDate={selectedDate ? new Date(selectedDate) : new Date()}
endDate={dayjs(selectedDate).add(extraDays, "day").toDate()}
onEmptyCellClick={(date) => setSelectedTimeslot(date.toString())}
gridCellsPerHour={2}
hoverEventDuration={30}
hideHeader
/>
</div>
);
};
@@ -184,3 +184,27 @@ export const useBookerResizeAnimation = (layout: BookerLayout, state: BookerStat
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: 4,
tablet: 2,
},
};
@@ -36,19 +36,25 @@ export const AvailableTimes = ({
const hasTimeSlots = !!seatsPerTimeslot;
const [layout] = useBookerStore((state) => [state.layout], shallow);
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
const isToday = dayjs().isSame(date, "day");
return (
<div className={classNames("text-default", className)}>
<header className="bg-default before:bg-default dark:bg-muted dark:before:bg-muted mb-5 flex w-full flex-row items-center font-medium">
<span className={classNames(isColumnView && "w-full text-center")}>
<span className="text-emphasis font-semibold">
<header className="bg-default before:bg-default dark:bg-muted dark:before:bg-muted mb-3 flex w-full flex-row items-center font-medium">
<span
className={classNames(
isColumnView && "w-full text-center",
isColumnView ? "text-subtle text-xs uppercase" : "text-emphasis font-semibold"
)}>
<span className={classNames(isToday && "!text-default")}>
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span>
<span
className={classNames(
isColumnView && isToday ? "bg-brand-default text-brand ml-2" : "text-default",
"inline-flex items-center justify-center rounded-3xl px-1 pt-0.5 text-sm font-medium"
isColumnView && isToday && "bg-brand-default text-brand ml-2",
"inline-flex items-center justify-center rounded-3xl px-1 pt-0.5 font-medium",
isMonthView ? "text-default text-sm" : "text-xs"
)}>
{date.format("DD")}
</span>
@@ -1,11 +1,10 @@
import React, { useEffect, useMemo, useRef } from "react";
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useCalendarStore } from "../state/store";
import "../styles/styles.css";
import type { CalendarComponentProps } from "../types/state";
import { getDaysBetweenDates, getHoursToDisplay } from "../utils";
import { calculateHourSizeInPx, getDaysBetweenDates, getHoursToDisplay } from "../utils";
import { DateValues } from "./DateValues";
import { BlockedList } from "./blocking/BlockedList";
import { EmptyCell } from "./event/Empty";
import { EventList } from "./event/EventList";
import { SchedulerColumns } from "./grid";
@@ -17,6 +16,7 @@ export function Calendar(props: CalendarComponentProps) {
const container = useRef<HTMLDivElement | null>(null);
const containerNav = useRef<HTMLDivElement | null>(null);
const containerOffset = useRef<HTMLDivElement | null>(null);
const schedulerGrid = useRef<HTMLOListElement | null>(null);
const initalState = useCalendarStore((state) => state.initState);
const startDate = useCalendarStore((state) => state.startDate);
@@ -24,12 +24,30 @@ export function Calendar(props: CalendarComponentProps) {
const startHour = useCalendarStore((state) => state.startHour || 0);
const endHour = useCalendarStore((state) => state.endHour || 23);
const usersCellsStopsPerHour = useCalendarStore((state) => state.gridCellsPerHour || 4);
const availableTimeslots = useCalendarStore((state) => state.availableTimeslots);
const hideHeader = useCalendarStore((state) => state.hideHeader);
const days = useMemo(() => getDaysBetweenDates(startDate, endDate), [startDate, endDate]);
const hours = useMemo(() => getHoursToDisplay(startHour || 0, endHour || 23), [startHour, endHour]);
const numberOfGridStopsPerDay = hours.length * usersCellsStopsPerHour;
const [hourSize, setHourSize] = useState(calculateHourSizeInPx(schedulerGrid?.current, startHour, endHour));
// Reset one hour size on window resize.
useLayoutEffect(() => {
const onResize = () => {
setHourSize(calculateHourSizeInPx(schedulerGrid?.current, startHour, endHour));
};
window.addEventListener("resize", onResize);
// By running this set function also one time in the uselayouteffect, instead of
// only in the default value of useState, we make sure that the ref is rendered
// to the screen and we read the correct value.
setHourSize(calculateHourSizeInPx(schedulerGrid?.current, startHour, endHour));
return () => window.removeEventListener("resize", onResize);
}, [startHour, endHour]);
// Initalise State on inital mount
useEffect(() => {
@@ -41,13 +59,18 @@ export function Calendar(props: CalendarComponentProps) {
<div
className="scheduler-wrapper flex h-full w-full flex-col overflow-y-scroll"
style={
{ "--one-minute-height": `calc(1.75rem/(60/${usersCellsStopsPerHour}))` } as React.CSSProperties // This can't live in the css file because it's a dynamic value and css variable gets super
{
"--one-minute-height": `calc(${hourSize}px/60)`,
"--gridDefaultSize": `${hourSize}px`,
} as React.CSSProperties // This can't live in the css file because it's a dynamic value and css variable gets super
}>
<SchedulerHeading />
<div ref={container} className="bg-default relative isolate flex flex-auto flex-col">
{hideHeader !== true && <SchedulerHeading />}
<div
ref={container}
className="bg-default dark:bg-muted relative isolate flex h-full flex-auto flex-col">
<div
style={{ width: "165%" }}
className="flex max-w-full flex-none flex-col sm:max-w-none md:max-w-full">
className="flex h-full max-w-full flex-none flex-col sm:max-w-none md:max-w-full">
<DateValues containerNavRef={containerNav} days={days} />
{/* TODO: Implement this at a later date. */}
{/* <CurrentTime
@@ -56,8 +79,14 @@ export function Calendar(props: CalendarComponentProps) {
containerRef={container}
/> */}
<div className="flex flex-auto">
<div className="bg-default ring-muted sticky left-0 z-10 w-14 flex-none ring-1" />
<div className="grid flex-auto grid-cols-1 grid-rows-1 ">
<div className="bg-default dark:bg-muted ring-muted sticky left-0 z-10 w-14 flex-none ring-1" />
<div
className="grid flex-auto grid-cols-1 grid-rows-1 [--disabled-gradient-foreground:#E6E7EB] [--disabled-gradient-background:#F8F9FB] dark:[--disabled-gradient-background:#262626] dark:[--disabled-gradient-foreground:#393939]"
style={{
backgroundColor: "var(--disabled-gradient-background)",
background:
"repeating-linear-gradient(-45deg, var(--disabled-gradient-background), var(--disabled-gradient-background) 2.5px, var(--disabled-gradient-foreground) 2.5px, var(--disabled-gradient-foreground) 5px)",
}}>
<HorizontalLines
hours={hours}
numberOfGridStopsPerCell={usersCellsStopsPerHour}
@@ -68,15 +97,16 @@ export function Calendar(props: CalendarComponentProps) {
{/* Empty Cells */}
<SchedulerColumns
zIndex={50}
ref={schedulerGrid}
offsetHeight={containerOffset.current?.offsetHeight}
gridStopsPerDay={numberOfGridStopsPerDay}>
<>
{[...Array(days.length)].map((_, i) => (
<li
className="relative"
key={i}
style={{
gridRow: `2 / span ${numberOfGridStopsPerDay}`,
position: "relative",
}}>
{/* While startDate < endDate: */}
{[...Array(numberOfGridStopsPerDay)].map((_, j) => {
@@ -89,6 +119,7 @@ export function Calendar(props: CalendarComponentProps) {
totalGridCells={numberOfGridStopsPerDay}
selectionLength={endHour - startHour}
startHour={startHour}
availableSlots={availableTimeslots}
/>
);
})}
@@ -105,7 +136,7 @@ export function Calendar(props: CalendarComponentProps) {
return (
<li key={day.toISOString()} className="relative" style={{ gridColumnStart: i + 1 }}>
<EventList day={day} />
<BlockedList day={day} containerRef={container} />
{/* <BlockedList day={day} containerRef={container} /> */}
</li>
);
})}
@@ -127,7 +158,7 @@ const MobileNotSupported = ({ children }: { children: React.ReactNode }) => {
<h1 className="text-2xl font-bold">Mobile not supported yet </h1>
<p className="text-subtle">Please use a desktop browser to view this page</p>
</div>
<div className="hidden sm:block">{children}</div>
<div className="hidden h-full sm:block">{children}</div>
</>
);
};
@@ -12,7 +12,7 @@ export function DateValues({ days, containerNavRef }: Props) {
return (
<div
ref={containerNavRef}
className="bg-default sticky top-0 z-30 flex-none border-b border-b-gray-300 sm:pr-8">
className="bg-default dark:bg-muted border-b-subtle sticky top-0 z-30 flex-none border-b sm:pr-8">
<div className="text-subtle flex text-sm leading-6 sm:hidden" data-dayslength={days.length}>
{days.map((day) => {
const isToday = dayjs().isSame(day, "day");
@@ -40,7 +40,10 @@ export function DateValues({ days, containerNavRef }: Props) {
return (
<div
key={day.toString()}
className={classNames("flex flex-1 items-center justify-center py-3", isToday && "font-bold")}>
className={classNames(
"flex flex-1 items-center justify-center py-3 text-xs font-medium uppercase",
isToday && "font-bold"
)}>
<span>
{day.format("ddd")}{" "}
<span
@@ -1,5 +1,5 @@
import { useMemo } from "react";
import shallow from "zustand/shallow";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";
@@ -3,12 +3,14 @@ import { classNames } from "@calcom/lib";
export function BlockedTimeCell() {
return (
<div
className={classNames("group absolute inset-0 flex h-full flex-col hover:cursor-not-allowed")}
className={classNames(
"group absolute inset-0 flex h-full flex-col hover:cursor-not-allowed",
"[--disabled-gradient-background:#E5E7EB] [--disabled-gradient-foreground:#D1D5DB] dark:[--disabled-gradient-background:#262626] dark:[--disabled-gradient-foreground:#393939]"
)}
style={{
backgroundColor: "#D1D5DB",
opacity: 0.2,
background:
"repeating-linear-gradient( -45deg, #E5E7EB, #E5E7EB 4.5px, #D1D5DB 4.5px, #D1D5DB 22.5px )",
"repeating-linear-gradient( -45deg, var(--disabled-gradient-background), var(--disabled-gradient-background) 2.5px, var(--disabled-gradient-foreground) 2.5px, var(--disabled-gradient-foreground) 6.5px )",
}}
/>
);
@@ -1,10 +1,18 @@
import shallow from "zustand/shallow";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";
import { classNames } from "@calcom/lib";
import { useCalendarStore } from "../../state/store";
import type { CalendarAvailableTimeslots } from "../../types/state";
import type { GridCellToDateProps } from "../../utils";
import { gridCellToDateTime } from "../../utils";
export function EmptyCell(props: GridCellToDateProps) {
type EmptyCellProps = GridCellToDateProps & {
availableSlots?: CalendarAvailableTimeslots;
};
export function EmptyCell(props: EmptyCellProps) {
const { onEmptyCellClick, hoverEventDuration } = useCalendarStore(
(state) => ({
onEmptyCellClick: state.onEmptyCellClick,
@@ -21,22 +29,41 @@ export function EmptyCell(props: GridCellToDateProps) {
startHour: props.startHour,
});
// Empty cell won't show it self (be disabled) when
// availableslots is passed in, and it's curren time is not part of the available slots
const isDisabled =
props.availableSlots &&
!props.availableSlots[dayjs(props.day).format("YYYY-MM-DD")]?.find(
(slot) => slot.start.getTime() === cellToDate.toDate().getTime()
);
return (
<div
className="group w-full"
style={{ height: "1.75rem", overflow: "visible" }}
className={classNames(
"group w-full",
isDisabled && "pointer-events-none",
!isDisabled && "bg-default dark:bg-muted"
)}
data-disabled={isDisabled}
data-day={props.day.toISOString()}
style={{ height: `calc(${hoverEventDuration}*var(--one-minute-height))`, overflow: "visible" }}
onClick={() => onEmptyCellClick && onEmptyCellClick(cellToDate.toDate())}>
{hoverEventDuration !== 0 && (
{!isDisabled && hoverEventDuration !== 0 && (
<div
className="opacity-4 bg-subtle hover:bg-emphasis text-emphasis absolute inset-x-1 hidden rounded-[4px]
border-[1px]
border-gray-900 py-1 px-[6px] text-xs font-semibold leading-5 group-hover:block group-hover:cursor-pointer"
className="opacity-4 bg-subtle hover:bg-emphasis text-emphasis dark:border-emphasis absolute hidden
rounded-[4px]
border-[1px] border-gray-900 py-1 px-[6px] text-xs font-semibold leading-5 group-hover:block group-hover:cursor-pointer"
style={{
height: `calc(${hoverEventDuration}*var(--one-minute-height))`,
zIndex: 49,
width: "90%",
// @TODO: This used to be 90% as per Sean's work. I think this was needed when
// multiple events are stacked next to each other. We might need to add this back later.
width: "100%",
}}>
<div className="overflow-ellipsis leading-4">{cellToDate.format("HH:mm")}</div>
<div className=" overflow-ellipsis leading-4">
{cellToDate.format("HH:mm")}
<span className="ml-2 inline">Click to select</span>
</div>
</div>
)}
</div>
@@ -1,4 +1,4 @@
import shallow from "zustand/shallow";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";
@@ -7,13 +7,17 @@ type Props = {
zIndex?: number;
};
export function SchedulerColumns({ offsetHeight, gridStopsPerDay, children, zIndex }: Props) {
export const SchedulerColumns = React.forwardRef<HTMLOListElement, Props>(function SchedulerColumns(
{ offsetHeight, gridStopsPerDay, children, zIndex },
ref
) {
return (
<ol
className="scheduler-grid-row-template col-start-1 col-end-2 row-start-1 grid auto-cols-auto sm:pr-8"
ref={ref}
className="scheduler-grid-row-template col-start-1 col-end-2 row-start-1 grid auto-cols-auto text-[0px] sm:pr-8"
style={{ marginTop: offsetHeight || "var(--gridDefaultSize)", zIndex }}
data-gridstopsperday={gridStopsPerDay}>
{children}
</ol>
);
}
});
@@ -15,9 +15,9 @@ export const HorizontalLines = ({
const id = useId();
return (
<div
className=" divide-subtle col-start-1 col-end-2 row-start-1 grid divide-y"
className=" divide-default pointer-events-none relative z-[60] col-start-1 col-end-2 row-start-1 grid divide-y"
style={{
gridTemplateRows: `repeat(${hours.length}, minmax(${1.75 * numberOfGridStopsPerCell}rem,1fr)`,
gridTemplateRows: `repeat(${hours.length}, minmax(var(--gridDefaultSize),1fr)`,
}}>
<div className="row-end-1 h-7 " ref={containerOffsetRef} />
{hours.map((hour) => (
@@ -3,8 +3,8 @@ import type dayjs from "@calcom/dayjs";
export const VeritcalLines = ({ days }: { days: dayjs.Dayjs[] }) => {
return (
<div
className="divide-subtle col-start-1 col-end-2 row-start-1 grid auto-cols-auto grid-rows-1 divide-x
sm:pr-8">
className="divide-default pointer-events-none relative z-[60] col-start-1 col-end-2 row-start-1 grid
auto-cols-auto grid-rows-1 divide-x sm:pr-8">
{days.map((_, i) => (
<div
key={`Key_vertical_${i}`}
@@ -1,6 +1,6 @@
import { TimeRange } from "@calcom/types/schedule";
import type { TimeRange } from "@calcom/types/schedule";
import { CalendarEvent } from "./events";
import type { CalendarEvent } from "./events";
export type View = "month" | "week" | "day";
export type Hours =
@@ -51,6 +51,11 @@ export type CalendarPrivateActions = {
handleDateChange: (payload: "INCREMENT" | "DECREMENT") => void;
};
export type CalendarAvailableTimeslots = {
// Key is the date in YYYY-MM-DD format
[key: string]: TimeRange[];
};
export type CalendarState = {
/** @NotImplemented This in future will change the view to be daily/weekly/monthly DAY/WEEK are supported currently however WEEK is the most adv.*/
view?: View;
@@ -62,6 +67,11 @@ export type CalendarState = {
* @Note Ideally you should pass in a sorted array from the DB however, pass the prop `sortEvents` if this is not possible and we will sort this for you..
*/
events: CalendarEvent[];
/**
* Instead of letting users choose any option, this will only show these timeslots.
* Users can not pick any time themselves but are restricted to the available options.
*/
availableTimeslots?: CalendarAvailableTimeslots;
/** Any time ranges passed in here will display as blocked on the users calendar. Note: Anything < than the current date automatically gets blocked. */
blockingDates?: TimeRange[];
/** Loading will only expect events to be loading. */
@@ -104,6 +114,10 @@ export type CalendarState = {
* @Note It is recommended to sort the events before passing them into the scheduler - e.g. On DB level.
*/
sortEvents?: boolean;
/**
* Optional boolean to hide the main header. Default the header will be visible.
*/
hideHeader?: boolean;
};
export type CalendarComponentProps = CalendarPublicActions & CalendarState;
@@ -1,5 +1,5 @@
import dayjs from "@calcom/dayjs";
import { TimeRange } from "@calcom/types/schedule";
import type { TimeRange } from "@calcom/types/schedule";
// By default starts on Sunday (Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
export function weekdayDates(weekStart = 0, startDate: Date, length = 6) {
@@ -34,7 +34,7 @@ export function gridCellToDateTime({
// Add startHour since we use StartOfDay for day props. This could be improved by changing the getDaysBetweenDates function
// To handle the startHour+endHour
const cellDateTime = dayjs(day).add(minutesIntoSelection, "minutes").add(startHour, "hours");
const cellDateTime = dayjs(day).startOf("day").add(minutesIntoSelection, "minutes").add(startHour, "hours");
return cellDateTime;
}
@@ -90,3 +90,17 @@ export function mergeOverlappingDateRanges(dateRanges: TimeRange[]) {
}
return mergedDateRanges;
}
export function calculateHourSizeInPx(
gridElementRef: HTMLOListElement | null,
startHour: number,
endHour: number
) {
// Gap added at bottom to give calendar some breathing room.
// I guess we could come up with a better way to do this in the future.
const gapOnBottom = 50;
// In case the calendar has for example a header above it. We take a look at the
// distance the grid is rendered from the top, and subtract that from the height.
const offsetFromTop = gridElementRef?.getBoundingClientRect().top ?? 65;
return (window.innerHeight - offsetFromTop - gapOnBottom) / (endHour - startHour);
}
@@ -426,10 +426,7 @@ export const FormBuilder = function FormBuilder({
}>
<DialogContent className="max-h-none p-0" data-testid="edit-field-dialog">
<div className="h-auto max-h-[85vh] overflow-auto px-8 pt-8 pb-7">
<DialogHeader
title={t("add_a_booking_question")}
subtitle={t("booking_questions_description")}
/>
<DialogHeader title={t("add_a_booking_question")} subtitle={t("booking_questions_description")} />
<Form
id="form-builder"
form={fieldForm}