diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx index 888e156253..5b2dde9d8f 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx @@ -6,10 +6,10 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Badge, Dialog, DialogContent } from "@calcom/ui"; import { getDurationFormatted } from "../../../components/event-meta/Duration"; -import { useTimePreferences } from "../../../lib"; import { useBookerStore } from "../../store"; import { FromTime } from "../../utils/dates"; import { useEvent } from "../../utils/event"; +import { useBookerTime } from "../hooks/useBookerTime"; const BookEventFormWrapper = ({ children, onCancel }: { onCancel: () => void; children: ReactNode }) => { const { data } = useEvent(); @@ -43,7 +43,7 @@ export const BookEventFormWrapperComponent = ({ const { i18n, t } = useLocale(); const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot); const selectedDuration = useBookerStore((state) => state.selectedDuration); - const { timeFormat, timezone } = useTimePreferences(); + const { timeFormat, timezone } = useBookerTime(); if (!selectedTimeslot) { return null; } diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx index 8ff7bf08c6..601317a104 100644 --- a/packages/features/bookings/Booker/components/EventMeta.tsx +++ b/packages/features/bookings/Booker/components/EventMeta.tsx @@ -19,6 +19,7 @@ import i18nConfigration from "../../../../../i18n.json"; import { fadeInUp } from "../config"; import { useBookerStore } from "../store"; import { FromToTime } from "../utils/dates"; +import { useBookerTime } from "./hooks/useBookerTime"; const WebTimezoneSelect = dynamic( () => import("@calcom/ui/components/form/timezone-select/TimezoneSelect").then((mod) => mod.TimezoneSelect), @@ -80,7 +81,9 @@ export const EventMeta = ({ }; locale?: string | null; }) => { - const { setTimezone, timeFormat, timezone } = useTimePreferences(); + const { timeFormat, timezone } = useBookerTime(); + const [setTimezone] = useTimePreferences((state) => [state.setTimezone]); + const [setBookerStoreTimezone] = useBookerStore((state) => [state.setTimezone], shallow); const selectedDuration = useBookerStore((state) => state.selectedDuration); const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot); const bookerState = useBookerStore((state) => state.state); @@ -214,7 +217,10 @@ export const EventMeta = ({ container: () => "max-w-full", }} value={timezone} - onChange={(tz) => setTimezone(tz.value)} + onChange={({ value }) => { + setTimezone(value); + setBookerStoreTimezone(value); + }} isDisabled={event.lockTimeZoneToggleOnBookingPage} /> diff --git a/packages/features/bookings/Booker/components/hooks/useBookerTime.ts b/packages/features/bookings/Booker/components/hooks/useBookerTime.ts new file mode 100644 index 0000000000..c5b403b6e4 --- /dev/null +++ b/packages/features/bookings/Booker/components/hooks/useBookerTime.ts @@ -0,0 +1,22 @@ +import { shallow } from "zustand/shallow"; + +import { useBookerStore } from "@calcom/features/bookings/Booker/store"; + +import { useTimePreferences } from "../../../lib/timePreferences"; +import { getBookerTimezone } from "../../utils/getBookerTimezone"; + +export const useBookerTime = () => { + const [timezoneFromBookerStore] = useBookerStore((state) => [state.timezone], shallow); + const { timezone: timezoneFromTimePreferences, timeFormat } = useTimePreferences(); + const timezone = getBookerTimezone({ + storeTimezone: timezoneFromBookerStore, + bookerUserPreferredTimezone: timezoneFromTimePreferences, + }); + + return { + timezone, + timeFormat, + timezoneFromBookerStore, + timezoneFromTimePreferences, + }; +}; diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts index 5cc7165e80..8e0e8febb1 100644 --- a/packages/features/bookings/Booker/store.ts +++ b/packages/features/bookings/Booker/store.ts @@ -32,6 +32,7 @@ type StoreInitializeType = { durationConfig?: number[] | null; org?: string | null; isInstantMeeting?: boolean; + timezone?: string | null; teamMemberEmail?: string | null; crmOwnerRecordType?: string | null; crmAppSlug?: string | null; @@ -152,6 +153,9 @@ export type BookerStore = { org?: string | null; setOrg: (org: string | null | undefined) => void; + timezone: string | null; + setTimezone: (timezone: string | null) => void; + teamMemberEmail?: string | null; crmOwnerRecordType?: string | null; crmAppSlug?: string | null; @@ -246,6 +250,12 @@ export const useBookerStore = create((set, get) => ({ set({ seatedEventData }); updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null"); }, + // This is different from timeZone in timePreferencesStore, because timeZone in timePreferencesStore is the preferred timezone of the booker, + // it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link. + timezone: getQueryParam("cal.tz") ?? null, + setTimezone: (timezone: string | null) => { + set({ timezone }); + }, initialize: ({ username, eventSlug, @@ -260,6 +270,7 @@ export const useBookerStore = create((set, get) => ({ durationConfig, org, isInstantMeeting, + timezone = null, teamMemberEmail, crmOwnerRecordType, crmAppSlug, @@ -275,6 +286,7 @@ export const useBookerStore = create((set, get) => ({ get().bookingUid === bookingUid && get().bookingData?.responses.email === bookingData?.responses.email && get().layout === layout && + get().timezone === timezone && get().rescheduledBy === rescheduledBy && get().teamMemberEmail === teamMemberEmail && get().crmOwnerRecordType === crmOwnerRecordType && @@ -293,6 +305,7 @@ export const useBookerStore = create((set, get) => ({ layout: layout || BookerLayouts.MONTH_VIEW, isTeamEvent: isTeamEvent || false, durationConfig, + timezone, // Preselect today's date in week / column view, since they use this to show the week title. selectedDate: selectedDateInStore || @@ -388,6 +401,7 @@ export const useInitializeBookerStore = ({ durationConfig, org, isInstantMeeting, + timezone = null, teamMemberEmail, crmOwnerRecordType, crmAppSlug, @@ -408,6 +422,7 @@ export const useInitializeBookerStore = ({ verifiedEmail, durationConfig, isInstantMeeting, + timezone, teamMemberEmail, crmOwnerRecordType, crmAppSlug, @@ -427,6 +442,7 @@ export const useInitializeBookerStore = ({ verifiedEmail, durationConfig, isInstantMeeting, + timezone, teamMemberEmail, crmOwnerRecordType, crmAppSlug, diff --git a/packages/features/bookings/Booker/utils/event.ts b/packages/features/bookings/Booker/utils/event.ts index fd04b3eebe..b81a40487b 100644 --- a/packages/features/bookings/Booker/utils/event.ts +++ b/packages/features/bookings/Booker/utils/event.ts @@ -1,11 +1,10 @@ -import { usePathname } from "next/navigation"; import { shallow } from "zustand/shallow"; import { useSchedule } from "@calcom/features/schedules"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { trpc } from "@calcom/trpc/react"; -import { useTimePreferences } from "../../lib/timePreferences"; +import { useBookerTime } from "../components/hooks/useBookerTime"; import { useBookerStore } from "../store"; export type useEventReturnType = ReturnType; @@ -88,7 +87,7 @@ export const useScheduleForEvent = ({ fromRedirectOfNonOrgLink?: boolean; isTeamEvent?: boolean; } = {}) => { - const { timezone } = useTimePreferences(); + const { timezone } = useBookerTime(); const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStore( (state) => [state.username, state.eventSlug, state.month, state.selectedDuration], shallow @@ -97,8 +96,6 @@ export const useScheduleForEvent = ({ const searchParams = useCompatSearchParams(); const rescheduleUid = searchParams?.get("rescheduleUid"); - const pathname = usePathname(); - const schedule = useSchedule({ username: usernameFromStore ?? username, eventSlug: eventSlugFromStore ?? eventSlug, diff --git a/packages/features/bookings/Booker/utils/getBookerTimezone.ts b/packages/features/bookings/Booker/utils/getBookerTimezone.ts new file mode 100644 index 0000000000..489100e2c6 --- /dev/null +++ b/packages/features/bookings/Booker/utils/getBookerTimezone.ts @@ -0,0 +1,14 @@ +/** + * TODO: We could convert it into a hook and then we can access bookerStore and timePreferences stores directly here, avoiding the need to do it by the caller + */ +export const getBookerTimezone = ({ + storeTimezone, + bookerUserPreferredTimezone, +}: { + storeTimezone: string | null; + bookerUserPreferredTimezone: string; +}) => { + // BookerStore timezone is the one that is updated no matter what could be the reason of the update + // e.g. timezone configured through cal.tz query param is available there but not in the preferences as those are booker user's preferences + return storeTimezone ?? bookerUserPreferredTimezone; +}; diff --git a/packages/features/bookings/components/AvailableTimes.tsx b/packages/features/bookings/components/AvailableTimes.tsx index 1e0eca7507..33a690db23 100644 --- a/packages/features/bookings/components/AvailableTimes.tsx +++ b/packages/features/bookings/components/AvailableTimes.tsx @@ -15,9 +15,9 @@ import { localStorage } from "@calcom/lib/webstorage"; import type { IGetAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util"; import { Button, Icon, SkeletonText } from "@calcom/ui"; +import { useBookerTime } from "../Booker/components/hooks/useBookerTime"; import { useBookerStore } from "../Booker/store"; import { getQueryParam } from "../Booker/utils/query-param"; -import { useTimePreferences } from "../lib"; import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay"; import { SeatsAvailabilityText } from "./SeatsAvailabilityText"; @@ -64,8 +64,8 @@ const SlotItem = ({ const { t } = useLocale(); const overlayCalendarToggled = - getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault"); - const [timeFormat, timezone] = useTimePreferences((state) => [state.timeFormat, state.timezone]); + getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault"); + const { timeFormat, timezone } = useBookerTime(); const bookingData = useBookerStore((state) => state.bookingData); const layout = useBookerStore((state) => state.layout); const { data: eventData } = event; diff --git a/packages/features/bookings/components/event-meta/Occurences.tsx b/packages/features/bookings/components/event-meta/Occurences.tsx index 1a0c716007..5fec9cb5e4 100644 --- a/packages/features/bookings/components/event-meta/Occurences.tsx +++ b/packages/features/bookings/components/event-meta/Occurences.tsx @@ -8,7 +8,7 @@ import { getRecurringFreq } from "@calcom/lib/recurringStrings"; import { Tooltip, Alert } from "@calcom/ui"; import { Input } from "@calcom/ui"; -import { useTimePreferences } from "../../lib"; +import { useBookerTime } from "../../Booker/components/hooks/useBookerTime"; export const EventOccurences = ({ event }: { event: Pick }) => { const maxOccurences = event.recurringEvent?.count || null; @@ -23,7 +23,7 @@ export const EventOccurences = ({ event }: { event: Pick state.selectedTimeslot); const bookerState = useBookerStore((state) => state.state); - const { timezone, timeFormat } = useTimePreferences(); + const { timezone, timeFormat } = useBookerTime(); const [warning, setWarning] = useState(false); // Set initial value in booker store. useEffect(() => { diff --git a/packages/features/calendars/weeklyview/components/Calendar.tsx b/packages/features/calendars/weeklyview/components/Calendar.tsx index 52e9dc6e92..dc79bff41c 100644 --- a/packages/features/calendars/weeklyview/components/Calendar.tsx +++ b/packages/features/calendars/weeklyview/components/Calendar.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useMemo, useRef } from "react"; -import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences"; import { classNames } from "@calcom/lib"; +import { useBookerTime } from "../../../bookings/Booker/components/hooks/useBookerTime"; import { useCalendarStore } from "../state/store"; import "../styles/styles.css"; import type { CalendarComponentProps } from "../types/state"; @@ -23,7 +23,7 @@ export function Calendar(props: CalendarComponentProps) { const containerOffset = useRef(null); const schedulerGrid = useRef(null); const initialState = useCalendarStore((state) => state.initState); - const { timezone } = useTimePreferences(); + const { timezone } = useBookerTime(); const startDate = useCalendarStore((state) => state.startDate); const endDate = useCalendarStore((state) => state.endDate); diff --git a/packages/features/calendars/weeklyview/components/currentTime/index.tsx b/packages/features/calendars/weeklyview/components/currentTime/index.tsx index 41a28698d8..8276219e4b 100644 --- a/packages/features/calendars/weeklyview/components/currentTime/index.tsx +++ b/packages/features/calendars/weeklyview/components/currentTime/index.tsx @@ -1,8 +1,8 @@ import { useEffect, useState, useRef } from "react"; import dayjs from "@calcom/dayjs"; -import { useTimePreferences } from "@calcom/features/bookings/lib"; +import { useBookerTime } from "../../../../bookings/Booker/components/hooks/useBookerTime"; import { useCalendarStore } from "../../state/store"; function calculateMinutesFromStart(startHour: number, currentHour: number, currentMinute: number) { @@ -19,7 +19,7 @@ export function CurrentTime() { startHour: state.startHour || 0, endHour: state.endHour || 23, })); - const { timeFormat, timezone } = useTimePreferences(); + const { timeFormat, timezone } = useBookerTime(); useEffect(() => { // Set the container scroll position based on the current time. diff --git a/packages/features/calendars/weeklyview/components/event/Empty.tsx b/packages/features/calendars/weeklyview/components/event/Empty.tsx index cc1406ae4d..e121195fd9 100644 --- a/packages/features/calendars/weeklyview/components/event/Empty.tsx +++ b/packages/features/calendars/weeklyview/components/event/Empty.tsx @@ -3,10 +3,10 @@ import { shallow } from "zustand/shallow"; import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; -import { useTimePreferences } from "@calcom/features/bookings/lib"; import { classNames } from "@calcom/lib"; import { OutOfOfficeInSlots } from "../../../../bookings/Booker/components/OutOfOfficeInSlots"; +import { useBookerTime } from "../../../../bookings/Booker/components/hooks/useBookerTime"; import { useCalendarStore } from "../../state/store"; import type { CalendarAvailableTimeslots } from "../../types/state"; import type { GridCellToDateProps } from "../../utils"; @@ -39,7 +39,7 @@ type AvailableCellProps = { }; export function AvailableCellsForDay({ availableSlots, day, startHour }: AvailableCellProps) { - const { timezone } = useTimePreferences(); + const { timezone } = useBookerTime(); const date = dayjs(day); const dateFormatted = date.format("YYYY-MM-DD"); const slotsForToday = availableSlots && availableSlots[dateFormatted]; @@ -136,7 +136,7 @@ type CellProps = { }; function Cell({ isDisabled, topOffsetMinutes, timeSlot }: CellProps) { - const { timeFormat } = useTimePreferences(); + const { timeFormat } = useBookerTime(); const { onEmptyCellClick, hoverEventDuration } = useCalendarStore( (state) => ({ diff --git a/packages/features/embed/Embed.tsx b/packages/features/embed/Embed.tsx index f136c9e1af..136717b632 100644 --- a/packages/features/embed/Embed.tsx +++ b/packages/features/embed/Embed.tsx @@ -13,7 +13,6 @@ import dayjs from "@calcom/dayjs"; import { AvailableTimes, AvailableTimesHeader } from "@calcom/features/bookings"; import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"; import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event"; -import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences"; import DatePicker from "@calcom/features/calendars/DatePicker"; import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; import { useSlotsForDate } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate"; @@ -41,6 +40,7 @@ import { TimezoneSelect, } from "@calcom/ui"; +import { useBookerTime } from "../bookings/Booker/components/hooks/useBookerTime"; import { buildCssVarsPerTheme } from "./lib/buildCssVarsPerTheme"; import { getDimension } from "./lib/getDimension"; import type { EmbedTabs, EmbedType, EmbedTypes, PreviewState } from "./types"; @@ -69,6 +69,19 @@ const queryParamsForDialog = [ "month", ]; +function chooseTimezone({ + timezoneFromBookerStore, + timezoneFromTimePreferences, + userSettingsTimezone, +}: { + timezoneFromBookerStore: string | null; + timezoneFromTimePreferences: string; + userSettingsTimezone: string | undefined; +}) { + // We prefer user's timezone configured in settings at the moment - Might be a better idea to prefer timezoneFromTimePreferences over user settings as the user might be in different timezone + return timezoneFromBookerStore ?? userSettingsTimezone ?? timezoneFromTimePreferences; +} + function useRouterHelpers() { const router = useRouter(); const searchParams = useCompatSearchParams(); @@ -146,15 +159,21 @@ const EmailEmbed = ({ username, orgSlug, isTeamEvent, + userSettingsTimezone, }: { eventType?: EventType; username: string; orgSlug?: string; isTeamEvent: boolean; + userSettingsTimezone?: string; }) => { const { t, i18n } = useLocale(); - - const [timezone] = useTimePreferences((state) => [state.timezone]); + const { timezoneFromBookerStore, timezoneFromTimePreferences } = useBookerTime(); + const timezone = chooseTimezone({ + timezoneFromBookerStore, + timezoneFromTimePreferences, + userSettingsTimezone, + }); useInitializeBookerStore({ username, @@ -169,15 +188,17 @@ const EmailEmbed = ({ (state) => [state.month, state.selectedDate, state.selectedDatesAndTimes], shallow ); - const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot] = useBookerStore( - (state) => [ - state.setSelectedDate, - state.setMonth, - state.setSelectedDatesAndTimes, - state.setSelectedTimeslot, - ], - shallow - ); + const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot, setTimezone] = + useBookerStore( + (state) => [ + state.setSelectedDate, + state.setMonth, + state.setSelectedDatesAndTimes, + state.setSelectedTimeslot, + state.setTimezone, + ], + shallow + ); const event = useEvent(); const schedule = useScheduleForEvent({ orgSlug, eventId: eventType?.id, isTeamEvent }); const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots); @@ -306,7 +327,7 @@ const EmailEmbed = ({
{t("timezone")}
- + setTimezone(value)} />
@@ -321,6 +342,7 @@ const EmailEmbedPreview = ({ month, selectedDateAndTime, calLink, + userSettingsTimezone, }: { eventType: EventType; timezone?: string; @@ -329,9 +351,15 @@ const EmailEmbedPreview = ({ month?: string; selectedDateAndTime: { [key: string]: string[] }; calLink: string; + userSettingsTimezone?: string; }) => { const { t } = useLocale(); - const [timeFormat, timezone] = useTimePreferences((state) => [state.timeFormat, state.timezone]); + const { timeFormat, timezoneFromBookerStore, timezoneFromTimePreferences } = useBookerTime(); + const timezone = chooseTimezone({ + timezoneFromBookerStore, + timezoneFromTimePreferences, + userSettingsTimezone, + }); if (!eventType) { return null; @@ -392,7 +420,10 @@ const EmailEmbedPreview = ({ Object.keys(selectedDateAndTime) .sort() .map((key) => { - const selectedDate = dayjs(key).tz(timezone).format("dddd, MMMM D, YYYY"); + const firstSlotOfSelectedDay = selectedDateAndTime[key][0]; + const selectedDate = dayjs(firstSlotOfSelectedDay) + .tz(timezone) + .format("dddd, MMMM D, YYYY"); return ( @@ -1120,6 +1153,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({ eventType={eventTypeData?.eventType} emailContentRef={emailContentRef} username={teamSlug ?? (data?.user.username as string)} + userSettingsTimezone={userSettings?.timeZone} month={month as string} selectedDateAndTime={ selectedDatesAndTimes diff --git a/packages/platform/atoms/booker/BookerWebWrapper.tsx b/packages/platform/atoms/booker/BookerWebWrapper.tsx index 88eb7acc68..b31dd51671 100644 --- a/packages/platform/atoms/booker/BookerWebWrapper.tsx +++ b/packages/platform/atoms/booker/BookerWebWrapper.tsx @@ -44,6 +44,7 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => { const bookingUid = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("bookingUid") : null; const date = dayjs(selectedDate).format("YYYY-MM-DD"); + const timezone = searchParams?.get("cal.tz") || null; useEffect(() => { // This event isn't processed by BookingPageTagManager because BookingPageTagManager hasn't loaded when it is fired. I think we should have a queue in fire method to handle this. @@ -58,6 +59,7 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => { bookingUid: bookingUid, layout: bookerLayout.defaultLayout, org: props.entity.orgSlug, + timezone, }); const [bookerState, _] = useBookerStore((state) => [state.state, state.setState], shallow); diff --git a/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts b/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts index 42d9f75b01..0dbcbbcaf5 100644 --- a/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts +++ b/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts @@ -1,11 +1,8 @@ +import { useBookerTime } from "@calcom/features/bookings/Booker/components/hooks/useBookerTime"; import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import { setLastBookingResponse } from "@calcom/features/bookings/Booker/utils/lastBookingResponse"; -import { - useTimePreferences, - mapBookingToMutationInput, - mapRecurringBookingToMutationInput, -} from "@calcom/features/bookings/lib"; +import { mapBookingToMutationInput, mapRecurringBookingToMutationInput } from "@calcom/features/bookings/lib"; import type { BookerEvent } from "@calcom/features/bookings/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { BookingCreateBody } from "@calcom/prisma/zod-utils"; @@ -41,7 +38,7 @@ export const useHandleBookEvent = ({ const setFormValues = useBookerStore((state) => state.setFormValues); const timeslot = useBookerStore((state) => state.selectedTimeslot); const duration = useBookerStore((state) => state.selectedDuration); - const { timezone } = useTimePreferences(); + const { timezone } = useBookerTime(); const rescheduleUid = useBookerStore((state) => state.rescheduleUid); const rescheduledBy = useBookerStore((state) => state.rescheduledBy); const { t, i18n } = useLocale();