Skip to content

Commit

Permalink
Fixes user availability to be contextual to the user timezone (calcom…
Browse files Browse the repository at this point in the history
…#1166)

* WIP, WIP, WIP, WIP

* Adds missing types

* Type fixes for useSlots

* Type fixes

* Fixes periodType 500 error when updating

* Adds missing dayjs plugin and type fixes

* An attempt was made to fix tests

* Save work in progress

* Added UTC overflow to days

* Update lib/availability.ts

Co-authored-by: Alex Johansson <[email protected]>

* No more magic numbers

* Fixed slots.test & added getWorkingHours.test

* Tests pass, simpler logic, profit?

* Timezone shifting!

* Forgot to unskip tests

* Updated the user page

* Added American seed user, some fixes

* tmp fix so to continue testing availability

* Removed timeZone parameter, fix defaultValue auto-scroll

Co-authored-by: Omar López <[email protected]>
Co-authored-by: Alex Johansson <[email protected]>
  • Loading branch information
3 people authored and pull[bot] committed Jan 25, 2022
1 parent bbf1dae commit 918b44b
Show file tree
Hide file tree
Showing 23 changed files with 587 additions and 333 deletions.
21 changes: 7 additions & 14 deletions components/booking/AvailableTimes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ import { useSlots } from "@lib/hooks/useSlots";
import Loader from "@components/Loader";

type AvailableTimesProps = {
workingHours: {
days: number[];
startTime: number;
endTime: number;
}[];
timeFormat: string;
minimumBookingNotice: number;
eventTypeId: number;
Expand All @@ -32,7 +27,6 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
eventLength,
eventTypeId,
minimumBookingNotice,
workingHours,
timeFormat,
users,
schedulingType,
Expand All @@ -45,16 +39,15 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
date,
eventLength,
schedulingType,
workingHours,
users,
minimumBookingNotice,
eventTypeId,
});

return (
<div className="sm:pl-4 mt-8 sm:mt-0 text-center sm:w-1/3 md:-mb-5">
<div className="text-gray-600 font-light text-lg mb-4 text-left">
<span className="w-1/2 dark:text-white text-gray-600">
<div className="mt-8 text-center sm:pl-4 sm:mt-0 sm:w-1/3 md:-mb-5">
<div className="mb-4 text-lg font-light text-left text-gray-600">
<span className="w-1/2 text-gray-600 dark:text-white">
<strong>{t(date.format("dddd").toLowerCase())}</strong>
<span className="text-gray-500">
{date.format(", DD ")}
Expand Down Expand Up @@ -91,7 +84,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
<div key={slot.time.format()}>
<Link href={bookingUrl}>
<a
className="block font-medium mb-2 bg-white dark:bg-gray-600 text-primary-500 dark:text-neutral-200 border border-brand dark:border-transparent rounded-sm hover:text-white hover:bg-brand dark:hover:border-black py-4 dark:hover:bg-black"
className="block py-4 mb-2 font-medium bg-white border rounded-sm dark:bg-gray-600 text-primary-500 dark:text-neutral-200 border-brand dark:border-transparent hover:text-white hover:bg-brand dark:hover:border-black dark:hover:bg-black"
data-testid="time">
{slot.time.format(timeFormat)}
</a>
Expand All @@ -100,18 +93,18 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
);
})}
{!loading && !error && !slots.length && (
<div className="w-full h-full flex flex-col justify-center content-center items-center -mt-4">
<div className="flex flex-col items-center content-center justify-center w-full h-full -mt-4">
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
</div>
)}

{loading && <Loader />}

