diff --git a/src/components/DatePicker/Common.tsx b/src/components/DatePicker/Common.tsx index cd48ac38..f93370ea 100644 --- a/src/components/DatePicker/Common.tsx +++ b/src/components/DatePicker/Common.tsx @@ -6,13 +6,7 @@ import { Container } from "../Container/Container"; import { useCalendar, UseCalendarOptions } from "@h6s/calendar"; import { IconButton } from "../IconButton/IconButton"; import { Text } from "../Typography/Text/Text"; - -const locale = "en-US"; -const selectedDateFormatter = new Intl.DateTimeFormat(locale, { - day: "2-digit", - month: "short", - year: "numeric", -}); +import { headerDateFormatter, selectedDateFormatter, weekdayFormatter } from "./utils"; const explicitWidth = "250px"; @@ -137,12 +131,6 @@ export const DateRangePickerInput = ({ ); }; -const weekdayFormatter = new Intl.DateTimeFormat(locale, { weekday: "short" }); -const headerDateFormatter = new Intl.DateTimeFormat(locale, { - month: "short", - year: "numeric", -}); - const DatePickerContainer = styled(Container)` background: ${({ theme }) => theme.click.datePicker.dateOption.color.background.default}; @@ -277,6 +265,7 @@ export const CalendarRenderer = ({ orientation="horizontal" > {headerDateFormatter.format(headerDate)} = { + component: DateRangePicker, args: { + maxRangeLength: undefined, onSelectDateRange: (startDate: Date, endDate: Date) => { console.log("Date range selected: ", startDate, endDate); }, }, - argTypes: { - startDate: { - control: "date", - }, - endDate: { - control: "date", - }, - futureDatesDisabled: { - control: "boolean", - }, - placeholder: { - control: "text", - }, - onSelectDateRange: { - control: "object", - }, + tags: ["autodocs"], + title: "Display/DateRangePicker", +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + predefinedDatesList: [], }, - component: DateRangePicker, render: (args: Args) => { const endDate = args.endDate ? new Date(args.endDate) : undefined; const startDate = args.startDate ? new Date(args.startDate) : undefined; + return ( ); }, - title: "Display/DateRangePicker", - tags: ["autodocs"], }; -export default defaultStory; +export const DateRangeWithMaxRange: Story = { + args: { + maxRangeLength: 15, + predefinedDatesList: [], + }, + render: (args: Args) => { + const endDate = args.endDate ? new Date(args.endDate) : undefined; + const startDate = args.startDate ? new Date(args.startDate) : undefined; -export const Playground = { - ...defaultStory, + return ( + + ); + }, +}; + +export const DateRangeFutureStartDatesDisabled: Story = { + args: { + futureStartDatesDisabled: true, + predefinedDatesList: [], + }, +}; + +export const PredefinedDatesLastSixMonths: Story = { + render: (args: Args) => { + const endDate = args.endDate ? new Date(args.endDate) : undefined; + const startDate = args.startDate ? new Date(args.startDate) : undefined; + const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6); + + return ( + + ); + }, +}; + +export const PredefinedDatesNextSixMonths: Story = { + render: (args: Args) => { + const endDate = args.endDate ? new Date(args.endDate) : undefined; + const startDate = args.startDate ? new Date(args.startDate) : undefined; + const predefinedDatesList = getPredefinedMonthsForDateRangePicker(6); + + return ( + + ); + }, +}; + +export const PredefinedDatesArbitraryDates: Story = { + render: (args: Args) => { + const endDate = args.endDate ? new Date(args.endDate) : undefined; + const startDate = args.startDate ? new Date(args.startDate) : undefined; + const predefinedDatesList = [ + { startDate: new Date("04/14/2025"), endDate: new Date("05/14/2025") }, + { startDate: new Date("05/14/2025"), endDate: new Date("06/14/2025") }, + { startDate: new Date("06/14/2025"), endDate: new Date("07/14/2025") }, + ]; + + return ( + + ); + }, +}; + +export const PredefinedDatesScrollable: Story = { + render: (args: Args) => { + const endDate = args.endDate ? new Date(args.endDate) : undefined; + const startDate = args.startDate ? new Date(args.startDate) : undefined; + const predefinedDatesList = [ + { startDate: new Date("09/14/2024"), endDate: new Date("10/14/2024") }, + { startDate: new Date("10/14/2024"), endDate: new Date("11/14/2024") }, + { startDate: new Date("11/14/2024"), endDate: new Date("12/14/2024") }, + { startDate: new Date("12/14/2024"), endDate: new Date("01/14/2025") }, + { startDate: new Date("01/14/2025"), endDate: new Date("02/14/2025") }, + { startDate: new Date("02/14/2025"), endDate: new Date("03/14/2025") }, + { startDate: new Date("03/14/2025"), endDate: new Date("04/14/2025") }, + { startDate: new Date("04/14/2025"), endDate: new Date("05/14/2025") }, + { startDate: new Date("05/14/2025"), endDate: new Date("06/14/2025") }, + { startDate: new Date("06/14/2025"), endDate: new Date("07/14/2025") }, + ]; + + return ( + + ); + }, }; diff --git a/src/components/DatePicker/DateRangePicker.test.tsx b/src/components/DatePicker/DateRangePicker.test.tsx index 32b921a8..e762ffbc 100644 --- a/src/components/DatePicker/DateRangePicker.test.tsx +++ b/src/components/DatePicker/DateRangePicker.test.tsx @@ -1,6 +1,8 @@ import { renderCUI } from "@/utils/test-utils"; import { DateRangePicker } from "./DateRangePicker"; import userEvent from "@testing-library/user-event"; +import { getPredefinedMonthsForDateRangePicker } from "./utils"; +import { fireEvent } from "@testing-library/dom"; describe("DateRangePicker", () => { it("opens the calendar on click", async () => { @@ -149,5 +151,235 @@ describe("DateRangePicker", () => { expect(handleSelectDate).not.toHaveBeenCalled(); }); + + it("allows restricting the max range length", async () => { + const startDate = new Date("07-04-2020"); + const handleSelectDate = vi.fn(); + const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); + + const { getByTestId, findByText } = renderCUI( + + ); + + user.click(getByTestId("daterangepicker-input")); + user.click(await findByText("25")); + + expect(handleSelectDate).not.toHaveBeenCalled(); + + fireEvent.click(await findByText("15")); + expect(handleSelectDate).toHaveBeenCalled(); + }); + }); + + describe("predefined date ranges", () => { + beforeEach(() => { + vi.setSystemTime(new Date("07-04-2020")); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("doesn't show any preselected dates if the predefined dates list isn't set", async () => { + const handleSelectDate = vi.fn(); + + const { getByTestId, queryByTestId } = renderCUI( + + ); + + await userEvent.click(getByTestId("daterangepicker-input")); + + expect(queryByTestId("predefined-dates-list")).not.toBeInTheDocument(); + }); + + it("shows dates in the past if getPredefinedMonthsForDateRangePicker's value is negative", async () => { + const handleSelectDate = vi.fn(); + + const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6); + const { getByTestId, getByText } = renderCUI( + + ); + + await userEvent.click(getByTestId("daterangepicker-input")); + + expect(getByText("Jul 01, 2020 - Jul 04, 2020")).toBeInTheDocument(); + expect(getByText("Jun 2020")).toBeInTheDocument(); + expect(getByText("May 2020")).toBeInTheDocument(); + expect(getByText("Apr 2020")).toBeInTheDocument(); + expect(getByText("Mar 2020")).toBeInTheDocument(); + expect(getByText("Feb 2020")).toBeInTheDocument(); + }); + + it("doesn't show a range of a single day when using the past six months and its the first of the month", async () => { + vi.setSystemTime(new Date("07-01-2020")); + + const handleSelectDate = vi.fn(); + + const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6); + const { getByTestId, getByText, queryByText } = renderCUI( + + ); + + await userEvent.click(getByTestId("daterangepicker-input")); + + expect(queryByText("Jul")).not.toBeInTheDocument(); + expect(getByText("Jun 2020")).toBeInTheDocument(); + expect(getByText("May 2020")).toBeInTheDocument(); + expect(getByText("Apr 2020")).toBeInTheDocument(); + expect(getByText("Mar 2020")).toBeInTheDocument(); + expect(getByText("Feb 2020")).toBeInTheDocument(); + }); + + it("shows dates in the future if getPredefinedMonthsForDateRangePicker's value is positive", async () => { + const handleSelectDate = vi.fn(); + + const predefinedDatesList = getPredefinedMonthsForDateRangePicker(6); + const { getByTestId, getByText } = renderCUI( + + ); + + await userEvent.click(getByTestId("daterangepicker-input")); + + expect(getByText("Jul 2020")).toBeInTheDocument(); + expect(getByText("Aug 2020")).toBeInTheDocument(); + expect(getByText("Sep 2020")).toBeInTheDocument(); + expect(getByText("Oct 2020")).toBeInTheDocument(); + expect(getByText("Nov 2020")).toBeInTheDocument(); + expect(getByText("Dec 2020")).toBeInTheDocument(); + }); + + it("shows the current current month if it's the first day of month if getPredefinedMonthsForDateRangePicker's value is positive", async () => { + vi.setSystemTime(new Date("07-01-2020")); + const handleSelectDate = vi.fn(); + + const predefinedDatesList = getPredefinedMonthsForDateRangePicker(6); + const { getByTestId, getByText } = renderCUI( + + ); + + await userEvent.click(getByTestId("daterangepicker-input")); + + expect(getByText("Jul 2020")).toBeInTheDocument(); + expect(getByText("Aug 2020")).toBeInTheDocument(); + expect(getByText("Sep 2020")).toBeInTheDocument(); + expect(getByText("Oct 2020")).toBeInTheDocument(); + expect(getByText("Nov 2020")).toBeInTheDocument(); + expect(getByText("Dec 2020")).toBeInTheDocument(); + }); + + it("allows showing the full calendar", async () => { + const handleSelectDate = vi.fn(); + + const predefinedDatesList = getPredefinedMonthsForDateRangePicker(6); + const { getByTestId, getByText, queryByTestId } = renderCUI( + + ); + + expect(queryByTestId("datepicker-calendar-container")).not.toBeInTheDocument(); + + await userEvent.click(getByTestId("daterangepicker-input")); + await userEvent.click(getByText("Custom time period")); + + expect(getByTestId("datepicker-calendar-container")).toBeInTheDocument(); + }); + + it("selects up to the current date if the current month is selected", async () => { + const handleSelectDate = vi.fn(); + + const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6); + const { getByTestId, getByText } = renderCUI( + + ); + + await userEvent.click(getByTestId("daterangepicker-input")); + + await userEvent.click(getByTestId("Jul 01, 2020 - Jul 04, 2020")); + expect(getByText("Jul 01, 2020 – Jul 04, 2020")).toBeInTheDocument(); + }); + + it("selects the full month if a date in the past or future is selected", async () => { + const handleSelectDate = vi.fn(); + + const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6); + const { getByTestId, getByText } = renderCUI( + + ); + + await userEvent.click(getByTestId("daterangepicker-input")); + + await userEvent.click(getByText("May 2020")); + expect(getByText("May 01, 2020 – May 31, 2020")).toBeInTheDocument(); + }); + + it("shows the selected month if an entire month is manually selected", async () => { + const handleSelectDate = vi.fn(); + + const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6); + const { getByTestId, getAllByText, getByText } = renderCUI( + + ); + + await userEvent.click(getByTestId("daterangepicker-input")); + await userEvent.click(getByText("Custom time period")); + await userEvent.click(getByTestId("calendar-previous-month")); + + await userEvent.click(getAllByText("1")[0]); + await userEvent.click(getByText("30")); + + expect(getByText("Jun 01, 2020 – Jun 30, 2020")).toBeInTheDocument(); + + await userEvent.click(getByTestId("daterangepicker-input")); + expect(getByText("Jun 2020").getAttribute("data-selected")).toBeTruthy(); + }); + + it("shows months wrapping around to the next or previous year", async () => { + vi.setSystemTime(new Date("03-04-2020")); + const handleSelectDate = vi.fn(); + + const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6); + const { getByTestId, getByText } = renderCUI( + + ); + + await userEvent.click(getByTestId("daterangepicker-input")); + + expect(getByText("Mar 01, 2020 - Mar 04, 2020")).toBeInTheDocument(); + expect(getByText("Feb 2020")).toBeInTheDocument(); + expect(getByText("Jan 2020")).toBeInTheDocument(); + expect(getByText("Dec 2019")).toBeInTheDocument(); + expect(getByText("Nov 2019")).toBeInTheDocument(); + expect(getByText("Oct 2019")).toBeInTheDocument(); + }); }); }); diff --git a/src/components/DatePicker/DateRangePicker.tsx b/src/components/DatePicker/DateRangePicker.tsx index 68119069..879eae78 100644 --- a/src/components/DatePicker/DateRangePicker.tsx +++ b/src/components/DatePicker/DateRangePicker.tsx @@ -1,8 +1,60 @@ -import { useCallback, useEffect, useState } from "react"; +import { + Dispatch, + MouseEvent, + SetStateAction, + useCallback, + useEffect, + useState, +} from "react"; import { isSameDate, UseCalendarOptions } from "@h6s/calendar"; import { styled } from "styled-components"; -import Dropdown from "../Dropdown/Dropdown"; +import { Dropdown } from "../Dropdown/Dropdown"; import { Body, CalendarRenderer, DateRangePickerInput, DateTableCell } from "./Common"; +import { Container } from "../Container/Container"; +import { Panel } from "../Panel/Panel"; +import { Icon } from "../Icon/Icon"; +import { + DateRange, + datesAreWithinMaxRange, + isDateRangeTheWholeMonth, + selectedDateFormatter, +} from "./utils"; + +const PredefinedCalendarContainer = styled(Panel)` + align-items: start; + background: ${({ theme }) => theme.click.panel.color.background.muted}; +`; + +const PredefinedDatesContainer = styled(Container)` + width: 275px; +`; + +// left value of 276px is the width of the PredefinedDatesContainer + 1 pixel for border +const CalendarRendererContainer = styled.div` + border: ${({ theme }) => + `${theme.click.datePicker.dateOption.stroke} solid ${theme.click.datePicker.dateOption.color.background.range}`}; + border-radius: ${({ theme }) => theme.click.datePicker.dateOption.radii.default}; + box-shadow: lch(6.77 0 0 / 0.15) 4px 4px 6px -1px, lch(6.77 0 0 / 0.15) 2px 2px 4px -1px; + left: 276px; + position: absolute; + top: 0; +`; + +// Height of 221px is height the height the calendar needs to match the PredefinedDatesContainer +const StyledCalendarRenderer = styled(CalendarRenderer)` + border-radius: ${({ theme }) => theme.click.datePicker.dateOption.radii.default}; + min-height: 221px; +`; + +const StyledDropdownItem = styled(Dropdown.Item)` + min-height: 24px; +`; + +// max-height of 210px allows the scrollable container to be a reasonble height that matches the calendar +const ScrollableContainer = styled(Container)` + max-height: 210px; + overflow-y: auto; +`; const DateRangeTableCell = styled(DateTableCell)<{ $shouldShowRangeIndicator?: boolean; @@ -20,6 +72,8 @@ interface CalendarProps { calendarBody: Body; closeDatepicker: () => void; futureDatesDisabled: boolean; + futureStartDatesDisabled: boolean; + maxRangeLength: number; setSelectedDate: (selectedDate: Date) => void; startDate?: Date; endDate?: Date; @@ -29,6 +83,8 @@ const Calendar = ({ calendarBody, closeDatepicker, futureDatesDisabled, + futureStartDatesDisabled, + maxRangeLength, setSelectedDate, startDate, endDate, @@ -50,11 +106,27 @@ const Calendar = ({ const today = new Date(); const isCurrentDate = isSameDate(today, fullDate); - const isDisabled = futureDatesDisabled ? fullDate > today : false; const isBetweenStartAndEndDates = Boolean( startDate && endDate && fullDate > startDate && fullDate < endDate ); + let isDisabled = false; + if (futureDatesDisabled && fullDate > today) { + isDisabled = true; + } + + if (futureStartDatesDisabled && !startDate && fullDate > today) { + isDisabled = true; + } + + if ( + maxRangeLength > 1 && + startDate && + !datesAreWithinMaxRange(startDate, fullDate, maxRangeLength) + ) { + isDisabled = true; + } + const shouldShowRangeIndicator = !endDate && Boolean( @@ -91,7 +163,7 @@ const Calendar = ({ return ( void; + predefinedDatesList: Array; + selectedEndDate: Date | undefined; + selectedStartDate: Date | undefined; + setEndDate: Dispatch>; + setStartDate: Dispatch>; + shouldShowCustomRange: boolean; + showCustomDateRange: Dispatch>; +} + +const PredefinedDates = ({ + onSelectDateRange, + predefinedDatesList, + selectedEndDate, + selectedStartDate, + setEndDate, + setStartDate, + shouldShowCustomRange, + showCustomDateRange, +}: PredefinedDatesProps) => { + const handleCustomTimePeriodClick = (event: MouseEvent) => { + event.preventDefault(); + showCustomDateRange(!shouldShowCustomRange); + }; + + return ( + + + {predefinedDatesList.map(({ startDate, endDate }) => { + const handleItemClick = () => { + setStartDate(startDate); + setEndDate(endDate); + onSelectDateRange(startDate, endDate); + }; + + const rangeIsSelected = + selectedEndDate && + isSameDate(selectedEndDate, endDate) && + selectedStartDate && + isSameDate(selectedStartDate, startDate); + + const isWholeMonth = isDateRangeTheWholeMonth({ startDate, endDate }); + + const formattedText = isWholeMonth + ? monthFormatter.format(startDate) + : `${selectedDateFormatter.format( + startDate + )} - ${selectedDateFormatter.format(endDate)}`.trim(); + + return ( + + + {formattedText} + {rangeIsSelected && } + + + ); + })} + + + + Custom time period + + + + ); +}; + +export interface DateRangePickerProps { endDate?: Date; disabled?: boolean; futureDatesDisabled?: boolean; + futureStartDatesDisabled?: boolean; onSelectDateRange: (selectedStartDate: Date, selectedEndDate: Date) => void; placeholder?: string; + predefinedDatesList?: Array; + maxRangeLength?: number; startDate?: Date; } @@ -125,12 +291,16 @@ export const DateRangePicker = ({ startDate, disabled = false, futureDatesDisabled = false, + futureStartDatesDisabled = false, + maxRangeLength = -1, onSelectDateRange, placeholder = "start date – end date", -}: DatePickerProps) => { + predefinedDatesList, +}: DateRangePickerProps) => { const [isOpen, setIsOpen] = useState(false); const [selectedStartDate, setSelectedStartDate] = useState(); const [selectedEndDate, setSelectedEndDate] = useState(); + const [shouldShowCustomRange, setShouldShowCustomRange] = useState(false); const calendarOptions: UseCalendarOptions = {}; @@ -153,13 +323,27 @@ export const DateRangePicker = ({ const closeDatePicker = useCallback((): void => { setIsOpen(false); + setShouldShowCustomRange(false); }, []); + const handleOpenChange = (isOpen: boolean): void => { + setIsOpen(isOpen); + + if (!isOpen) { + setShouldShowCustomRange(false); + } + }; + const handleSelectDate = useCallback( (selectedDate: Date): void => { // Start date and end date are selected, user clicks any date. // Set start date to the selected date, clear the end date. if (selectedStartDate && selectedEndDate) { + // If futureStartDatesDisabled is true, only set the selected date to the date clicked if it's before today + if (futureStartDatesDisabled && selectedDate > new Date()) { + setSelectedEndDate(undefined); + return; + } setSelectedStartDate(selectedDate); setSelectedEndDate(undefined); return; @@ -183,17 +367,21 @@ export const DateRangePicker = ({ // Otherwise, set the end date to the date the user clicked. setSelectedEndDate(selectedDate); onSelectDateRange(selectedStartDate, selectedDate); + setShouldShowCustomRange(false); return; } setSelectedStartDate(selectedDate); }, - [onSelectDateRange, selectedEndDate, selectedStartDate] + [futureStartDatesDisabled, onSelectDateRange, selectedEndDate, selectedStartDate] ); + const shouldShowPredefinedDates = + predefinedDatesList !== undefined && predefinedDatesList.length > 0; + return ( @@ -207,18 +395,58 @@ export const DateRangePicker = ({ /> - - {body => ( - + - )} - + + {shouldShowCustomRange && ( + + + {(body: Body) => ( + + )} + + + )} + + ) : ( + + {(body: Body) => ( + + )} + + )} ); diff --git a/src/components/DatePicker/utils.test.ts b/src/components/DatePicker/utils.test.ts new file mode 100644 index 00000000..41da3035 --- /dev/null +++ b/src/components/DatePicker/utils.test.ts @@ -0,0 +1,75 @@ +import { datesAreWithinMaxRange, isDateRangeTheWholeMonth } from "./utils"; + +describe("DatePicker utils", () => { + describe("checking if two dates are fall within a range", () => { + it("returns true if the two dates are within the range", () => { + const startDate = new Date("07-01-2025"); + const endDate = new Date("07-08-2025"); + + expect(datesAreWithinMaxRange(startDate, endDate, 15)).toBeTruthy(); + }); + + it("returns false if the two dates are not within the range", () => { + const startDate = new Date("07-01-2025"); + const endDate = new Date("07-31-2025"); + + expect(datesAreWithinMaxRange(startDate, endDate, 15)).toBeFalsy(); + }); + + it("is inclusive with dates", () => { + const startDate = new Date("07-01-2025"); + const endDate = new Date("07-16-2025"); + + expect(datesAreWithinMaxRange(startDate, endDate, 15)).toBeTruthy(); + }); + }); + + describe("checking if a date range occupies an entire month", () => { + it("returns false is the date range don't have the same month", () => { + const startDate = new Date("07-01-2025"); + const endDate = new Date("08-16-2025"); + expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeFalsy(); + }); + + it("returns false is the date range starts before the first day in the same month", () => { + const startDate = new Date("07-02-2025"); + const endDate = new Date("07-31-2025"); + expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeFalsy(); + }); + + it("returns false is the date range ends before the last day in the same month", () => { + const startDate = new Date("07-01-2025"); + const endDate = new Date("07-30-2025"); + expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeFalsy(); + }); + + it("returns true is the date range occupies the whole month", () => { + let startDate = new Date("07-01-2025"); + let endDate = new Date("07-31-2025"); + expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeTruthy(); + + startDate = new Date("08-01-2025"); + endDate = new Date("08-31-2025"); + expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeTruthy(); + + startDate = new Date("09-01-2025"); + endDate = new Date("09-30-2025"); + expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeTruthy(); + + startDate = new Date("02-01-2025"); + endDate = new Date("02-28-2025"); + expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeTruthy(); + }); + + it("handles leap years", () => { + // 2024 was a leap year + let startDate = new Date("02-01-2024"); + let endDate = new Date("02-29-2024"); + expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeTruthy(); + + startDate = new Date("02-01-2024"); + endDate = new Date("02-28-2024"); + expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeFalsy(); + }); + }); +}); diff --git a/src/components/DatePicker/utils.ts b/src/components/DatePicker/utils.ts new file mode 100644 index 00000000..0696730f --- /dev/null +++ b/src/components/DatePicker/utils.ts @@ -0,0 +1,76 @@ +import dayjs from "dayjs"; + +export interface DateRange { + startDate: Date; + endDate: Date; +} + +const locale = "en-US"; + +export const selectedDateFormatter = new Intl.DateTimeFormat(locale, { + day: "2-digit", + month: "short", + year: "numeric", +}); + +export const weekdayFormatter = new Intl.DateTimeFormat(locale, { weekday: "short" }); +export const headerDateFormatter = new Intl.DateTimeFormat(locale, { + month: "short", + year: "numeric", +}); + +export const getPredefinedMonthsForDateRangePicker = ( + numberOfMonths: number +): Array => { + const now = dayjs(); + + if (numberOfMonths < 0) { + const lastSixMonths: Array = []; + for (let i = 0; i < Math.abs(numberOfMonths); i++) { + const date = now.subtract(i, "month"); + if (date.date() === 1 && date.month() === now.month()) { + continue; + } + lastSixMonths.push({ + startDate: date.startOf("month").toDate(), + endDate: i === 0 ? now.toDate() : date.endOf("month").toDate(), + }); + } + + return lastSixMonths.reverse(); + } + + const nextSixMonths: Array = []; + for (let i = 0; i < numberOfMonths; i++) { + const date = now.add(i, "month"); + nextSixMonths.push({ + startDate: date.startOf("month").toDate(), + endDate: date.endOf("month").toDate(), + }); + } + + return nextSixMonths; +}; + +export const datesAreWithinMaxRange = ( + startDate: Date, + endDate: Date, + maxRangeLength: number +): boolean => { + const daysDifference = Math.abs(dayjs(startDate).diff(dayjs(endDate), "days")); + + return daysDifference <= maxRangeLength; +}; + +export const isDateRangeTheWholeMonth = ({ startDate, endDate }: DateRange): boolean => { + if (startDate.getMonth() !== endDate.getMonth()) { + return false; + } + + const start = dayjs(startDate); + const end = dayjs(endDate); + const startDateIsFirstDay = start.isSame(start.startOf("month"), "day"); + const endDateIsLastDay = end.isSame(end.endOf("month"), "day"); + + return startDateIsFirstDay && endDateIsLastDay; +}; diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index d2f396f2..85b26b0b 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -1,9 +1,10 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { styled } from "styled-components"; -import { HorizontalDirection, Icon, IconName } from "@/components"; import { Arrow, GenericMenuItem, GenericMenuPanel } from "../GenericMenu"; import PopoverArrow from "../icons/PopoverArrow"; import IconWrapper from "../IconWrapper/IconWrapper"; +import { HorizontalDirection, IconName } from "../types"; +import { Icon } from "../Icon/Icon"; export const Dropdown = (props: DropdownMenu.DropdownMenuProps) => ( diff --git a/src/components/index.ts b/src/components/index.ts index a2c24c20..f6bce05a 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -25,6 +25,8 @@ export { ContextMenu } from "./ContextMenu/ContextMenu"; export { Container } from "./Container/Container"; export { DateDetails } from "@/components/DateDetails/DateDetails"; export { DatePicker } from "./DatePicker/DatePicker"; +export { DateRangePicker } from "./DatePicker/DateRangePicker"; +export { getPredefinedMonthsForDateRangePicker } from "./DatePicker/utils"; export { Dialog } from "./Dialog/Dialog"; export { EllipsisContent } from "./EllipsisContent/EllipsisContent"; export { FileUpload } from "./FileUpload/FileUpload";