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 (
+ {showTimezone && timezone && (
+ {getTimezoneDisplay()}
)}
- />
+
{days.map((day) => {
const isToday = dayjs().isSame(day, "day");
return (
diff --git a/packages/features/calendars/weeklyview/state/store.ts b/packages/features/calendars/weeklyview/state/store.ts
index 5776328b58..57bc2b0fb3 100644
--- a/packages/features/calendars/weeklyview/state/store.ts
+++ b/packages/features/calendars/weeklyview/state/store.ts
@@ -25,6 +25,7 @@ const defaultState: CalendarComponentProps = {
showBackgroundPattern: true,
showBorder: true,
borderColor: "default",
+ showTimezone: false,
};
export function createCalendarStore(initial?: Partial
): StoreApi {
diff --git a/packages/features/calendars/weeklyview/types/state.ts b/packages/features/calendars/weeklyview/types/state.ts
index 00ebb33fd1..edd5f33d19 100644
--- a/packages/features/calendars/weeklyview/types/state.ts
+++ b/packages/features/calendars/weeklyview/types/state.ts
@@ -147,6 +147,11 @@ export type CalendarState = {
* @default "default"
*/
borderColor?: BorderColor;
+ /**
+ * Show the timezone in the empty space next to the date headers
+ * @default false
+ */
+ showTimezone?: boolean;
};
export type CalendarComponentProps = CalendarPublicActions & CalendarState & { isPending?: boolean };
diff --git a/packages/features/shell/DynamicModals.tsx b/packages/features/shell/DynamicModals.tsx
index a80f9c7383..8ad9aa6e3f 100644
--- a/packages/features/shell/DynamicModals.tsx
+++ b/packages/features/shell/DynamicModals.tsx
@@ -2,10 +2,12 @@
import { WelcomeToOrganizationsModal } from "@calcom/features/ee/organizations/components/WelcomeToOrganizationsModal";
+import { WelcomeToCalcomModal } from "./components/WelcomeToCalcomModal";
+
/**
* Container for all query-param driven modals that should appear globally across the app.
* This keeps the Shell component clean and provides a centralized place for dynamic modals.
- *
+ *
* We can probably also use this for thinks like the T&C and Privacy Policy modals. That @marketing are discussing
*
* To add a new modal:
@@ -16,6 +18,7 @@ export function DynamicModals() {
return (
<>
+
{/* Add more query-param driven modals here */}
>
);
diff --git a/packages/features/shell/components/WelcomeToCalcomModal.tsx b/packages/features/shell/components/WelcomeToCalcomModal.tsx
new file mode 100644
index 0000000000..5e2bae6bd8
--- /dev/null
+++ b/packages/features/shell/components/WelcomeToCalcomModal.tsx
@@ -0,0 +1,109 @@
+"use client";
+
+import { APP_NAME } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button } from "@calcom/ui/components/button";
+import { Dialog, DialogContent } from "@calcom/ui/components/dialog";
+import { Icon } from "@calcom/ui/components/icon";
+import { Logo } from "@calcom/ui/components/logo";
+
+import { useWelcomeToCalcomModal } from "../hooks/useWelcomeToCalcomModal";
+
+const features = ["1_user", "unlimited_calendars", "accept_payments_via_stripe"];
+
+export function WelcomeToCalcomModal() {
+ const { t } = useLocale();
+ const { isOpen, closeModal } = useWelcomeToCalcomModal();
+
+ const LARGE = { outer: 48, icon: 24 };
+ const RINGS = [60, 95, 130]; // Ring radii in px
+ const RING_STROKE = 1;
+
+ return (
+
+ );
+}
diff --git a/packages/features/shell/hooks/useWelcomeToCalcomModal.ts b/packages/features/shell/hooks/useWelcomeToCalcomModal.ts
index 1a6faaf78e..cf3b9afef0 100644
--- a/packages/features/shell/hooks/useWelcomeToCalcomModal.ts
+++ b/packages/features/shell/hooks/useWelcomeToCalcomModal.ts
@@ -1,6 +1,8 @@
import { parseAsBoolean, useQueryState } from "nuqs";
import { useEffect, useState } from "react";
+import { sessionStorage } from "@calcom/lib/webstorage";
+
const STORAGE_KEY = "showWelcomeToCalcomModal";
export function useWelcomeToCalcomModal() {
@@ -12,14 +14,12 @@ export function useWelcomeToCalcomModal() {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
- // Check query param first
if (welcomeToCalcomModal) {
setIsOpen(true);
return;
}
- // Check sessionStorage as fallback (for cases where we redirect through personal onboarding)
- if (typeof window !== "undefined" && sessionStorage.getItem(STORAGE_KEY) === "true") {
+ if (sessionStorage.getItem(STORAGE_KEY) === "true") {
setIsOpen(true);
}
}, [welcomeToCalcomModal]);
@@ -29,9 +29,7 @@ export function useWelcomeToCalcomModal() {
// Remove the query param from URL
setWelcomeToCalcomModal(null);
// Also clear sessionStorage
- if (typeof window !== "undefined") {
- sessionStorage.removeItem(STORAGE_KEY);
- }
+ sessionStorage.removeItem(STORAGE_KEY);
};
return {
@@ -40,12 +38,6 @@ export function useWelcomeToCalcomModal() {
};
}
-/**
- * Helper function to set the flag that triggers the welcome modal.
- * Use this before redirecting to ensure the modal shows after navigation.
- */
export function setShowWelcomeToCalcomModalFlag() {
- if (typeof window !== "undefined") {
- sessionStorage.setItem(STORAGE_KEY, "true");
- }
+ sessionStorage.setItem(STORAGE_KEY, "true");
}