Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b34864f
fix: refactor
Ryukemeister Sep 15, 2025
962a984
add `isMonthViewProp` to header
Ryukemeister Sep 15, 2025
31cfe82
feat: init v1 for calendar view atom
Ryukemeister Sep 15, 2025
0029a58
Merge branch 'main' into calendar-view-atom-v1
Ryukemeister Sep 15, 2025
f522e3d
Merge branch 'main' into calendar-view-atom-v1
Ryukemeister Sep 16, 2025
fcec72f
Merge branch 'main' into calendar-view-atom-v1
Ryukemeister Sep 16, 2025
5a8e0bf
Merge branch 'main' into calendar-view-atom-v1
Ryukemeister Sep 16, 2025
6cd88ea
test breaking toggle buttons
Ryukemeister Sep 17, 2025
1400eac
fix: make sure week start is always sunday for calendar view atom
Ryukemeister Sep 17, 2025
6127bbe
fixup
Ryukemeister Sep 17, 2025
7412a63
Merge branch 'main' into calendar-view-atom-v1
Ryukemeister Sep 17, 2025
cff8138
fix: remove extra comments
Ryukemeister Sep 17, 2025
6552d1a
fix: add calendar view page in examples app
Ryukemeister Sep 17, 2025
7550a4b
Merge branch 'main' into calendar-view-atom-v1
Ryukemeister Sep 17, 2025
df4b2d6
chore: add changesets
Ryukemeister Sep 18, 2025
575f129
fix: coderabbit feedback
Ryukemeister Sep 18, 2025
f50d616
Merge branch 'main' into calendar-view-atom-v1
Ryukemeister Sep 18, 2025
c07a72b
fixup
Ryukemeister Sep 19, 2025
adfb52d
Merge branch 'main' into calendar-view-atom-v1
Ryukemeister Sep 19, 2025
6dc26b4
Merge branch 'main' into calendar-view-atom-v1
Ryukemeister Sep 22, 2025
d65d2bb
Merge branch 'main' into calendar-view-atom-v1
Ryukemeister Sep 22, 2025
c940118
fix: import path
Ryukemeister Sep 22, 2025
41d54d1
fix: `customReplyToEmail` breaking
Ryukemeister Sep 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fuzzy-beers-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@calcom/atoms": minor
---

