diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 7c62698369..dcf04f2627 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -1,6 +1,6 @@ import { BookingStatus } from "@prisma/client"; import { useRouter } from "next/router"; -import { useState, useRef } from "react"; +import { useState, useMemo } from "react"; import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations"; import dayjs from "@calcom/dayjs"; @@ -19,6 +19,7 @@ import MeetingTimeInTimezones from "@calcom/ui/v2/core/MeetingTimeInTimezones"; import showToast from "@calcom/ui/v2/core/notifications"; import useMeQuery from "@lib/hooks/useMeQuery"; +import { extractRecurringDates } from "@lib/parseDate"; import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; import { RescheduleDialog } from "@components/dialog/RescheduleDialog"; @@ -175,14 +176,16 @@ function BookingListItem(booking: BookingItemProps) { setLocationMutation.mutate({ bookingId: booking.id, newLocation }); }; + // Extract recurring dates is intensive to run, so use useMemo. // Calculate the booking date(s) and setup recurring event data to show - const recurringStrings: string[] = []; - const recurringDates: Date[] = []; - // @FIXME: This is importing the RRULE library which is already heavy. Find out a more optimal way do this. - // if (booking.recurringBookings !== undefined && booking.eventType.recurringEvent?.freq !== undefined) { - // [recurringStrings, recurringDates] = extractRecurringDates(booking, user?.timeZone, i18n); - // } + const [recurringStrings, recurringDates] = useMemo(() => { + if (booking.recurringBookings !== undefined && booking.eventType.recurringEvent?.freq !== undefined) { + return extractRecurringDates(booking, user?.timeZone, i18n); + } + return [[], []]; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [user?.timeZone, i18n.language, booking.recurringBookings]); const location = booking.location || ""; @@ -291,11 +294,7 @@ function BookingListItem(booking: BookingItemProps) { )}
- {aDate} + {formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} + {" - "} + {dayjs(aDate).format("D MMMM YYYY")}
))}>{booking.status === BookingStatus.ACCEPTED ? `${t("event_remaining", { - count: recurringDates.length, + count: recurringCount, })}` : getEveryFreqFor({ t, recurringEvent: booking.eventType.recurringEvent, - recurringCount: recurringDates.filter((date) => { - return date >= now; - }).length, + recurringCount, })}
{aDate}
)} + recurringDates.slice(0, 5).map((aDate, key) => { + return ( +{`${formatTime(aDate, user?.timeFormat, user?.timeZone)}, ${dayjs( + aDate + ).format("dddd, D MMMM YYYY")}`}
+ ); + })} {!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && ({aDate}
+ content={recurringDates.slice(5).map((aDate, key) => ( +{`${formatTime(aDate, user?.timeFormat, user?.timeZone)}, ${dayjs( + aDate + ).format("dddd, D MMMM YYYY")}`}
))}>{t("plus_more", { count: recurringStrings.length - 5 })} diff --git a/apps/web/lib/parseDate.ts b/apps/web/lib/parseDate.ts index 601d5e7ec5..ee9ab30342 100644 --- a/apps/web/lib/parseDate.ts +++ b/apps/web/lib/parseDate.ts @@ -20,6 +20,15 @@ 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, @@ -39,22 +48,16 @@ export const parseRecurringDates = ( const rule = new RRule({ ...restRecurringEvent, count: recurringCount, - dtstart: new Date( - Date.UTC( - dayjs.utc(startDate).get("year"), - dayjs.utc(startDate).get("month"), - dayjs.utc(startDate).get("date"), - dayjs.utc(startDate).get("hour"), - dayjs.utc(startDate).get("minute") - ) - ), + 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); }); - const utcOffset = dayjs(startDate).tz(timeZone).utcOffset(); - const dateStrings = rule.all().map((r) => { - return processDate(dayjs.utc(r).utcOffset(utcOffset), i18n); - }); - return [dateStrings, rule.all()]; + return [dateStrings, times]; }; export const extractRecurringDates = ( @@ -66,6 +69,7 @@ export const extractRecurringDates = ( timeZone: string | undefined, i18n: I18n ): [string[], Date[]] => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { count = 0, ...rest } = booking.eventType.recurringEvent !== null ? booking.eventType.recurringEvent : {}; const recurringInfo = booking.recurringBookings.find( diff --git a/apps/web/pages/success.tsx b/apps/web/pages/success.tsx index 0f1af44a7a..a66f561ac2 100644 --- a/apps/web/pages/success.tsx +++ b/apps/web/pages/success.tsx @@ -638,8 +638,6 @@ export function RecurringBookings({ : null; if (recurringBookingsSorted && listingStatus === "recurring") { - // recurring bookings should only be adjusted to the start date. - const utcOffset = dayjs(recurringBookingsSorted[0]).utcOffset(); return ( <> {eventType.recurringEvent?.count && ( @@ -654,10 +652,9 @@ export function RecurringBookings({ {eventType.recurringEvent?.count && recurringBookingsSorted.slice(0, 4).map((dateStr, idx) => (