From dc3742d32247cc26e8639bf0a017ffe71f3ceb34 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:47:22 +0000 Subject: [PATCH] chore: Add calendar weekly view enhancements and welcome modal feature (#24948) ## What does this PR do? - Adds a welcome modal for new Cal.com users - Implements timezone display in the weekly calendar view - Creates a hook for fetching onboarding calendar events ## Visual Demo (For contributors especially) #### Image Demo:   ## Mandatory Tasks - [x] I have self-reviewed the code - [x] I have updated the developer docs in /docs - [x] I confirm automated tests are in place that prove my fix is effective or that my feature works. ## How should this be tested? 1. **Welcome Modal:** - Create a new user account - Verify the welcome modal appears with correct content - Test the "Continue" button closes the modal - Check that the modal can be triggered via URL parameter `?welcomeToCalcomModal=true` 2. **Timezone Display:** - Go to the weekly calendar view - Verify the timezone is displayed correctly when `showTimezone` is enabled - Test with different timezones to ensure proper formatting 3. **Onboarding Calendar Events:** - Test the hook by connecting a calendar during onboarding - Verify events are fetched and displayed correctly - Check that events refresh when new calendars are connected ## Checklist - I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md) - My code follows the style guidelines of this project - I have commented my code, particularly in hard-to-understand areas - I have checked if my changes generate no new warnings --- .../hooks/useOnboardingCalendarEvents.ts | 79 +++++++++++++ .../components/DateValues/index.tsx | 37 +++++- .../calendars/weeklyview/state/store.ts | 1 + .../calendars/weeklyview/types/state.ts | 5 + packages/features/shell/DynamicModals.tsx | 5 +- .../shell/components/WelcomeToCalcomModal.tsx | 109 ++++++++++++++++++ .../shell/hooks/useWelcomeToCalcomModal.ts | 18 +-- 7 files changed, 238 insertions(+), 16 deletions(-) create mode 100644 apps/web/modules/onboarding/hooks/useOnboardingCalendarEvents.ts create mode 100644 packages/features/shell/components/WelcomeToCalcomModal.tsx diff --git a/apps/web/modules/onboarding/hooks/useOnboardingCalendarEvents.ts b/apps/web/modules/onboarding/hooks/useOnboardingCalendarEvents.ts new file mode 100644 index 0000000000..fc3a9f4804 --- /dev/null +++ b/apps/web/modules/onboarding/hooks/useOnboardingCalendarEvents.ts @@ -0,0 +1,79 @@ +import { useSession } from "next-auth/react"; +import { useMemo, useEffect } from "react"; + +import dayjs from "@calcom/dayjs"; +import type { GetUserAvailabilityResult } from "@calcom/features/availability/lib/getUserAvailability"; +import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events"; +import { BookingStatus } from "@calcom/prisma/enums"; +import { trpc } from "@calcom/trpc/react"; + +type UseOnboardingCalendarEventsProps = { + startDate: Date; + endDate: Date; +}; + +const emptyAvailabilityData = { + busy: [], + timeZone: "", + dateRanges: [], + oooExcludedDateRanges: [], + workingHours: [], + dateOverrides: [], + currentSeats: null, + datesOutOfOffice: {}, +} as GetUserAvailabilityResult; + +export const useOnboardingCalendarEvents = ({ startDate, endDate }: UseOnboardingCalendarEventsProps) => { + const { data: session } = useSession(); + const utils = trpc.useUtils(); + + // Watch for calendar installations to refetch events + const { data: connectedCalendars } = trpc.viewer.calendars.connectedCalendars.useQuery(undefined, { + refetchInterval: 5000, // Poll every 5 seconds to detect new calendar installations + }); + + const { data: busyEvents } = trpc.viewer.availability.user.useQuery( + { + username: session?.user?.username || "", + dateFrom: dayjs(startDate).startOf("day").utc().format(), + dateTo: dayjs(endDate).endOf("day").utc().format(), + withSource: true, + }, + { + enabled: !!session?.user?.username, + // Don't show loading state - return empty array immediately + placeholderData: emptyAvailabilityData, + } + ); + + // Refetch availability when calendars change + useEffect(() => { + if (connectedCalendars?.connectedCalendars) { + utils.viewer.availability.user.invalidate(); + } + }, [connectedCalendars?.connectedCalendars?.length, utils.viewer.availability.user]); + + // Format events similar to Troubleshooter + // Always return an array, never undefined + const events = useMemo((): CalendarEvent[] => { + if (!busyEvents?.busy) return []; + + return busyEvents.busy.map((event, idx) => { + return { + id: idx, + title: event.title ?? `Busy`, + start: new Date(event.start), + end: new Date(event.end), + options: { + color: event.source ? undefined : undefined, + status: BookingStatus.ACCEPTED, + }, + }; + }); + }, [busyEvents]); + + return { + events, + isLoading: false, // Never show loading state + }; +}; diff --git a/packages/features/calendars/weeklyview/components/DateValues/index.tsx b/packages/features/calendars/weeklyview/components/DateValues/index.tsx index 77e967109a..e7e2c6e712 100644 --- a/packages/features/calendars/weeklyview/components/DateValues/index.tsx +++ b/packages/features/calendars/weeklyview/components/DateValues/index.tsx @@ -4,6 +4,7 @@ import dayjs from "@calcom/dayjs"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import classNames from "@calcom/ui/classNames"; +import { useCalendarStore } from "../../state/store"; import type { BorderColor } from "../../types/common"; type Props = { @@ -15,9 +16,38 @@ type Props = { export function DateValues({ showBorder, borderColor, days, containerNavRef }: Props) { const { i18n } = useLocale(); + const timezone = useCalendarStore((state) => state.timezone); + const showTimezone = useCalendarStore((state) => state.showTimezone ?? false); + const formatDate = (date: dayjs.Dayjs): string => { return new Intl.DateTimeFormat(i18n.language, { weekday: "short" }).format(date.toDate()); }; + + const getTimezoneDisplay = () => { + if (!showTimezone || !timezone) return null; + try { + const timeRaw = dayjs().tz(timezone); + const utcOffsetInMinutes = timeRaw.utcOffset(); + + // Convert offset to decimal hours + const offsetInHours = Math.abs(utcOffsetInMinutes / 60); + const sign = utcOffsetInMinutes < 0 ? "-" : "+"; + + // If offset is 0, just return "GMT" + if (utcOffsetInMinutes === 0) { + return "GMT"; + } + + // Format as decimal (e.g., 1.5 for 1:30, 1 for 1:00) + const offsetFormatted = `${sign}${offsetInHours}`; + + return `GMT ${offsetFormatted}`; + } catch { + // Fallback to showing the timezone name if formatting fails + return timezone.split("/").pop()?.replace(/_/g, " ") || timezone; + } + }; + return (