{error && (
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4">
<div className="p-4 border-l-4 border-yellow-400 bg-yellow-50">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
<ExclamationIcon className="w-5 h-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm text-yellow-700">{t("slots_load_fail")}</p>
Expand Down
62 changes: 37 additions & 25 deletions components/booking/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,52 @@
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
import { PeriodType } from "@prisma/client";
import dayjs, { Dayjs } from "dayjs";
// Then, include dayjs-business-time
import dayjsBusinessTime from "dayjs-business-time";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { useEffect, useState } from "react";

import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
import getSlots from "@lib/slots";
import { WorkingHours } from "@lib/types/schedule";

dayjs.extend(dayjsBusinessTime);
dayjs.extend(utc);
dayjs.extend(timezone);

// FIXME prop types
type DatePickerProps = {
weekStart: string;
onDatePicked: (pickedDate: Dayjs) => void;
workingHours: WorkingHours[];
eventLength: number;
date: Dayjs | null;
periodType: string;
periodStartDate: Date | null;
periodEndDate: Date | null;
periodDays: number | null;
periodCountCalendarDays: boolean | null;
minimumBookingNotice: number;
};

function DatePicker({
weekStart,
onDatePicked,
workingHours,
organizerTimeZone,
eventLength,
date,
periodType = "unlimited",
periodType = PeriodType.UNLIMITED,
periodStartDate,
periodEndDate,
periodDays,
periodCountCalendarDays,
minimumBookingNotice,
}: any): JSX.Element {
}: DatePickerProps): JSX.Element {
const { t } = useLocale();
const [days, setDays] = useState<({ disabled: boolean; date: number } | null)[]>([]);

const [selectedMonth, setSelectedMonth] = useState<number | null>(
const [selectedMonth, setSelectedMonth] = useState<number>(
date
? periodType === "range"
? periodType === PeriodType.RANGE
? dayjs(periodStartDate).utcOffset(date.utcOffset()).month()
: date.month()
: dayjs().month() /* High chance server is going to have the same month */
Expand Down Expand Up @@ -71,10 +83,13 @@ function DatePicker({
const isDisabled = (day: number) => {
const date: Dayjs = inviteeDate().date(day);
switch (periodType) {
case "rolling": {
case PeriodType.ROLLING: {
if (!periodDays) {
throw new Error("PeriodType rolling requires periodDays");
}
const periodRollingEndDay = periodCountCalendarDays
? dayjs().tz(organizerTimeZone).add(periodDays, "days").endOf("day")
: dayjs().tz(organizerTimeZone).addBusinessTime(periodDays, "days").endOf("day");
? dayjs.utc().add(periodDays, "days").endOf("day")
: (dayjs.utc() as Dayjs).addBusinessTime(periodDays, "days").endOf("day");
return (
date.endOf("day").isBefore(dayjs().utcOffset(date.utcOffset())) ||
date.endOf("day").isAfter(periodRollingEndDay) ||
Expand All @@ -83,14 +98,13 @@ function DatePicker({
frequency: eventLength,
minimumBookingNotice,
workingHours,
organizerTimeZone,
}).length
);
}

case "range": {
const periodRangeStartDay = dayjs(periodStartDate).tz(organizerTimeZone).endOf("day");
const periodRangeEndDay = dayjs(periodEndDate).tz(organizerTimeZone).endOf("day");
case PeriodType.RANGE: {
const periodRangeStartDay = dayjs(periodStartDate).utc().endOf("day");
const periodRangeEndDay = dayjs(periodEndDate).utc().endOf("day");
return (
date.endOf("day").isBefore(dayjs().utcOffset(date.utcOffset())) ||
date.endOf("day").isBefore(periodRangeStartDay) ||
Expand All @@ -100,12 +114,11 @@ function DatePicker({
frequency: eventLength,
minimumBookingNotice,
workingHours,
organizerTimeZone,
}).length
);
}

case "unlimited":
case PeriodType.UNLIMITED:
default:
return (
date.endOf("day").isBefore(dayjs().utcOffset(date.utcOffset())) ||
Expand All @@ -114,7 +127,6 @@ function DatePicker({
frequency: eventLength,
minimumBookingNotice,
workingHours,
organizerTimeZone,
}).length
);
}
Expand All @@ -137,7 +149,7 @@ function DatePicker({
? "w-full sm:w-1/2 md:w-1/3 sm:border-r sm:dark:border-gray-800 sm:pl-4 sm:pr-6 "
: "w-full sm:pl-4")
}>
<div className="flex text-gray-600 font-light text-xl mb-4">
<div className="flex mb-4 text-xl font-light text-gray-600">
<span className="w-1/2 text-gray-600 dark:text-white">
<strong className="text-gray-900 dark:text-white">
{t(inviteeDate().format("MMMM").toLowerCase())}
Expand All @@ -155,18 +167,18 @@ function DatePicker({
)}
disabled={typeof selectedMonth === "number" && selectedMonth <= dayjs().month()}
data-testid="decrementMonth">
<ChevronLeftIcon className="group-hover:text-black dark:group-hover:text-white w-5 h-5" />
<ChevronLeftIcon className="w-5 h-5 group-hover:text-black dark:group-hover:text-white" />
</button>
<button className="group p-1" onClick={incrementMonth} data-testid="incrementMonth">
<ChevronRightIcon className="group-hover:text-black dark:group-hover:text-white w-5 h-5" />
<button className="p-1 group" onClick={incrementMonth} data-testid="incrementMonth">
<ChevronRightIcon className="w-5 h-5 group-hover:text-black dark:group-hover:text-white" />
</button>
</div>
</div>
<div className="grid grid-cols-7 gap-4 text-center border-b border-t dark:border-gray-800 sm:border-0">
<div className="grid grid-cols-7 gap-4 text-center border-t border-b dark:border-gray-800 sm:border-0">
{["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
.sort((a, b) => (weekStart.startsWith(a) ? -1 : weekStart.startsWith(b) ? 1 : 0))
.map((weekDay) => (
<div key={weekDay} className="uppercase text-gray-500 text-xs tracking-widest my-4">
<div key={weekDay} className="my-4 text-xs tracking-widest text-gray-500 uppercase">
{t(weekDay.toLowerCase()).substring(0, 3)}
</div>
))}
Expand All @@ -178,7 +190,7 @@ function DatePicker({
style={{
paddingTop: "100%",
}}
className="w-full relative">
className="relative w-full">
{day === null ? (
<div key={`e-${idx}`} />
) : (
Expand Down
45 changes: 26 additions & 19 deletions components/booking/pages/AvailabilityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
<HeadSeo
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
name={profile.name}
avatar={profile.image}
name={profile.name || undefined}
avatar={profile.image || undefined}
/>
<CustomBranding val={profile.brandColor} />
<div>
Expand All @@ -109,14 +109,18 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
<div className="block p-4 sm:p-8 md:hidden">
<div className="flex items-center">
<AvatarGroup
items={[{ image: profile.image, alt: profile.name }].concat(
eventType.users
.filter((user) => user.name !== profile.name)
.map((user) => ({
title: user.name,
image: user.avatar,
}))
)}
items={
[
{ image: profile.image, alt: profile.name, title: profile.name },
...eventType.users
.filter((user) => user.name !== profile.name)
.map((user) => ({
title: user.name,
image: user.avatar || undefined,
alt: user.name || undefined,
})),
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
}
size={9}
truncateAfter={5}
/>
Expand Down Expand Up @@ -153,14 +157,18 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
(selectedDate ? "sm:w-1/3" : "sm:w-1/2")
}>
<AvatarGroup
items={[{ image: profile.image, alt: profile.name }].concat(
eventType.users
.filter((user) => user.name !== profile.name)
.map((user) => ({
title: user.name,
image: user.avatar,
}))
)}
items={
[
{ image: profile.image, alt: profile.name, title: profile.name },
...eventType.users
.filter((user) => user.name !== profile.name)
.map((user) => ({
title: user.name,
alt: user.name,
image: user.avatar,
})),
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
}
size={10}
truncateAfter={3}
/>
Expand Down Expand Up @@ -209,7 +217,6 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {

{selectedDate && (
<AvailableTimes
workingHours={workingHours}
timeFormat={timeFormat}
minimumBookingNotice={eventType.minimumBookingNotice}
eventTypeId={eventType.id}
Expand Down
8 changes: 4 additions & 4 deletions components/ui/AvatarGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type AvatarGroupProps = {
items: {
image: string;
title?: string;
alt: string;
alt?: string;
}[];
className?: string;
};
Expand All @@ -30,17 +30,17 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
<ul className={classNames("flex -space-x-2 overflow-hidden", props.className)}>
{props.items.slice(0, props.truncateAfter).map((item, idx) => (
<li key={idx} className="inline-block">
<Avatar imageSrc={item.image} title={item.title} alt={item.alt} size={props.size} />
<Avatar imageSrc={item.image} title={item.title} alt={item.alt || ""} size={props.size} />
</li>
))}
{/*props.items.length > props.truncateAfter && (
<li className="inline-block relative">
<li className="relative inline-block">
<Tooltip.Tooltip delayDuration="300">
<Tooltip.TooltipTrigger className="cursor-default">
<span className="w-16 absolute bottom-1.5 border-2 border-gray-300 flex-inline items-center text-white pt-4 text-2xl top-0 rounded-full block bg-neutral-600">+1</span>
</Tooltip.TooltipTrigger>
{truncatedAvatars.length !== 0 && (
<Tooltip.Content className="p-2 rounded-sm text-sm bg-brand text-white shadow-sm">
<Tooltip.Content className="p-2 text-sm text-white rounded-sm shadow-sm bg-brand">
<Tooltip.Arrow />
<ul>
{truncatedAvatars.map((title) => (
Expand Down
4 changes: 2 additions & 2 deletions components/ui/Scheduler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import React, { useEffect, useState } from "react";
import TimezoneSelect, { ITimezoneOption } from "react-timezone-select";

import { useLocale } from "@lib/hooks/useLocale";
import { OpeningHours, DateOverride } from "@lib/types/event-type";
import { WorkingHours } from "@lib/types/schedule";

import { WeekdaySelect } from "./WeekdaySelect";
import SetTimesModal from "./modal/SetTimesModal";
Expand All @@ -19,7 +19,7 @@ type Props = {
timeZone: string;
availability: Availability[];
setTimeZone: (timeZone: string) => void;
setAvailability: (schedule: { openingHours: OpeningHours[]; dateOverrides: DateOverride[] }) => void;
setAvailability: (schedule: { openingHours: WorkingHours[]; dateOverrides: WorkingHours[] }) => void;
};

/**
Expand Down
Loading

0 comments on commit 918b44b

Please sign in to comment.