fix: Calendar View (#10514)

Co-authored-by: Sean Brydon <sean@cal.com>
This commit is contained in:
Udit Takkar
2023-08-09 12:19:01 +02:00
committed by GitHub
co-authored by Sean Brydon
parent fcd892bfa0
commit cf89598279
8 changed files with 72 additions and 39 deletions
@@ -57,7 +57,7 @@ export function Header({
return <LayoutToggleWithData />;
}
const endDate = selectedDate.add(extraDays, "days");
const endDate = selectedDate.add(extraDays - 1, "days");
const isSameMonth = () => {
return selectedDate.format("MMM") === endDate.format("MMM");
@@ -4,7 +4,6 @@ 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 { useEvent, useScheduleForEvent } from "../utils/event";
@@ -16,7 +15,6 @@ export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
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;
@@ -28,15 +26,18 @@ export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
for (const day in schedule.data.slots) {
availableTimeslots[day] = schedule.data.slots[day].map((slot) => ({
// 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")),
start: dayjs(slot.time).toDate(),
end: dayjs(slot.time).add(eventDuration, "minutes").toDate(),
}));
}
return availableTimeslots;
}, [schedule, timezone, eventDuration]);
}, [schedule, eventDuration]);
const startDate = selectedDate ? dayjs(selectedDate).toDate() : dayjs().toDate();
const endDate = dayjs(startDate)
.add(extraDays - 1, "day")
.toDate();
return (
<div className="h-full [--calendar-dates-sticky-offset:66px]">
@@ -46,9 +47,9 @@ export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
startHour={0}
endHour={23}
events={[]}
startDate={selectedDate ? new Date(selectedDate) : new Date()}
endDate={dayjs(selectedDate).add(extraDays, "day").toDate()}
onEmptyCellClick={(date) => setSelectedTimeslot(date.toString())}
startDate={startDate}
endDate={endDate}
onEmptyCellClick={(date) => setSelectedTimeslot(date.toISOString())}
gridCellsPerHour={60 / eventDuration}
hoverEventDuration={eventDuration}
hideHeader
@@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useRef } from "react";
import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences";
import { classNames } from "@calcom/lib";
import { useCalendarStore } from "../state/store";
@@ -22,6 +23,7 @@ export function Calendar(props: CalendarComponentProps) {
const containerOffset = useRef<HTMLDivElement | null>(null);
const schedulerGrid = useRef<HTMLOListElement | null>(null);
const initalState = useCalendarStore((state) => state.initState);
const { timezone } = useTimePreferences();
const startDate = useCalendarStore((state) => state.startDate);
const endDate = useCalendarStore((state) => state.endDate);
@@ -32,7 +34,11 @@ 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 hours = useMemo(
() => getHoursToDisplay(startHour || 0, endHour || 23, timezone),
[startHour, endHour, timezone]
);
const numberOfGridStopsPerDay = hours.length * usersCellsStopsPerHour;
const hourSize = 58;
@@ -108,7 +114,7 @@ export function Calendar(props: CalendarComponentProps) {
{availableTimeslots ? (
<AvailableCellsForDay
key={days[i].toISOString()}
day={days[i].toDate()}
day={days[i]}
startHour={startHour}
availableSlots={availableTimeslots}
/>
@@ -119,11 +125,12 @@ export function Calendar(props: CalendarComponentProps) {
return (
<EmptyCell
key={key}
day={days[i].toDate()}
day={days[i]}
gridCellIdx={j}
totalGridCells={numberOfGridStopsPerDay}
selectionLength={endHour - startHour}
startHour={startHour}
timezone={timezone}
/>
);
})}
@@ -5,6 +5,12 @@ import { useTimePreferences } from "@calcom/features/bookings/lib";
import { useCalendarStore } from "../../state/store";
function calculateMinutesFromStart(startHour: number, currentHour: number, currentMinute: number) {
const startMinute = startHour * 60;
const currentMinuteOfDay = currentHour * 60 + currentMinute;
return currentMinuteOfDay - startMinute;
}
export function CurrentTime() {
const currentTimeRef = useRef<HTMLDivElement>(null);
const [scrolledIntoView, setScrolledIntoView] = useState(false);
@@ -13,19 +19,21 @@ export function CurrentTime() {
startHour: state.startHour || 0,
endHour: state.endHour || 23,
}));
const { timeFormat } = useTimePreferences();
const { timeFormat, timezone } = 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();
const currentDateTime = dayjs().tz(timezone); // Get current date and time in the specified timezone
const currentHour = currentDateTime.hour();
const currentMinute = currentDateTime.minute();
if (currentHour > endHour || currentHour < startHour) {
setCurrentTimePos(null);
}
const minutesFromStart = currentMinute - startHour * 60;
const minutesFromStart = calculateMinutesFromStart(startHour, currentHour, currentMinute);
setCurrentTimePos(minutesFromStart);
if (!currentTimeRef.current || scrolledIntoView) return;
@@ -35,7 +43,7 @@ export function CurrentTime() {
setScrolledIntoView(true);
}, 100);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [startHour, endHour, scrolledIntoView]);
}, [startHour, endHour, scrolledIntoView, timezone]);
return (
<div
@@ -46,7 +54,7 @@ export function CurrentTime() {
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="w-14 pr-2 text-right">{dayjs().tz(timezone).format(timeFormat)}</div>
<div className="bg-inverted h-3 w-px" />
<div className="bg-inverted h-px w-screen" />
</div>
@@ -23,12 +23,12 @@ export function EmptyCell(props: EmptyCellProps) {
totalGridCells: props.totalGridCells,
selectionLength: props.selectionLength,
startHour: props.startHour,
timezone: props.timezone,
});
const minuesFromStart =
(cellToDate.toDate().getHours() - props.startHour) * 60 + cellToDate.toDate().getMinutes();
const minuesFromStart = (cellToDate.hour() - props.startHour) * 60 + cellToDate.minute();
return <Cell topOffsetMinutes={minuesFromStart} timeSlot={cellToDate} />;
return <Cell topOffsetMinutes={minuesFromStart} timeSlot={dayjs(cellToDate).tz(props.timezone)} />;
}
type AvailableCellProps = {
@@ -38,26 +38,29 @@ type AvailableCellProps = {
};
export function AvailableCellsForDay({ availableSlots, day, startHour }: AvailableCellProps) {
const { timezone } = useTimePreferences();
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(),
topOffsetMinutes:
(dayjs(slot.start).tz(timezone).hour() - startHour) * 60 + dayjs(slot.start).tz(timezone).minute(),
})),
[slotsForToday, startHour]
[slotsForToday, startHour, timezone]
);
if (!availableSlots) return null;
return (
<>
{slots?.map((slot) => {
{slots?.map((slot, index) => {
return (
<Cell
key={slot.slot.start.toISOString()}
timeSlot={dayjs(slot.slot.start)}
key={index}
timeSlot={dayjs(slot.slot.start).tz(timezone)}
topOffsetMinutes={slot.topOffsetMinutes}
/>
);
@@ -73,7 +76,8 @@ type CellProps = {
};
function Cell({ isDisabled, topOffsetMinutes, timeSlot }: CellProps) {
const timeFormat = useTimePreferences((state) => state.timeFormat);
const { timeFormat } = useTimePreferences();
const { onEmptyCellClick, hoverEventDuration } = useCalendarStore(
(state) => ({
onEmptyCellClick: state.onEmptyCellClick,
@@ -97,7 +101,9 @@ function Cell({ isDisabled, topOffsetMinutes, timeSlot }: CellProps) {
overflow: "visible",
top: topOffsetMinutes ? `calc(${topOffsetMinutes}*var(--one-minute-height))` : undefined,
}}
onClick={() => onEmptyCellClick && onEmptyCellClick(timeSlot.toDate())}>
onClick={() => {
onEmptyCellClick && onEmptyCellClick(timeSlot.toDate());
}}>
{!isDisabled && hoverEventDuration !== 0 && (
<div
className={classNames(
@@ -16,6 +16,7 @@ export const HorizontalLines = ({
// 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
className=" divide-default pointer-events-none relative z-[60] col-start-1 col-end-2 row-start-1 grid divide-y"
@@ -53,6 +53,7 @@ export type CalendarPrivateActions = {
export type CalendarAvailableTimeslots = {
// Key is the date in YYYY-MM-DD format
// start and end are ISOstring
[key: string]: TimeRange[];
};
@@ -13,11 +13,12 @@ export function weekdayDates(weekStart = 0, startDate: Date, length = 6) {
};
}
export type GridCellToDateProps = {
day: Date;
day: dayjs.Dayjs;
gridCellIdx: number;
totalGridCells: number;
selectionLength: number;
startHour: number;
timezone: string;
};
export function gridCellToDateTime({
@@ -26,6 +27,7 @@ export function gridCellToDateTime({
totalGridCells,
selectionLength,
startHour,
timezone,
}: GridCellToDateProps) {
// endHour - startHour = selectionLength
const minutesInSelection = (selectionLength + 1) * 60;
@@ -34,27 +36,34 @@ 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).startOf("day").add(minutesIntoSelection, "minutes").add(startHour, "hours");
const cellDateTime = dayjs(day)
.tz(timezone)
.startOf("day")
.add(minutesIntoSelection, "minutes")
.add(startHour, "hours");
return cellDateTime;
}
export function getDaysBetweenDates(dateFrom: Date, dateTo: Date) {
const dates = []; // this is as dayjs date
let startDate = dayjs(dateFrom).utc().hour(0).minute(0).second(0).millisecond(0);
let startDate = dayjs(dateFrom).hour(0).minute(0).second(0).millisecond(0);
dates.push(startDate);
const endDate = dayjs(dateTo).utc().hour(0).minute(0).second(0).millisecond(0);
const endDate = dayjs(dateTo).hour(0).minute(0).second(0).millisecond(0);
while (startDate.isBefore(endDate)) {
dates.push(startDate.add(1, "day"));
startDate = startDate.add(1, "day");
}
return dates;
return dates.slice(0, 7);
}
export function getHoursToDisplay(startHour: number, endHour: number) {
export function getHoursToDisplay(startHour: number, endHour: number, timezone?: string) {
const dates = []; // this is as dayjs date
let startDate = dayjs("1970-01-01").utc().hour(startHour);
let startDate = dayjs("1970-01-01").tz(timezone).hour(startHour);
dates.push(startDate);
const endDate = dayjs("1970-01-01").utc().hour(endHour);
const endDate = dayjs("1970-01-01").tz(timezone).hour(endHour);
while (startDate.isBefore(endDate)) {
dates.push(startDate.add(1, "hour"));
startDate = startDate.add(1, "hour");