diff --git a/components/booking/DatePicker.tsx b/components/booking/DatePicker.tsx index 5fd9832b..e52104d7 100644 --- a/components/booking/DatePicker.tsx +++ b/components/booking/DatePicker.tsx @@ -1,18 +1,22 @@ import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid"; -import { PeriodType } from "@prisma/client"; +import { EventType, 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 { useEffect, useMemo, useState } from "react"; import classNames from "@lib/classNames"; +import { timeZone } from "@lib/clock"; import { useLocale } from "@lib/hooks/useLocale"; import getSlots from "@lib/slots"; import { WorkingHours } from "@lib/types/schedule"; +import Loader from "@components/Loader"; + dayjs.extend(dayjsBusinessTime); dayjs.extend(utc); +dayjs.extend(timezone); type DatePickerProps = { weekStart: string; @@ -20,7 +24,7 @@ type DatePickerProps = { workingHours: WorkingHours[]; eventLength: number; date: Dayjs | null; - periodType: string; + periodType: PeriodType; periodStartDate: Date | null; periodEndDate: Date | null; periodDays: number | null; @@ -28,6 +32,43 @@ type DatePickerProps = { minimumBookingNotice: number; }; +function isOutOfBounds( + time: dayjs.ConfigType, + { + periodType, + periodDays, + periodCountCalendarDays, + periodStartDate, + periodEndDate, + }: Pick< + EventType, + "periodType" | "periodDays" | "periodCountCalendarDays" | "periodStartDate" | "periodEndDate" + > +) { + const date = dayjs(time); + + switch (periodType) { + case PeriodType.ROLLING: { + const periodRollingEndDay = periodCountCalendarDays + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + dayjs().utcOffset(date.utcOffset()).add(periodDays!, "days").endOf("day") + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + dayjs().utcOffset(date.utcOffset()).addBusinessTime(periodDays!, "days").endOf("day"); + return date.endOf("day").isAfter(periodRollingEndDay); + } + + case PeriodType.RANGE: { + const periodRangeStartDay = dayjs(periodStartDate).utcOffset(date.utcOffset()).endOf("day"); + const periodRangeEndDay = dayjs(periodEndDate).utcOffset(date.utcOffset()).endOf("day"); + return date.endOf("day").isBefore(periodRangeStartDay) || date.endOf("day").isAfter(periodRangeEndDay); + } + + case PeriodType.UNLIMITED: + default: + return false; + } +} + function DatePicker({ weekStart, onDatePicked, @@ -42,37 +83,21 @@ function DatePicker({ minimumBookingNotice, }: DatePickerProps): JSX.Element { const { t } = useLocale(); - const [days, setDays] = useState<({ disabled: boolean; date: number } | null)[]>([]); - const [selectedMonth, setSelectedMonth] = useState( - date - ? periodType === PeriodType.RANGE - ? dayjs(periodStartDate).utcOffset(date.utcOffset()).month() - : date.month() - : dayjs().month() /* High chance server is going to have the same month */ - ); + const [browsingDate, setBrowsingDate] = useState(date); useEffect(() => { - if (dayjs().month() !== selectedMonth) { - setSelectedMonth(dayjs().month()); + if (!browsingDate || (date && browsingDate.utcOffset() !== date?.utcOffset())) { + setBrowsingDate(date || dayjs().tz(timeZone())); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [date, browsingDate]); - // Handle month changes - const incrementMonth = () => { - setSelectedMonth((selectedMonth ?? 0) + 1); - }; - - const decrementMonth = () => { - setSelectedMonth((selectedMonth ?? 0) - 1); - }; - - const inviteeDate = (): Dayjs => (date || dayjs()).month(selectedMonth); - - useEffect(() => { + const days = useMemo(() => { + if (!browsingDate) { + return []; + } // Create placeholder elements for empty days in first week - let weekdayOfFirst = inviteeDate().date(1).day(); + let weekdayOfFirst = browsingDate.startOf("month").day(); if (weekStart === "Monday") { weekdayOfFirst -= 1; if (weekdayOfFirst < 0) weekdayOfFirst = 6; @@ -81,65 +106,45 @@ function DatePicker({ const days = Array(weekdayOfFirst).fill(null); const isDisabled = (day: number) => { - const date: Dayjs = inviteeDate().date(day); - switch (periodType) { - case PeriodType.ROLLING: { - if (!periodDays) { - throw new Error("PeriodType rolling requires periodDays"); - } - const periodRollingEndDay = periodCountCalendarDays - ? 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) || - !getSlots({ - inviteeDate: date, - frequency: eventLength, - minimumBookingNotice, - workingHours, - }).length - ); - } - - 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) || - date.endOf("day").isAfter(periodRangeEndDay) || - !getSlots({ - inviteeDate: date, - frequency: eventLength, - minimumBookingNotice, - workingHours, - }).length - ); - } - - case PeriodType.UNLIMITED: - default: - return ( - date.endOf("day").isBefore(dayjs().utcOffset(date.utcOffset())) || - !getSlots({ - inviteeDate: date, - frequency: eventLength, - minimumBookingNotice, - workingHours, - }).length - ); - } + const date = browsingDate.startOf("day").date(day); + return ( + isOutOfBounds(date, { + periodType, + periodStartDate, + periodEndDate, + periodCountCalendarDays, + periodDays, + }) || + !getSlots({ + inviteeDate: date, + frequency: eventLength, + minimumBookingNotice, + workingHours, + }).length + ); }; - const daysInMonth = inviteeDate().daysInMonth(); + const daysInMonth = browsingDate.daysInMonth(); for (let i = 1; i <= daysInMonth; i++) { days.push({ disabled: isDisabled(i), date: i }); } - setDays(days); + return days; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedMonth]); + }, [browsingDate]); + + if (!browsingDate) { + return ; + } + + // Handle month changes + const incrementMonth = () => { + setBrowsingDate(browsingDate?.add(1, "month")); + }; + + const decrementMonth = () => { + setBrowsingDate(browsingDate?.subtract(1, "month")); + }; return (
- {t(inviteeDate().format("MMMM").toLowerCase())} + {t(browsingDate.format("MMMM").toLowerCase())} {" "} - {inviteeDate().format("YYYY")} + {browsingDate.format("YYYY")}
@@ -195,13 +198,13 @@ function DatePicker({
) : (