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:
1 parent
a33dbd8922
commit
ca47a589ba
17 files changed
+236
-98
No files matched your search
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user