diff --git a/apps/web/components/booking/AvailableTimes.tsx b/apps/web/components/booking/AvailableTimes.tsx index 0ce732d9c0..5bc5330618 100644 --- a/apps/web/components/booking/AvailableTimes.tsx +++ b/apps/web/components/booking/AvailableTimes.tsx @@ -83,7 +83,7 @@ const AvailableTimes: FC = ({ pathname: router.pathname.endsWith("/embed") ? "../book" : "book", query: { ...router.query, - date: dayjs(slot.time).format(), + date: dayjs.utc(slot.time).tz(timeZone()).format(), type: eventTypeId, slug: eventTypeSlug, /** Treat as recurring only when a count exist and it's not a rescheduling workflow */ diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 37dbda3776..b7c72b173f 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -497,26 +497,17 @@ const BookingPage = ({
- {(rescheduleUid || !eventType.recurringEvent?.freq) && - `${formatTime(dayjs(date).toDate(), user?.timeFormat, user?.timeZone)}, ${dayjs( - date - ).format("dddd, D MMMM YYYY")}`} + {(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`} {!rescheduleUid && eventType.recurringEvent?.freq && - recurringDates.slice(0, 5).map((aDate, key) => { - return ( -

{`${formatTime(aDate, user?.timeFormat, user?.timeZone)}, ${dayjs( - aDate - ).format("dddd, D MMMM YYYY")}`}

- ); + recurringStrings.slice(0, 5).map((timeFormatted, key) => { + return

{timeFormatted}

; })} {!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
( -

{`${formatTime(aDate, user?.timeFormat, user?.timeZone)}, ${dayjs( - aDate - ).format("dddd, D MMMM YYYY")}`}

+ content={recurringStrings.slice(5).map((timeFormatted, key) => ( +

{timeFormatted}

))}>

{t("plus_more", { count: recurringStrings.length - 5 })} diff --git a/apps/web/lib/parseDate.ts b/apps/web/lib/parseDate.ts index 291e34b045..56d67f25a8 100644 --- a/apps/web/lib/parseDate.ts +++ b/apps/web/lib/parseDate.ts @@ -20,15 +20,6 @@ export const parseDate = (date: string | null | Dayjs, i18n: I18n) => { return processDate(date, i18n); }; -// tzid is currently broken in rrule library. -// @see https://github.com/jakubroztocil/rrule/issues/523 -const dateWithZone = (d: Date, timeZone?: string) => { - const dateInLocalTZ = new Date(d.toLocaleString("en-US", { timeZone: "UTC" })); - const dateInTargetTZ = new Date(d.toLocaleString("en-US", { timeZone: timeZone || "UTC" })); - const tzOffset = dateInTargetTZ.getTime() - dateInLocalTZ.getTime(); - return new Date(d.getTime() - tzOffset); -}; - export const parseRecurringDates = ( { startDate, @@ -48,16 +39,21 @@ export const parseRecurringDates = ( const rule = new RRule({ ...restRecurringEvent, count: recurringCount, - dtstart: dayjs(startDate).utc(true).toDate(), - }); - // UTC times with tzOffset applied to account for DST - const times = rule.all().map((t) => dateWithZone(t, timeZone)); - const dateStrings = times.map((t) => { - // undo DST diffs for localized display. - return processDate(dayjs.utc(t).tz(timeZone), i18n); + dtstart: new Date(dayjs(startDate).valueOf()), }); - return [dateStrings, times]; + const startUtcOffset = dayjs(startDate).utcOffset(); + // UTC still need to have DST applied, rrule does not do this. + const times = rule.all().map((t) => { + // applying the DST offset. + return dayjs.utc(t).add(startUtcOffset - dayjs(t).utcOffset(), "minute"); + }); + const dateStrings = times.map((t) => { + // finally; show in local timeZone again + return processDate(t.tz(timeZone), i18n); + }); + + return [dateStrings, times.map((t) => t.toDate())]; }; export const extractRecurringDates = ( diff --git a/packages/lib/slots.ts b/packages/lib/slots.ts index 6385ba0b93..ef84d0544c 100644 --- a/packages/lib/slots.ts +++ b/packages/lib/slots.ts @@ -111,7 +111,24 @@ const getSlots = ({ inviteeDate, frequency, minimumBookingNotice, workingHours, }); slotsTimeFrameAvailable.forEach((item) => { - const slot = startOfInviteeDay.add(item.startTime, "minute"); + // XXX: Hack alert, as dayjs is supposedly not aware of timezone the current slot may have invalid UTC offset. + const timeZone = (startOfInviteeDay as unknown as { $x: { $timezone: string } })["$x"]["$timezone"]; + /* + * @calcom/web:dev: 2022-11-06T00:00:00-04:00 + * @calcom/web:dev: 2022-11-06T01:00:00-04:00 + * @calcom/web:dev: 2022-11-06T01:00:00-04:00 <-- note there is no offset change, but we did lose an hour. + * @calcom/web:dev: 2022-11-06T02:00:00-04:00 + * @calcom/web:dev: 2022-11-06T03:00:00-04:00 + * ... + */ + let slot = dayjs.tz( + startOfInviteeDay.add(item.startTime, "minute").format("YYYY-MM-DDTHH:mm:ss"), + timeZone + ); + // If the startOfInviteeDay has a different UTC offset than the slot, a DST change has occurred. + // As the time has now fallen backwards, or forwards; this difference - + // needs to be manually added as this is not done for us. Usually 0. + slot = slot.add(startOfInviteeDay.utcOffset() - slot.utcOffset(), "minutes"); // Validating slot its not on the past if (!slot.isBefore(startDate)) { slots.push(slot);