Feat/3334 show times in timezones for bookingpage (#4971)

Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
pull/5091/head
Jeroen Reumkens 2022-10-19 11:45:44 +02:00 committed by GitHub
parent f4fb4ddad1
commit 373c255733
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 178 additions and 25 deletions

View File

@ -1,10 +1,11 @@
import { BookingStatus } from "@prisma/client";
import { useRouter } from "next/router";
import { useState } from "react";
import { useState, useRef } from "react";
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { formatTime } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
@ -15,10 +16,10 @@ import { Tooltip } from "@calcom/ui/Tooltip";
import { TextArea } from "@calcom/ui/form/fields";
import Badge from "@calcom/ui/v2/core/Badge";
import Button from "@calcom/ui/v2/core/Button";
import MeetingTimeInTimezones from "@calcom/ui/v2/core/MeetingTimeInTimezones";
import useMeQuery from "@lib/hooks/useMeQuery";
// import { extractRecurringDates } from "@lib/parseDate";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import TableActions, { ActionType } from "@components/ui/TableActions";
@ -185,7 +186,7 @@ function BookingListItem(booking: BookingItemProps) {
const location = booking.location || "";
const onClick = () => {
const onClickTableData = () => {
router.push({
pathname: "/success",
query: {
@ -207,8 +208,6 @@ function BookingListItem(booking: BookingItemProps) {
});
};
const utcOffset = dayjs().tz(user?.timeZone).utcOffset();
return (
<>
<RescheduleDialog
@ -257,19 +256,21 @@ function BookingListItem(booking: BookingItemProps) {
</Dialog>
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
<td className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[10rem]" onClick={onClick}>
<td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}>
<div className="cursor-pointer py-4">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="text-sm text-gray-500">
{dayjs
.utc(booking.startTime)
.utcOffset(utcOffset)
.format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}{" "}
-{" "}
{dayjs
.utc(booking.endTime)
.utcOffset(utcOffset)
.format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
<MeetingTimeInTimezones
timeFormat={user?.timeFormat}
userTimezone={user?.timeZone}
startTime={booking.startTime}
endTime={booking.endTime}
attendees={booking.attendees}
/>
</div>
{isPending && (
@ -298,21 +299,21 @@ function BookingListItem(booking: BookingItemProps) {
</div>
</div>
</td>
<td className={"w-full px-4" + (isRejected ? " line-through" : "")} onClick={onClick}>
<td className={"w-full px-4" + (isRejected ? " line-through" : "")} onClick={onClickTableData}>
{/* Time and Badges for mobile */}
<div className="w-full pt-4 pb-2 sm:hidden">
<div className="flex w-full items-center justify-between sm:hidden">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="pr-2 text-sm text-gray-500">
{dayjs
.utc(booking.startTime)
.utcOffset(utcOffset)
.format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}{" "}
-{" "}
{dayjs
.utc(booking.endTime)
.utcOffset(utcOffset)
.format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
<MeetingTimeInTimezones
timeFormat={user?.timeFormat}
userTimezone={user?.timeZone}
startTime={booking.startTime}
endTime={booking.endTime}
attendees={booking.attendees}
/>
</div>
</div>

View File

@ -6,3 +6,60 @@ export const yyyymmdd = (date: Date | Dayjs) =>
export const daysInMonth = (date: Date | Dayjs) =>
date instanceof Date ? dayjs(date).daysInMonth() : date.daysInMonth();
/**
* Expects timeFormat to be either 12 or 24, if null or undefined
* is passed in, we always default back to 24 hour notation.
*/
export const formatTime = (
date: string | Date | Dayjs,
timeFormat?: number | null,
timeZone?: string | null
) =>
timeZone
? dayjs(date)
.tz(timeZone)
.format(timeFormat === 12 ? "h:mma" : "HH:mm")
: dayjs(date).format(timeFormat === 12 ? "h:mma" : "HH:mm");
/**
* Sorts two timezones by their offset from GMT.
*/
export const sortByTimezone = (timezoneA: string, timezoneB: string) => {
const timezoneAGmtOffset = dayjs.utc().tz(timezoneA).utcOffset();
const timezoneBGmtOffset = dayjs.utc().tz(timezoneB).utcOffset();
if (timezoneAGmtOffset === timezoneBGmtOffset) return 0;
return timezoneAGmtOffset < timezoneBGmtOffset ? -1 : 1;
};
/**
* Verifies given time is a day before in timezoneB.
*/
export const isPreviousDayInTimezone = (time: string, timezoneA: string, timezoneB: string) => {
const timeInTimezoneA = formatTime(time, 24, timezoneA);
const timeInTimezoneB = formatTime(time, 24, timezoneB);
if (time === timeInTimezoneB) return false;
// Eg timeInTimezoneA = 12:00 and timeInTimezoneB = 23:00
const hoursTimezoneBIsLater = timeInTimezoneB.localeCompare(timeInTimezoneA) === 1;
// If it is 23:00, does timezoneA come before or after timezoneB in GMT?
const timezoneBIsEarlierTimezone = sortByTimezone(timezoneA, timezoneB) === 1;
return hoursTimezoneBIsLater && timezoneBIsEarlierTimezone;
};
/**
* Verifies given time is a day after in timezoneB.
*/
export const isNextDayInTimezone = (time: string, timezoneA: string, timezoneB: string) => {
const timeInTimezoneA = formatTime(time, 24, timezoneA);
const timeInTimezoneB = formatTime(time, 24, timezoneB);
if (time === timeInTimezoneB) return false;
// Eg timeInTimezoneA = 12:00 and timeInTimezoneB = 09:00
const hoursTimezoneBIsEarlier = timeInTimezoneB.localeCompare(timeInTimezoneA) === -1;
// If it is 09:00, does timezoneA come before or after timezoneB in GMT?
const timezoneBIsLaterTimezone = sortByTimezone(timezoneA, timezoneB) === -1;
return hoursTimezoneBIsEarlier && timezoneBIsLaterTimezone;
};

View File

@ -0,0 +1,94 @@
import * as Popover from "@radix-ui/react-popover";
import {
formatTime,
isNextDayInTimezone,
isPreviousDayInTimezone,
sortByTimezone,
} from "@calcom/lib/date-fns";
import { Icon } from "../../Icon";
import { Attendee } from ".prisma/client";
interface MeetingTimeInTimezonesProps {
attendees: Attendee[];
userTimezone?: string;
timeFormat?: number | null;
startTime: string;
endTime: string;
}
const MeetingTimeInTimezones = ({
attendees,
userTimezone,
timeFormat,
startTime,
endTime,
}: MeetingTimeInTimezonesProps) => {
if (!userTimezone || !attendees.length) return null;
const attendeeTimezones = attendees.map((attendee) => attendee.timeZone);
const uniqueTimezones = [userTimezone, ...attendeeTimezones].filter(
(value, index, self) => self.indexOf(value) === index
);
// Convert times to time in timezone, and then sort from earliest to latest time in timezone.
const times = uniqueTimezones
.map((timezone) => {
const isPreviousDay = isPreviousDayInTimezone(startTime, userTimezone, timezone);
const isNextDay = isNextDayInTimezone(startTime, userTimezone, timezone);
return {
startTime: formatTime(startTime, timeFormat, timezone),
endTime: formatTime(endTime, timeFormat, timezone),
timezone,
isPreviousDay,
isNextDay,
};
})
.sort((timeA, timeB) => sortByTimezone(timeA.timezone, timeB.timezone));
// We don't show the popover if there's only one timezone.
if (times.length === 1) return null;
return (
<Popover.Root>
<Popover.Trigger
onClick={preventBubbling}
className="popover-button ml-2 inline-flex h-5 w-5 items-center justify-center rounded-sm text-gray-900 transition-colors hover:bg-gray-200 focus:bg-gray-200">
<Icon.FiGlobe />
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
onClick={preventBubbling}
side="top"
className="popover-content slideInBottom shadow-dropdown border-5 bg-brand-500 rounded-md border-gray-200 p-3 text-sm text-white shadow-sm">
{times.map((time) => (
<span className="mt-2 block first:mt-0" key={time.timezone}>
<span className="inline-flex align-baseline">
{time.startTime} - {time.endTime}
{(time.isNextDay || time.isPreviousDay) && (
<span className="text-medium ml-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-gray-700 text-[10px]">
{time.isNextDay ? "+1" : "-1"}
</span>
)}
</span>
<br />
<span className="text-gray-400">{time.timezone}</span>
</span>
))}
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
};
MeetingTimeInTimezones.displayName = "MeetingTimeInTimezones";
// Prevents propagation so the click on eg booking overview won't
// bubble to the row of the table, causing a navigation to the
// detaill page.
const preventBubbling = (event: React.MouseEvent) => {
event.stopPropagation();
};
export default MeetingTimeInTimezones;

View File

@ -11,6 +11,7 @@ export { default as Dropdown } from "./Dropdown";
export * from "./Dropdown";
export { default as EmptyScreen } from "./EmptyScreen";
export { default as Loader } from "./Loader";
export { default as MeetingTimeInTimezones } from "./MeetingTimeInTimezones";
export { default as PageHeader } from "./PageHeader";
export { default as Shell } from "./Shell";
export { default as Stepper } from "./Stepper";