This PR adds a new atom called `CalendarView` which is a read only calendar view component for a user.
13 changes: 11 additions & 2 deletions packages/features/bookings/Booker/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import dayjs from "@calcom/dayjs";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useInitializeWeekStart } from "@calcom/features/bookings/Booker/components/hooks/useInitializeWeekStart";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
Expand All @@ -25,6 +26,7 @@ export function Header({
eventSlug,
isMyLink,
renderOverlay,
isCalendarView,
}: {
extraDays: number;
isMobile: boolean;
Expand All @@ -33,21 +35,25 @@ export function Header({
eventSlug: string;
isMyLink: boolean;
renderOverlay?: () => JSX.Element | null;
isCalendarView?: boolean;
}) {
const { t, i18n } = useLocale();
const isEmbed = useIsEmbed();
const isPlatform = useIsPlatform();
const [layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow);
const selectedDateString = useBookerStoreContext((state) => state.selectedDate);
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
const addToSelectedDate = useBookerStoreContext((state) => state.addToSelectedDate);
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
const isMonthView = isCalendarView !== undefined ? !isCalendarView : layout === BookerLayouts.MONTH_VIEW;
const today = dayjs();
const selectedDate = selectedDateString ? dayjs(selectedDateString) : today;
const selectedDateMin3DaysDifference = useMemo(() => {
const diff = today.diff(selectedDate, "days");
return diff > 3 || diff < -3;
}, [today, selectedDate]);

useInitializeWeekStart(isPlatform, isCalendarView ?? false);

const onLayoutToggle = useCallback(
(newLayout: string) => {
if (layout === newLayout || !newLayout) return;
Expand Down Expand Up @@ -130,7 +136,10 @@ export function Header({
<Button
className="capitalize ltr:ml-2 rtl:mr-2"
color="secondary"
onClick={() => setSelectedDate({ date: today.format("YYYY-MM-DD") })}>
onClick={() => {
const selectedDate = (isCalendarView ? today.startOf("week") : today).format("YYYY-MM-DD");
setSelectedDate({ date: selectedDate });
}}>
{t("today")}
</Button>
)}
Expand Down
21 changes: 2 additions & 19 deletions packages/features/bookings/Booker/components/LargeCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { useMemo, useEffect } from "react";

import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/components/hooks/useAvailableTimeSlots";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { Calendar } from "@calcom/features/calendars/weeklyview";
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
import { localStorage } from "@calcom/lib/webstorage";

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

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

const availableSlots = useMemo(() => {
const availableTimeslots: CalendarAvailableTimeslots = {};
if (!schedule) return availableTimeslots;
if (!schedule.slots) return availableTimeslots;

for (const day in schedule.slots) {
availableTimeslots[day] = schedule.slots[day].map((slot) => {
const { time, ...rest } = slot;
return {
start: dayjs(time).toDate(),
end: dayjs(time).add(eventDuration, "minutes").toDate(),
...rest,
};
});
}

return availableTimeslots;
}, [schedule, eventDuration]);
const availableSlots = useAvailableTimeSlots({ schedule, eventDuration });

const startDate = selectedDate ? dayjs(selectedDate).toDate() : dayjs().toDate();
const endDate = dayjs(startDate)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useMemo } from "react";

import dayjs from "@calcom/dayjs";
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
import type { IGetAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util";

interface UseAvailableTimeSlotsProps {
eventDuration: number;
schedule?: IGetAvailableSlots;
}

export const useAvailableTimeSlots = ({ schedule, eventDuration }: UseAvailableTimeSlotsProps) => {
return useMemo(() => {
const availableTimeslots: CalendarAvailableTimeslots = {};
if (!schedule || !schedule.slots) return availableTimeslots;

for (const day in schedule.slots) {
availableTimeslots[day] = schedule.slots[day].map((slot) => {
const { time, ...rest } = slot;
return {
start: dayjs(time).toDate(),
end: dayjs(time).add(eventDuration, "minutes").toDate(),
...rest,
};
});
}

return availableTimeslots;
}, [schedule, eventDuration]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect } from "react";

import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";

export const useInitializeWeekStart = (isPlatform: boolean, isCalendarView: boolean) => {
const today = dayjs();
const weekStart = today.startOf("week").format("YYYY-MM-DD");
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);

useEffect(() => {
if (isPlatform && isCalendarView) {
setSelectedDate({ date: weekStart, omitUpdatingParams: true });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};
79 changes: 79 additions & 0 deletions packages/features/calendar-view/LargeCalendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useMemo, useEffect } from "react";

import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/components/hooks/useAvailableTimeSlots";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { Calendar } from "@calcom/features/calendars/weeklyview";
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
import { localStorage } from "@calcom/lib/webstorage";

import { useOverlayCalendarStore } from "../bookings/Booker/components/OverlayCalendar/store";
import type { useScheduleForEventReturnType } from "../bookings/Booker/utils/event";
import { getQueryParam } from "../bookings/Booker/utils/query-param";

export const LargeCalendar = ({
extraDays,
schedule,
isLoading,
event,
}: {
extraDays: number;
schedule?: useScheduleForEventReturnType["data"];
isLoading: boolean;
event: {
data?: Pick<BookerEvent, "length"> | null;
};
}) => {
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration);
const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates);
const displayOverlay =
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");

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

const availableSlots = useAvailableTimeSlots({ schedule, eventDuration });

const startDate = selectedDate ? dayjs(selectedDate).toDate() : dayjs().toDate();
const endDate = dayjs(startDate)
.add(extraDays - 1, "day")
.toDate();

// HACK: force rerender when overlay events change
// Sine we dont use react router here we need to force rerender (ATOM SUPPORT)
// eslint-disable-next-line @typescript-eslint/no-empty-function
useEffect(() => {}, [displayOverlay]);

const overlayEventsForDate = useMemo(() => {
if (!overlayEvents || !displayOverlay) return [];
return overlayEvents.map((event, id) => {
return {
id,
start: dayjs(event.start).toDate(),
end: dayjs(event.end).toDate(),
title: "Busy",
options: {
status: "ACCEPTED",
},
} as CalendarEvent;
});
}, [overlayEvents, displayOverlay]);

return (
<div className="h-full [--calendar-dates-sticky-offset:66px]">
<Calendar
isPending={isLoading}
availableTimeslots={availableSlots}
startHour={0}
endHour={23}
events={overlayEventsForDate}
startDate={startDate}
endDate={endDate}
gridCellsPerHour={60 / eventDuration}
hoverEventDuration={eventDuration}
hideHeader
/>
</div>
);
};
19 changes: 2 additions & 17 deletions packages/features/troubleshooter/components/LargeCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { useSession } from "next-auth/react";
import { useMemo } from "react";

import dayjs from "@calcom/dayjs";
import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/components/hooks/useAvailableTimeSlots";
import { Calendar } from "@calcom/features/calendars/weeklyview";
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
import { BookingStatus } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc";

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

const availableSlots = useMemo(() => {
const availableTimeslots: CalendarAvailableTimeslots = {};
if (!schedule) return availableTimeslots;
if (!schedule?.slots) return availableTimeslots;

for (const day in schedule.slots) {
availableTimeslots[day] = schedule.slots[day].map((slot) => ({
start: dayjs(slot.time).toDate(),
end: dayjs(slot.time)
.add(event?.duration ?? 30, "minutes")
.toDate(),
}));
}

return availableTimeslots;
}, [schedule, event]);
const availableSlots = useAvailableTimeSlots({ schedule, eventDuration: event?.duration ?? 30 });

const events = useMemo(() => {
if (!busyEvents?.busy) return [];
Expand Down
3 changes: 2 additions & 1 deletion packages/platform/atoms/booker/BookerPlatformWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Remove file‑wide exhaustive‑deps disable; scope it to specific hooks.

A blanket disable can hide stale-closure bugs across this hook-heavy file. Prefer fixing deps or narrowly disabling per‑hook (you already do this in a few places).

Apply:

-/* eslint-disable react-hooks/exhaustive-deps */

Run to audit where we still rely on blanket vs scoped disables:


🏁 Script executed:

#!/bin/bash
# Hooks and nearby eslint disables for this file
rg -n -C1 -e 'useEffect\(|useMemo\(|useCallback\(' packages/platform/atoms/booker/BookerPlatformWrapper.tsx
rg -n 'eslint-disable-next-line react-hooks/exhaustive-deps' packages/platform/atoms/booker/BookerPlatformWrapper.tsx

Length of output: 2023


Remove file-wide eslint-disable react-hooks/exhaustive-deps. Scoped disables already cover the five specific hooks at lines 153, 158, 231, 468, and 490—drop the blanket disable at the top.

-/* eslint-disable react-hooks/exhaustive-deps */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/* eslint-disable react-hooks/exhaustive-deps */
🤖 Prompt for AI Agents
In packages/platform/atoms/booker/BookerPlatformWrapper.tsx around line 1,
remove the file-wide comment "/* eslint-disable react-hooks/exhaustive-deps */"
and leave the existing scoped eslint-disable comments at lines 153, 158, 231,
468, and 490 intact; after removing the blanket disable, run ESLint (or your IDE
linter) to confirm there are no new react-hooks/exhaustive-deps warnings and
address any remaining specific hook warnings by scoping disables only to the
individual lines where necessary.

import { useQueryClient } from "@tanstack/react-query";
// eslint-disable-next-line no-restricted-imports
import debounce from "lodash/debounce";
Expand Down Expand Up @@ -594,7 +595,7 @@ export const BookerPlatformWrapper = (
);
};

function formatUsername(username: string | string[]): string {
export function formatUsername(username: string | string[]): string {
if (typeof username === "string") {
return username;
}
Expand Down
1 change: 1 addition & 0 deletions packages/platform/atoms/calendar-view/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CalendarViewPlatformWrapper } from "./wrappers/CalendarViewPlatformWrapper";
Loading
Loading