feat: weekly view calendar improvements (#9491)
* Add correct start/end time for weekly calendar by looking at calendar's availability * Always show 24h in weekly calendar, added scroll functionality, current time and events in correct timezone * Improved current time style * Show slots for every minute, so also odd timeslots due to offsets work * Show correct timeformat for weekly view on left side time stamps * Fix data attr for debug * Position events differently so we can accomodate for offset start times. * Removed schedule from public event api, because we don't use it anymore * Fixed alignment of timeslots in weekview calendar. * Added loading spinner to weekly calendar * Force weekly view calendar hour labels to show 00 minutes, also for gmt x.5 timezones that are offset 30 mins. * Change event duration blocks in weekly calendar when user changes duration in multi duration event * Improved week view slot time label alignments for shorter durations --------- Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
1 parent
faac3d0f57
commit
955de3133c
13 files changed
+233
-138
No files matched your search
@@ -96,9 +96,13 @@ module.exports = {
|
||||
from: { opacity: 0, transform: "translateY(10px)" },
|
||||
to: { opacity: 1, transform: "none" },
|
||||
},
|
||||
spinning: {
|
||||
"100%": { transform: "rotate(360deg)" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"fade-in-up": "fade-in-up 600ms var(--animation-delay, 0ms) cubic-bezier(.21,1.02,.73,1) forwards",
|
||||
spinning: "spinning 0.75s linear infinite",
|
||||
},
|
||||
boxShadow: {
|
||||
dropdown: "0px 2px 6px -1px rgba(0, 0, 0, 0.08)",
|
||||
|
||||
@@ -58,7 +58,7 @@ const BookerComponent = ({
|
||||
const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings;
|
||||
const animationScope = useBookerResizeAnimation(layout, bookerState);
|
||||
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
|
||||
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
|
||||
|
||||
// We only want the initial url value, that's why we memo it. The embed seems to change the url, which sometimes drops
|
||||
// the layout query param.
|
||||
const layoutFromQueryParam = useMemo(() => validateLayout(getQueryParam("layout") as BookerLayouts), []);
|
||||
@@ -109,21 +109,27 @@ const BookerComponent = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-default flex h-full w-full flex-col items-center overflow-x-clip">
|
||||
<div className="text-default flex min-h-full w-full flex-col items-center overflow-clip">
|
||||
<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 dark:[color-scheme:dark] sm:duration-300 sm:motion-reduce:transition-none md:flex-row",
|
||||
layout === BookerLayouts.MONTH_VIEW && "rounded-md border",
|
||||
"bg-default dark:bg-muted grid max-w-full items-start dark:[color-scheme:dark] sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row",
|
||||
layout === BookerLayouts.MONTH_VIEW && "border-subtle rounded-md border",
|
||||
!isEmbed && "sm:transition-[width] sm:duration-300",
|
||||
isEmbed && layout === BookerLayouts.MONTH_VIEW && "border-booker sm:border-booker-width",
|
||||
!isEmbed && layout === BookerLayouts.MONTH_VIEW && "border-subtle",
|
||||
layout === BookerLayouts.MONTH_VIEW && isEmbed && "mt-20"
|
||||
)}>
|
||||
<AnimatePresence>
|
||||
<BookerSection area="header" className={classNames(isMonthView && "fixed top-3 right-3 z-10")}>
|
||||
<BookerSection
|
||||
area="header"
|
||||
className={classNames(
|
||||
layout === BookerLayouts.MONTH_VIEW && "fixed top-3 right-3 z-10",
|
||||
(layout === BookerLayouts.COLUMN_VIEW || layout === BookerLayouts.WEEK_VIEW) &&
|
||||
"bg-muted sticky top-0 z-10"
|
||||
)}>
|
||||
<Header
|
||||
enabledLayouts={bookerLayouts.enabledLayouts}
|
||||
extraDays={extraDays}
|
||||
|
||||
@@ -4,6 +4,7 @@ import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Badge, Dialog, DialogContent } from "@calcom/ui";
|
||||
|
||||
import { useTimePreferences } from "../../../lib";
|
||||
import { useBookerStore } from "../../store";
|
||||
import { useEvent } from "../../utils/event";
|
||||
import { BookEventForm } from "./BookEventForm";
|
||||
@@ -14,6 +15,7 @@ export function BookFormAsModal({ visible, onCancel }: { visible: boolean; onCan
|
||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
||||
const { data } = useEvent();
|
||||
const parsedSelectedTimeslot = dayjs(selectedTimeslot);
|
||||
const { timeFormat } = useTimePreferences();
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={onCancel}>
|
||||
@@ -24,7 +26,9 @@ export function BookFormAsModal({ visible, onCancel }: { visible: boolean; onCan
|
||||
<h1 className="font-cal text-emphasis text-xl leading-5">{t("confirm_your_details")} </h1>
|
||||
<div className="my-4 flex space-x-2 rounded-md leading-none">
|
||||
<Badge variant="grayWithoutHover" startIcon={Calendar} size="lg">
|
||||
<span>{parsedSelectedTimeslot.format("LLL")}</span>
|
||||
<span>
|
||||
{parsedSelectedTimeslot.format("LL")} {parsedSelectedTimeslot.format(timeFormat)}
|
||||
</span>
|
||||
</Badge>
|
||||
{(selectedDuration || data?.length) && (
|
||||
<Badge variant="grayWithoutHover" startIcon={Clock} size="lg">
|
||||
|
||||
@@ -4,43 +4,53 @@ import dayjs from "@calcom/dayjs";
|
||||
import { Calendar } from "@calcom/features/calendars/weeklyview";
|
||||
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
|
||||
|
||||
import { useTimePreferences } from "../../lib/timePreferences";
|
||||
import { useBookerStore } from "../store";
|
||||
import { useScheduleForEvent } from "../utils/event";
|
||||
import { useEvent, useScheduleForEvent } from "../utils/event";
|
||||
|
||||
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 selectedEventDuration = useBookerStore((state) => state.selectedDuration);
|
||||
const schedule = useScheduleForEvent({
|
||||
prefetchNextMonth: !!extraDays && dayjs(date).month() !== dayjs(date).add(extraDays, "day").month(),
|
||||
});
|
||||
const { timezone } = useTimePreferences();
|
||||
|
||||
const event = useEvent();
|
||||
const eventDuration = selectedEventDuration || event?.data?.length || 30;
|
||||
|
||||
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(),
|
||||
// First formatting to LLL and then passing it to date prevents toDate()
|
||||
// from changing the timezone to users local machine (instead of itmezone selected in UI dropdown)
|
||||
start: new Date(dayjs(slot.time).utc().tz(timezone).format("LLL")),
|
||||
end: new Date(dayjs(slot.time).utc().tz(timezone).add(eventDuration, "minutes").format("LLL")),
|
||||
}));
|
||||
}
|
||||
|
||||
return availableTimeslots;
|
||||
}, [schedule]);
|
||||
}, [schedule, timezone, eventDuration]);
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className="h-full [--calendar-dates-sticky-offset:66px]">
|
||||
<Calendar
|
||||
isLoading={schedule.isLoading}
|
||||
availableTimeslots={availableSlots}
|
||||
startHour={8}
|
||||
endHour={18}
|
||||
startHour={0}
|
||||
endHour={23}
|
||||
events={[]}
|
||||
startDate={selectedDate ? new Date(selectedDate) : new Date()}
|
||||
endDate={dayjs(selectedDate).add(extraDays, "day").toDate()}
|
||||
onEmptyCellClick={(date) => setSelectedTimeslot(date.toString())}
|
||||
gridCellsPerHour={2}
|
||||
hoverEventDuration={30}
|
||||
gridCellsPerHour={60 / eventDuration}
|
||||
hoverEventDuration={eventDuration}
|
||||
hideHeader
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -87,8 +87,8 @@ export const resizeAnimationConfig: ResizeAnimationConfig = {
|
||||
week_view: {
|
||||
default: {
|
||||
width: "100vw",
|
||||
minHeight: "450px",
|
||||
height: "100vh",
|
||||
minHeight: "100vh",
|
||||
height: "auto",
|
||||
gridTemplateAreas: `
|
||||
"meta header header"
|
||||
"meta main main"
|
||||
@@ -100,8 +100,8 @@ export const resizeAnimationConfig: ResizeAnimationConfig = {
|
||||
column_view: {
|
||||
default: {
|
||||
width: "100vw",
|
||||
minHeight: "450px",
|
||||
height: "100vh",
|
||||
minHeight: "100vh",
|
||||
height: "auto",
|
||||
gridTemplateAreas: `
|
||||
"meta header header"
|
||||
"meta main main"
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
import { useCalendarStore } from "../state/store";
|
||||
import "../styles/styles.css";
|
||||
import type { CalendarComponentProps } from "../types/state";
|
||||
import { calculateHourSizeInPx, getDaysBetweenDates, getHoursToDisplay } from "../utils";
|
||||
import { getDaysBetweenDates, getHoursToDisplay } from "../utils";
|
||||
import { DateValues } from "./DateValues";
|
||||
import { EmptyCell } from "./event/Empty";
|
||||
import { CurrentTime } from "./currentTime";
|
||||
import { AvailableCellsForDay, EmptyCell } from "./event/Empty";
|
||||
import { EventList } from "./event/EventList";
|
||||
import { SchedulerColumns } from "./grid";
|
||||
import { SchedulerHeading } from "./heading/SchedulerHeading";
|
||||
import { HorizontalLines } from "./horizontalLines";
|
||||
import { Spinner } from "./spinner/Spinner";
|
||||
import { VeritcalLines } from "./verticalLines";
|
||||
|
||||
export function Calendar(props: CalendarComponentProps) {
|
||||
@@ -28,26 +32,9 @@ export function Calendar(props: CalendarComponentProps) {
|
||||
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]);
|
||||
const hourSize = 58;
|
||||
|
||||
// Initalise State on inital mount
|
||||
useEffect(() => {
|
||||
@@ -57,7 +44,7 @@ export function Calendar(props: CalendarComponentProps) {
|
||||
return (
|
||||
<MobileNotSupported>
|
||||
<div
|
||||
className="scheduler-wrapper flex h-full w-full flex-col overflow-y-scroll"
|
||||
className={classNames("scheduler-wrapper flex h-full w-full flex-col")}
|
||||
style={
|
||||
{
|
||||
"--one-minute-height": `calc(${hourSize}px/60)`,
|
||||
@@ -65,6 +52,7 @@ export function Calendar(props: CalendarComponentProps) {
|
||||
} as React.CSSProperties // This can't live in the css file because it's a dynamic value and css variable gets super
|
||||
}>
|
||||
{hideHeader !== true && <SchedulerHeading />}
|
||||
{props.isLoading && <Spinner />}
|
||||
<div
|
||||
ref={container}
|
||||
className="bg-default dark:bg-muted relative isolate flex h-full flex-auto flex-col">
|
||||
@@ -72,13 +60,8 @@ export function Calendar(props: CalendarComponentProps) {
|
||||
style={{ width: "165%" }}
|
||||
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
|
||||
containerNavRef={containerNav}
|
||||
containerOffsetRef={containerOffset}
|
||||
containerRef={container}
|
||||
/> */}
|
||||
<div className="flex flex-auto">
|
||||
<div className="relative flex flex-auto">
|
||||
<CurrentTime />
|
||||
<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]"
|
||||
@@ -94,40 +77,6 @@ export function Calendar(props: CalendarComponentProps) {
|
||||
/>
|
||||
<VeritcalLines days={days} />
|
||||
|
||||
{/* 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}`,
|
||||
}}>
|
||||
{/* While startDate < endDate: */}
|
||||
{[...Array(numberOfGridStopsPerDay)].map((_, j) => {
|
||||
const key = `${i}-${j}`;
|
||||
return (
|
||||
<EmptyCell
|
||||
key={key}
|
||||
day={days[i].toDate()}
|
||||
gridCellIdx={j}
|
||||
totalGridCells={numberOfGridStopsPerDay}
|
||||
selectionLength={endHour - startHour}
|
||||
startHour={startHour}
|
||||
availableSlots={availableTimeslots}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
</SchedulerColumns>
|
||||
|
||||
<SchedulerColumns
|
||||
offsetHeight={containerOffset.current?.offsetHeight}
|
||||
gridStopsPerDay={numberOfGridStopsPerDay}>
|
||||
@@ -141,6 +90,49 @@ export function Calendar(props: CalendarComponentProps) {
|
||||
);
|
||||
})}
|
||||
</SchedulerColumns>
|
||||
|
||||
{/* Empty Cells */}
|
||||
<SchedulerColumns
|
||||
ref={schedulerGrid}
|
||||
offsetHeight={containerOffset.current?.offsetHeight}
|
||||
gridStopsPerDay={numberOfGridStopsPerDay}>
|
||||
<>
|
||||
{[...Array(days.length)].map((_, i) => (
|
||||
<li
|
||||
className="relative"
|
||||
key={i}
|
||||
style={{
|
||||
gridRow: `1 / span ${numberOfGridStopsPerDay}`,
|
||||
}}>
|
||||
{/* While startDate < endDate: */}
|
||||
{availableTimeslots ? (
|
||||
<AvailableCellsForDay
|
||||
key={days[i].toISOString()}
|
||||
day={days[i].toDate()}
|
||||
startHour={startHour}
|
||||
availableSlots={availableTimeslots}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{[...Array(numberOfGridStopsPerDay)].map((_, j) => {
|
||||
const key = `${i}-${j}`;
|
||||
return (
|
||||
<EmptyCell
|
||||
key={key}
|
||||
day={days[i].toDate()}
|
||||
gridCellIdx={j}
|
||||
totalGridCells={numberOfGridStopsPerDay}
|
||||
selectionLength={endHour - startHour}
|
||||
startHour={startHour}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
</SchedulerColumns>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ export function DateValues({ days, containerNavRef }: Props) {
|
||||
return (
|
||||
<div
|
||||
ref={containerNavRef}
|
||||
className="bg-default dark:bg-muted border-b-subtle sticky top-0 z-30 flex-none border-b sm:pr-8">
|
||||
className="bg-default dark:bg-muted border-b-subtle sticky top-[var(--calendar-dates-sticky-offset,0px)] z-[80] 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");
|
||||
|
||||
@@ -1,43 +1,54 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
|
||||
import { useCalendarStore } from "../../state/store";
|
||||
|
||||
type Props = {
|
||||
containerNavRef: React.RefObject<HTMLDivElement>;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
containerOffsetRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export function CurrentTime({ containerOffsetRef }: Props) {
|
||||
const [currentTimePos, setCurrentTimePos] = useState<number>(0);
|
||||
export function CurrentTime() {
|
||||
const currentTimeRef = useRef<HTMLDivElement>(null);
|
||||
const [scrolledIntoView, setScrolledIntoView] = useState(false);
|
||||
const [currentTimePos, setCurrentTimePos] = useState<number | null>(null);
|
||||
const { startHour, endHour } = useCalendarStore((state) => ({
|
||||
startHour: state.startHour || 0,
|
||||
endHour: state.endHour || 23,
|
||||
}));
|
||||
const { timeFormat } = useTimePreferences();
|
||||
|
||||
useEffect(() => {
|
||||
// Set the container scroll position based on the current time.
|
||||
const currentHour = new Date().getHours();
|
||||
let currentMinute = new Date().getHours() * 60;
|
||||
currentMinute = currentMinute + new Date().getMinutes();
|
||||
|
||||
if (containerOffsetRef.current) {
|
||||
const totalHours = endHour - startHour;
|
||||
const currentTimePos = currentMinute;
|
||||
setCurrentTimePos(currentTimePos);
|
||||
if (currentHour > endHour || currentHour < startHour) {
|
||||
setCurrentTimePos(null);
|
||||
}
|
||||
|
||||
const minutesFromStart = currentMinute - startHour * 60;
|
||||
setCurrentTimePos(minutesFromStart);
|
||||
|
||||
if (!currentTimeRef.current || scrolledIntoView) return;
|
||||
// Within a small timeout so element has time to render.
|
||||
setTimeout(() => {
|
||||
currentTimeRef?.current?.scrollIntoView({ block: "center" });
|
||||
setScrolledIntoView(true);
|
||||
}, 100);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [startHour, endHour]);
|
||||
}, [startHour, endHour, scrolledIntoView]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute z-40 ml-3 flex h-1 items-center justify-center border-gray-800"
|
||||
ref={currentTimeRef}
|
||||
className="absolute top-0 z-40 flex h-px items-center justify-center text-xs"
|
||||
aria-hidden="true"
|
||||
style={{ top: `calc(${currentTimePos}*var(--one-minute-height))`, zIndex: 100 }}>
|
||||
{dayjs().format("HH:mm")}
|
||||
<div className="ml-1 h-3 w-1 bg-gray-800" />
|
||||
<div className="h-1 w-full bg-gray-800" />
|
||||
style={{
|
||||
top: `calc(${currentTimePos}*var(--one-minute-height) + var(--calendar-offset-top))`,
|
||||
zIndex: 70,
|
||||
}}>
|
||||
<div className="w-14 pr-2 text-right">{dayjs().format(timeFormat)}</div>
|
||||
<div className="bg-inverted h-3 w-px" />
|
||||
<div className="bg-inverted h-px w-screen" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
import { classNames } from "@calcom/lib";
|
||||
@@ -10,10 +12,67 @@ import type { GridCellToDateProps } from "../../utils";
|
||||
import { gridCellToDateTime } from "../../utils";
|
||||
|
||||
type EmptyCellProps = GridCellToDateProps & {
|
||||
availableSlots?: CalendarAvailableTimeslots;
|
||||
isDisabled?: boolean;
|
||||
topOffsetMinutes?: number;
|
||||
};
|
||||
|
||||
export function EmptyCell(props: EmptyCellProps) {
|
||||
const cellToDate = gridCellToDateTime({
|
||||
day: props.day,
|
||||
gridCellIdx: props.gridCellIdx,
|
||||
totalGridCells: props.totalGridCells,
|
||||
selectionLength: props.selectionLength,
|
||||
startHour: props.startHour,
|
||||
});
|
||||
|
||||
const minuesFromStart =
|
||||
(cellToDate.toDate().getHours() - props.startHour) * 60 + cellToDate.toDate().getMinutes();
|
||||
|
||||
return <Cell topOffsetMinutes={minuesFromStart} timeSlot={cellToDate} />;
|
||||
}
|
||||
|
||||
type AvailableCellProps = {
|
||||
availableSlots: CalendarAvailableTimeslots;
|
||||
day: GridCellToDateProps["day"];
|
||||
startHour: GridCellToDateProps["startHour"];
|
||||
};
|
||||
|
||||
export function AvailableCellsForDay({ availableSlots, day, startHour }: AvailableCellProps) {
|
||||
const slotsForToday = availableSlots && availableSlots[dayjs(day).format("YYYY-MM-DD")];
|
||||
|
||||
const slots = useMemo(
|
||||
() =>
|
||||
slotsForToday?.map((slot) => ({
|
||||
slot,
|
||||
topOffsetMinutes: (slot.start.getHours() - startHour) * 60 + slot.start.getMinutes(),
|
||||
})),
|
||||
[slotsForToday, startHour]
|
||||
);
|
||||
|
||||
if (!availableSlots) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{slots?.map((slot) => {
|
||||
return (
|
||||
<Cell
|
||||
key={slot.slot.start.toISOString()}
|
||||
timeSlot={dayjs(slot.slot.start)}
|
||||
topOffsetMinutes={slot.topOffsetMinutes}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type CellProps = {
|
||||
isDisabled?: boolean;
|
||||
topOffsetMinutes?: number;
|
||||
timeSlot: Dayjs;
|
||||
};
|
||||
|
||||
function Cell({ isDisabled, topOffsetMinutes, timeSlot }: CellProps) {
|
||||
const timeFormat = useTimePreferences((state) => state.timeFormat);
|
||||
const { onEmptyCellClick, hoverEventDuration } = useCalendarStore(
|
||||
(state) => ({
|
||||
@@ -23,46 +82,37 @@ export function EmptyCell(props: EmptyCellProps) {
|
||||
shallow
|
||||
);
|
||||
|
||||
const cellToDate = gridCellToDateTime({
|
||||
day: props.day,
|
||||
gridCellIdx: props.gridCellIdx,
|
||||
totalGridCells: props.totalGridCells,
|
||||
selectionLength: props.selectionLength,
|
||||
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={classNames(
|
||||
"group flex w-full items-center justify-center",
|
||||
"group flex w-[calc(100%-1px)] items-center justify-center",
|
||||
isDisabled && "pointer-events-none",
|
||||
!isDisabled && "bg-default dark:bg-muted"
|
||||
!isDisabled && "bg-default dark:bg-muted",
|
||||
topOffsetMinutes && "absolute"
|
||||
)}
|
||||
data-disabled={isDisabled}
|
||||
data-day={props.day.toISOString()}
|
||||
style={{ height: `calc(${hoverEventDuration}*var(--one-minute-height))`, overflow: "visible" }}
|
||||
onClick={() => onEmptyCellClick && onEmptyCellClick(cellToDate.toDate())}>
|
||||
data-slot={timeSlot.toISOString()}
|
||||
style={{
|
||||
height: `calc(${hoverEventDuration}*var(--one-minute-height))`,
|
||||
overflow: "visible",
|
||||
top: topOffsetMinutes ? `calc(${topOffsetMinutes}*var(--one-minute-height))` : undefined,
|
||||
}}
|
||||
onClick={() => onEmptyCellClick && onEmptyCellClick(timeSlot.toDate())}>
|
||||
{!isDisabled && hoverEventDuration !== 0 && (
|
||||
<div
|
||||
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"
|
||||
className={classNames(
|
||||
"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:flex group-hover:cursor-pointer",
|
||||
hoverEventDuration && hoverEventDuration > 15 && "items-start pt-3",
|
||||
hoverEventDuration && hoverEventDuration < 15 && "items-center"
|
||||
)}
|
||||
style={{
|
||||
height: `calc(${hoverEventDuration}*var(--one-minute-height) - 2px)`,
|
||||
zIndex: 49,
|
||||
zIndex: 80,
|
||||
// @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: "calc(100% - 2px)",
|
||||
}}>
|
||||
<div className=" overflow-ellipsis leading-4">{cellToDate.format(timeFormat)}</div>
|
||||
<div className="overflow-ellipsis leading-[0]">{timeSlot.format(timeFormat)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useId } from "react";
|
||||
|
||||
import type dayjs from "@calcom/dayjs";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
|
||||
export const HorizontalLines = ({
|
||||
hours,
|
||||
@@ -11,7 +12,9 @@ export const HorizontalLines = ({
|
||||
numberOfGridStopsPerCell: number;
|
||||
containerOffsetRef: React.RefObject<HTMLDivElement>;
|
||||
}) => {
|
||||
const finalHour = hours[hours.length - 1].add(1, "hour").format("h A");
|
||||
const { timeFormat } = useTimePreferences();
|
||||
// We need to force the minute to zero, because otherwise in ex GMT+5.5, it would show :30 minute times (but at the positino of :00)
|
||||
const finalHour = hours[hours.length - 1].add(1, "hour").minute(0).format(timeFormat);
|
||||
const id = useId();
|
||||
return (
|
||||
<div
|
||||
@@ -19,15 +22,14 @@ export const HorizontalLines = ({
|
||||
style={{
|
||||
gridTemplateRows: `repeat(${hours.length}, minmax(var(--gridDefaultSize),1fr)`,
|
||||
}}>
|
||||
<div className="row-end-1 h-7 " ref={containerOffsetRef} />
|
||||
<div className="row-end-1 h-[--calendar-offset-top] " ref={containerOffsetRef} />
|
||||
{hours.map((hour) => (
|
||||
<>
|
||||
<div key={`${id}-${hour.get("hour")}`}>
|
||||
<div className="text-muted sticky left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right text-xs leading-5">
|
||||
{hour.format("h A")}
|
||||
</div>
|
||||
<div key={`${id}-${hour.get("hour")}`}>
|
||||
<div className="text-muted sticky left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right text-xs leading-5">
|
||||
{/* We need to force the minute to zero, because otherwise in ex GMT+5.5, it would show :30 minute times (but at the positino of :00) */}
|
||||
{hour.minute(0).format(timeFormat)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
))}
|
||||
<div key={`${id}-${finalHour}`}>
|
||||
<div className="text-muted sticky left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right text-xs leading-5">
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export const Spinner = () => (
|
||||
<div className="fixed left-[calc(50%+calc(var(--booker-meta-width,0px)/2))] top-1/2 z-[80] h-10 w-10 -translate-x-1/2 -translate-y-1/2">
|
||||
<svg className="h-10 w-10" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
className="fill-default"
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/>
|
||||
<path
|
||||
className="animate-spinning fill-emphasis origin-center"
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
@@ -3,6 +3,7 @@
|
||||
--hoursInDay: 24;
|
||||
--minuteInHour:60;
|
||||
--gridMobileSize: 100vw;
|
||||
--calendar-offset-top: 28px;
|
||||
}
|
||||
|
||||
.scheduler-grid-row-template{
|
||||
|
||||
@@ -120,6 +120,6 @@ export type CalendarState = {
|
||||
hideHeader?: boolean;
|
||||
};
|
||||
|
||||
export type CalendarComponentProps = CalendarPublicActions & CalendarState;
|
||||
export type CalendarComponentProps = CalendarPublicActions & CalendarState & { isLoading?: boolean };
|
||||
|
||||
export type CalendarStoreProps = CalendarComponentProps & CalendarPrivateActions;
|
||||
Reference in new issue
Block a user