From 46fc67f70dd0e98c82fdee5154b91e39eab7be5f Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Mon, 23 Oct 2023 01:21:06 +0100 Subject: [PATCH] fix: Date add 1 day adds 24 hours, not 1 day (#12019) * Date add 1 day adds 24 hours, not 1 day, causing the last date to be lost on dst change * Alternate fix with tests * Extract logic so test file doesnt register tsx --- packages/features/calendars/DatePicker.tsx | 35 +++++------------ .../lib/getAvailableDatesInMonth.test.ts | 39 +++++++++++++++++++ .../calendars/lib/getAvailableDatesInMonth.ts | 32 +++++++++++++++ 3 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 packages/features/calendars/lib/getAvailableDatesInMonth.test.ts create mode 100644 packages/features/calendars/lib/getAvailableDatesInMonth.ts diff --git a/packages/features/calendars/DatePicker.tsx b/packages/features/calendars/DatePicker.tsx index e046aadcea..2bea7d04fa 100644 --- a/packages/features/calendars/DatePicker.tsx +++ b/packages/features/calendars/DatePicker.tsx @@ -5,6 +5,7 @@ import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import { useEmbedStyles } from "@calcom/embed-core/embed-iframe"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; +import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth"; import classNames from "@calcom/lib/classNames"; import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -23,9 +24,9 @@ export type DatePickerProps = { /** which date or dates are currently selected (not tracked from here) */ selected?: Dayjs | Dayjs[] | null; /** defaults to current date. */ - minDate?: Dayjs; + minDate?: Date; /** Furthest date selectable in the future, default = UNLIMITED */ - maxDate?: Dayjs; + maxDate?: Date; /** locale, any IETF language tag, e.g. "hu-HU" - defaults to Browser settings */ locale: string; /** Defaults to [], which dates are not bookable. Array of valid dates like: ["2022-04-23", "2022-04-24"] */ @@ -102,7 +103,7 @@ const NoAvailabilityOverlay = ({ }; const Days = ({ - minDate = dayjs.utc(), + minDate, excludedDates = [], browsingDate, weekStart, @@ -121,30 +122,12 @@ const Days = ({ }) => { // Create placeholder elements for empty days in first week const weekdayOfFirst = browsingDate.date(1).day(); - const currentDate = minDate.utcOffset(browsingDate.utcOffset()); - const availableDates = (includedDates: string[] | undefined) => { - const dates = []; - const lastDateOfMonth = browsingDate.date(daysInMonth(browsingDate)); - for ( - let date = currentDate; - date.isBefore(lastDateOfMonth) || date.isSame(lastDateOfMonth, "day"); - date = date.add(1, "day") - ) { - // even if availableDates is given, filter out the passed included dates - if (includedDates && !includedDates.includes(yyyymmdd(date))) { - continue; - } - dates.push(yyyymmdd(date)); - } - return dates; - }; - const utcBrowsingDateWithOffset = browsingDate.utc().add(browsingDate.utcOffset(), "minute"); - const utcCurrentDateWithOffset = currentDate.utc().add(browsingDate.utcOffset(), "minute"); - - const includedDates = utcCurrentDateWithOffset.isSame(utcBrowsingDateWithOffset, "month") - ? availableDates(props.includedDates) - : props.includedDates; + const includedDates = getAvailableDatesInMonth({ + browsingDate: browsingDate.toDate(), + minDate, + includedDates: props.includedDates, + }); const days: (Dayjs | null)[] = Array((weekdayOfFirst - weekStart + 7) % 7).fill(null); for (let day = 1, dayCount = daysInMonth(browsingDate); day <= dayCount; day++) { diff --git a/packages/features/calendars/lib/getAvailableDatesInMonth.test.ts b/packages/features/calendars/lib/getAvailableDatesInMonth.test.ts new file mode 100644 index 0000000000..10e8fdc147 --- /dev/null +++ b/packages/features/calendars/lib/getAvailableDatesInMonth.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "vitest"; + +import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth"; +import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; + +describe("Test Suite: Date Picker", () => { + describe("Calculates the available dates left in the month", () => { + // *) Use right amount of days in given month. (28, 30, 31) + test("it returns the right amount of days in a given month", () => { + const currentDate = new Date(); + const nextMonthDate = new Date(Date.UTC(currentDate.getFullYear(), currentDate.getMonth() + 1)); + + const result = getAvailableDatesInMonth({ + browsingDate: nextMonthDate, + }); + + expect(result).toHaveLength(daysInMonth(nextMonthDate)); + }); + // *) Dates in the past are not available. + test("it doesn't return dates that already passed", () => { + const currentDate = new Date(); + const result = getAvailableDatesInMonth({ + browsingDate: currentDate, + }); + + expect(result).toHaveLength(daysInMonth(currentDate) - currentDate.getDate() + 1); + }); + // *) Intersect with included dates. + test("it intersects with given included dates", () => { + const currentDate = new Date(); + const result = getAvailableDatesInMonth({ + browsingDate: currentDate, + includedDates: [yyyymmdd(currentDate)], + }); + + expect(result).toHaveLength(1); + }); + }); +}); diff --git a/packages/features/calendars/lib/getAvailableDatesInMonth.ts b/packages/features/calendars/lib/getAvailableDatesInMonth.ts new file mode 100644 index 0000000000..8fbace876b --- /dev/null +++ b/packages/features/calendars/lib/getAvailableDatesInMonth.ts @@ -0,0 +1,32 @@ +import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; + +// calculate the available dates in the month: +// *) Intersect with included dates. +// *) Dates in the past are not available. +// *) Use right amount of days in given month. (28, 30, 31) +export function getAvailableDatesInMonth({ + browsingDate, // pass as UTC + minDate = new Date(), + includedDates, +}: { + browsingDate: Date; + minDate?: Date; + includedDates?: string[]; +}) { + const dates = []; + const lastDateOfMonth = new Date( + Date.UTC(browsingDate.getFullYear(), browsingDate.getMonth(), daysInMonth(browsingDate)) + ); + for ( + let date = browsingDate > minDate ? browsingDate : minDate; + date <= lastDateOfMonth; + date = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate() + 1)) + ) { + // intersect included dates + if (includedDates && !includedDates.includes(yyyymmdd(date))) { + continue; + } + dates.push(yyyymmdd(date)); + } + return dates; +}