Skip to content

Commit 63740c0

Browse files
authored
feat: calendar view atom v1 (calcom#23840)
* fix: refactor * add `isMonthViewProp` to header * feat: init v1 for calendar view atom * test breaking toggle buttons * fix: make sure week start is always sunday for calendar view atom * fixup * fix: remove extra comments * fix: add calendar view page in examples app * chore: add changesets * fix: coderabbit feedback * fixup
1 parent c5942bc commit 63740c0

15 files changed

Lines changed: 413 additions & 56 deletions

File tree

.changeset/fuzzy-beers-know.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@calcom/atoms": minor
3+
---
4+
5+
This PR adds a new atom called `CalendarView` which is a read only calendar view component for a user.

packages/features/bookings/Booker/components/Header.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
55
import dayjs from "@calcom/dayjs";
66
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
77
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
8+
import { useInitializeWeekStart } from "@calcom/features/bookings/Booker/components/hooks/useInitializeWeekStart";
89
import { WEBAPP_URL } from "@calcom/lib/constants";
910
import { useLocale } from "@calcom/lib/hooks/useLocale";
1011
import { BookerLayouts } from "@calcom/prisma/zod-utils";
@@ -25,6 +26,7 @@ export function Header({
2526
eventSlug,
2627
isMyLink,
2728
renderOverlay,
29+
isCalendarView,
2830
}: {
2931
extraDays: number;
3032
isMobile: boolean;
@@ -33,21 +35,25 @@ export function Header({
3335
eventSlug: string;
3436
isMyLink: boolean;
3537
renderOverlay?: () => JSX.Element | null;
38+
isCalendarView?: boolean;
3639
}) {
3740
const { t, i18n } = useLocale();
3841
const isEmbed = useIsEmbed();
42+
const isPlatform = useIsPlatform();
3943
const [layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow);
4044
const selectedDateString = useBookerStoreContext((state) => state.selectedDate);
4145
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
4246
const addToSelectedDate = useBookerStoreContext((state) => state.addToSelectedDate);
43-
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
47+
const isMonthView = isCalendarView !== undefined ? !isCalendarView : layout === BookerLayouts.MONTH_VIEW;
4448
const today = dayjs();
4549
const selectedDate = selectedDateString ? dayjs(selectedDateString) : today;
4650
const selectedDateMin3DaysDifference = useMemo(() => {
4751
const diff = today.diff(selectedDate, "days");
4852
return diff > 3 || diff < -3;
4953
}, [today, selectedDate]);
5054

55+
useInitializeWeekStart(isPlatform, isCalendarView ?? false);
56+
5157
const onLayoutToggle = useCallback(
5258
(newLayout: string) => {
5359
if (layout === newLayout || !newLayout) return;
@@ -130,7 +136,10 @@ export function Header({
130136
<Button
131137
className="capitalize ltr:ml-2 rtl:mr-2"
132138
color="secondary"
133-
onClick={() => setSelectedDate({ date: today.format("YYYY-MM-DD") })}>
139+
onClick={() => {
140+
const selectedDate = (isCalendarView ? today.startOf("week") : today).format("YYYY-MM-DD");
141+
setSelectedDate({ date: selectedDate });
142+
}}>
134143
{t("today")}
135144
</Button>
136145
)}

packages/features/bookings/Booker/components/LargeCalendar.tsx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { useMemo, useEffect } from "react";
22

33
import dayjs from "@calcom/dayjs";
44
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
5+
import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/components/hooks/useAvailableTimeSlots";
56
import type { BookerEvent } from "@calcom/features/bookings/types";
67
import { Calendar } from "@calcom/features/calendars/weeklyview";
78
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
8-
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
99
import { localStorage } from "@calcom/lib/webstorage";
1010

1111
import type { useScheduleForEventReturnType } from "../utils/event";
@@ -34,24 +34,7 @@ export const LargeCalendar = ({
3434

3535
const eventDuration = selectedEventDuration || event?.data?.length || 30;
3636

37-
const availableSlots = useMemo(() => {
38-
const availableTimeslots: CalendarAvailableTimeslots = {};
39-
if (!schedule) return availableTimeslots;
40-
if (!schedule.slots) return availableTimeslots;
41-
42-
for (const day in schedule.slots) {
43-
availableTimeslots[day] = schedule.slots[day].map((slot) => {
44-
const { time, ...rest } = slot;
45-
return {
46-
start: dayjs(time).toDate(),
47-
end: dayjs(time).add(eventDuration, "minutes").toDate(),
48-
...rest,
49-
};
50-
});
51-
}
52-
53-
return availableTimeslots;
54-
}, [schedule, eventDuration]);
37+
const availableSlots = useAvailableTimeSlots({ schedule, eventDuration });
5538

5639
const startDate = selectedDate ? dayjs(selectedDate).toDate() : dayjs().toDate();
5740
const endDate = dayjs(startDate)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useMemo } from "react";
2+
3+
import dayjs from "@calcom/dayjs";
4+
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
5+
import type { IGetAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util";
6+
7+
interface UseAvailableTimeSlotsProps {
8+
eventDuration: number;
9+
schedule?: IGetAvailableSlots;
10+
}
11+
12+
export const useAvailableTimeSlots = ({ schedule, eventDuration }: UseAvailableTimeSlotsProps) => {
13+
return useMemo(() => {
14+
const availableTimeslots: CalendarAvailableTimeslots = {};
15+
if (!schedule || !schedule.slots) return availableTimeslots;
16+
17+
for (const day in schedule.slots) {
18+
availableTimeslots[day] = schedule.slots[day].map((slot) => {
19+
const { time, ...rest } = slot;
20+
return {
21+
start: dayjs(time).toDate(),
22+
end: dayjs(time).add(eventDuration, "minutes").toDate(),
23+
...rest,
24+
};
25+
});
26+
}
27+
28+
return availableTimeslots;
29+
}, [schedule, eventDuration]);
30+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useEffect } from "react";
2+
3+
import dayjs from "@calcom/dayjs";
4+
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
5+
6+
export const useInitializeWeekStart = (isPlatform: boolean, isCalendarView: boolean) => {
7+
const today = dayjs();
8+
const weekStart = today.startOf("week").format("YYYY-MM-DD");
9+
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
10+
11+
useEffect(() => {
12+
if (isPlatform && isCalendarView) {
13+
setSelectedDate({ date: weekStart, omitUpdatingParams: true });
14+
}
15+
// eslint-disable-next-line react-hooks/exhaustive-deps
16+
}, []);
17+
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useMemo, useEffect } from "react";
2+
3+
import dayjs from "@calcom/dayjs";
4+
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
5+
import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/components/hooks/useAvailableTimeSlots";
6+
import type { BookerEvent } from "@calcom/features/bookings/types";
7+
import { Calendar } from "@calcom/features/calendars/weeklyview";
8+
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
9+
import { localStorage } from "@calcom/lib/webstorage";
10+
11+
import { useOverlayCalendarStore } from "../bookings/Booker/components/OverlayCalendar/store";
12+
import type { useScheduleForEventReturnType } from "../bookings/Booker/utils/event";
13+
import { getQueryParam } from "../bookings/Booker/utils/query-param";
14+
15+
export const LargeCalendar = ({
16+
extraDays,
17+
schedule,
18+
isLoading,
19+
event,
20+
}: {
21+
extraDays: number;
22+
schedule?: useScheduleForEventReturnType["data"];
23+
isLoading: boolean;
24+
event: {
25+
data?: Pick<BookerEvent, "length"> | null;
26+
};
27+
}) => {
28+
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
29+
const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration);
30+
const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates);
31+
const displayOverlay =
32+
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");
33+
34+
const eventDuration = selectedEventDuration || event?.data?.length || 30;
35+
36+
const availableSlots = useAvailableTimeSlots({ schedule, eventDuration });
37+
38+
const startDate = selectedDate ? dayjs(selectedDate).toDate() : dayjs().toDate();
39+
const endDate = dayjs(startDate)
40+
.add(extraDays - 1, "day")
41+
.toDate();
42+
43+
// HACK: force rerender when overlay events change
44+
// Sine we dont use react router here we need to force rerender (ATOM SUPPORT)
45+
// eslint-disable-next-line @typescript-eslint/no-empty-function
46+
useEffect(() => {}, [displayOverlay]);
47+
48+
const overlayEventsForDate = useMemo(() => {
49+
if (!overlayEvents || !displayOverlay) return [];
50+
return overlayEvents.map((event, id) => {
51+
return {
52+
id,
53+
start: dayjs(event.start).toDate(),
54+
end: dayjs(event.end).toDate(),
55+
title: "Busy",
56+
options: {
57+
status: "ACCEPTED",
58+
},
59+
} as CalendarEvent;
60+
});
61+
}, [overlayEvents, displayOverlay]);
62+
63+
return (
64+
<div className="h-full [--calendar-dates-sticky-offset:66px]">
65+
<Calendar
66+
isPending={isLoading}
67+
availableTimeslots={availableSlots}
68+
startHour={0}
69+
endHour={23}
70+
events={overlayEventsForDate}
71+
startDate={startDate}
72+
endDate={endDate}
73+
gridCellsPerHour={60 / eventDuration}
74+
hoverEventDuration={eventDuration}
75+
hideHeader
76+
/>
77+
</div>
78+
);
79+
};

packages/features/troubleshooter/components/LargeCalendar.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { useSession } from "next-auth/react";
22
import { useMemo } from "react";
33

44
import dayjs from "@calcom/dayjs";
5+
import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/components/hooks/useAvailableTimeSlots";
56
import { Calendar } from "@calcom/features/calendars/weeklyview";
6-
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
77
import { BookingStatus } from "@calcom/prisma/enums";
88
import { trpc } from "@calcom/trpc";
99

@@ -48,22 +48,7 @@ export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
4848
.add(extraDays - 1, "day")
4949
.toDate();
5050

51-
const availableSlots = useMemo(() => {
52-
const availableTimeslots: CalendarAvailableTimeslots = {};
53-
if (!schedule) return availableTimeslots;
54-
if (!schedule?.slots) return availableTimeslots;
55-
56-
for (const day in schedule.slots) {
57-
availableTimeslots[day] = schedule.slots[day].map((slot) => ({
58-
start: dayjs(slot.time).toDate(),
59-
end: dayjs(slot.time)
60-
.add(event?.duration ?? 30, "minutes")
61-
.toDate(),
62-
}));
63-
}
64-
65-
return availableTimeslots;
66-
}, [schedule, event]);
51+
const availableSlots = useAvailableTimeSlots({ schedule, eventDuration: event?.duration ?? 30 });
6752

6853
const events = useMemo(() => {
6954
if (!busyEvents?.busy) return [];

packages/platform/atoms/booker/BookerPlatformWrapper.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable react-hooks/exhaustive-deps */
12
import { useQueryClient } from "@tanstack/react-query";
23
// eslint-disable-next-line no-restricted-imports
34
import debounce from "lodash/debounce";
@@ -594,7 +595,7 @@ export const BookerPlatformWrapper = (
594595
);
595596
};
596597

597-
function formatUsername(username: string | string[]): string {
598+
export function formatUsername(username: string | string[]): string {
598599
if (typeof username === "string") {
599600
return username;
600601
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { CalendarViewPlatformWrapper } from "./wrappers/CalendarViewPlatformWrapper";

0 commit comments

Comments
 (0)