diff --git a/src/components/DatePicker/DatePicker.css b/src/components/DatePicker/DatePicker.css
index 1b6f3a626..39053460f 100644
--- a/src/components/DatePicker/DatePicker.css
+++ b/src/components/DatePicker/DatePicker.css
@@ -167,8 +167,8 @@
@apply !bg-blue !text-white;
}
-/* Make the start date have a 50% light blue background towards the RIGHT side */
-.date-range-picker .DayPicker-Day--start:not(.DayPicker-Day--outside) {
+/* Make the start date have a 50% light blue background towards the RIGHT side when start and end is not same */
+.date-range-picker .DayPicker-Day--start:not(.DayPicker-Day--outside):not(.DayPicker-Day--end) {
@apply rounded-none;
background: linear-gradient(90deg, #ffffff 40%, #d1e1ff 25%);
}
@@ -177,18 +177,23 @@
@apply rounded-full;
}
-/* Make the end date have a 50% light blue background towards the LEFT side */
-.date-range-picker .DayPicker-Day--end:not(.DayPicker-Day--outside) {
+/* Make the end date have a 50% light blue background towards the LEFT side when start and end is not same */
+.date-range-picker .DayPicker-Day--end:not(.DayPicker-Day--outside):not(.DayPicker-Day--start) {
@apply rounded-none;
background: linear-gradient(90deg, #d1e1ff 40%, #ffffff 25%); /* D1E1FF Blue lighter */
}
+/* This is when start & end are the same. We shown an outline to indicate it is different */
+.date-range-picker .DayPicker-Day--start.DayPicker-Day--end:not(.DayPicker-Day--disabled) .ui-date-picker-day {
+ @apply outline outline-offset-1 outline-blue-lighter;
+}
+
.date-range-picker .DayPicker-Day--end div {
@apply rounded-full;
}
/**
- * These are stylings for the Date Picker, but only when they have custom content like prices or seller messaging
+ * These are styles for the Date Picker, but only when they have custom content like prices or seller messaging
*/
.has-custom-content .DayPicker-Month {
border-spacing: 1px 16px;
diff --git a/src/components/DatePicker/DatePicker.jsx b/src/components/DatePicker/DatePicker.jsx
index 44ff4869b..a2a21306f 100644
--- a/src/components/DatePicker/DatePicker.jsx
+++ b/src/components/DatePicker/DatePicker.jsx
@@ -7,6 +7,7 @@ import "react-day-picker/lib/style.css";
import "./DatePicker.css";
import { isArray, isFunction } from "lodash";
import { Tooltip } from "../..";
+import { now, isSame, toDate, isValidTimeZoneName } from "../../helpers/date";
import { Day } from "./Day";
import { MonthYearSelector } from "./MonthYearSelector";
import { RelativeDateRange } from "./RelativeDateRange";
@@ -41,7 +42,7 @@ export const DatePicker = ({
...rest
}) => {
const initialValue = value ? (variant === variants.single ? value : value.from) : null;
- const [currentMonth, setCurrentMonth] = useState(initialValue ?? dayjs().toDate());
+ const [currentMonth, setCurrentMonth] = useState(initialValue ?? now(null, timezoneName).toDate());
const [startMonth, setStartMonth] = useState(() => {
if (!value || !value.from) {
return new Date();
@@ -51,11 +52,11 @@ export const DatePicker = ({
});
const [endMonth, setEndMonth] = useState(() => {
if (!value || !value.to || !value.from) {
- return dayjs(new Date()).add(1, "month").toDate();
+ return now(null, timezoneName).add(1, "month").toDate();
}
- return dayjs(value.to).isSame(dayjs(value.from), "month")
- ? dayjs(value.from).add(1, "month").toDate()
+ return isSame(now(value.to, timezoneName), now(value.from, timezoneName), "month")
+ ? now(value.from, timezoneName).add(1, "month").toDate()
: value.to;
});
const [rangeName, setRangeName] = useState("");
@@ -67,24 +68,31 @@ export const DatePicker = ({
onMonthChange?.(currentMonth);
}, [currentMonth, onMonthChange]);
+ useEffect(() => {
+ if (timezoneName && !isValidTimeZoneName(timezoneName)) {
+ console.log(`${timezoneName} is not a valid timezone. Using default timezone now`);
+ dayjs.tz.setDefault();
+ }
+ }, [timezoneName]);
+
const handleTodayClick = (day, options, event) => {
if (isRangeVariant) {
return;
}
- const today = timezoneName ? dayjs().tz(timezoneName).toDate() : new Date();
+ const today = timezoneName ? toDate(now(day, timezoneName)) : new Date();
if (options.disabled || isDisabled(today)) {
setCurrentMonth(today);
onMonthChange?.(today);
} else {
- onChange(day, options, event);
+ onChange(today, options, event);
}
};
const isDisabled = (date) => {
if (isArray(disabledDays)) {
- return disabledDays.some((_date) => dayjs(_date).isSame(date, "day"));
+ return disabledDays.some((_date) => isSame(now(_date, timezoneName), date, "day"));
}
if (isFunction(disabledDays)) {
@@ -97,6 +105,7 @@ export const DatePicker = ({
const handleRelativeRangeChanged = (rangeName, range) => {
setCurrentMonth(range.from);
setStartMonth(range.from);
+ setEndMonth(range.to);
onChange({ ...range, rangeName }, modifiers, null);
};
@@ -120,7 +129,7 @@ export const DatePicker = ({
return;
}
- if (dayjs(value?.from).isSame(day, "month")) {
+ if (isSame(now(value?.from, timezoneName), now(day, timezoneName), "month")) {
handleStartMonthChange(day);
}
@@ -129,25 +138,31 @@ export const DatePicker = ({
if (isValidValue) {
// This allows us to easily select another date range,
// if both dates are selected.
- onChange({ from: day, to: null }, options, event);
+ onChange({ from: toDate(now(day, timezoneName).startOf("day")), to: null }, options, event);
} else if (value && (value.from || value.to) && (value.from || value.to).getTime() === day.getTime()) {
- const from = dayjs(day).startOf("day").toDate();
- const to = dayjs(day).endOf("day").toDate();
+ const from = toDate(now(day, timezoneName).startOf("day"));
+ const to = toDate(now(day, timezoneName).endOf("day"), false);
onChange({ from, to }, options, event);
} else {
- onChange(DateUtils.addDayToRange(day, value), options, event);
+ onChange(
+ DateUtils.addDayToRange(toDate(now(day, timezoneName).endOf("day"), false), value),
+ options,
+ event,
+ );
}
} else {
- onChange(day, options, event);
+ onChange(toDate(now(day, timezoneName)), options, event);
}
};
+ // TODO: Should be outside this component because this returns JSX
const CaptionElement =
shouldShowYearPicker && currentMonth
? ({ date }) =>
: undefined;
+ // TODO: Should be outside this component because this returns JSX
const renderDay = (date) => {
const tooltipContent = getTooltip?.(date);
const disabled = isDisabled(date);
@@ -178,7 +193,7 @@ export const DatePicker = ({
// Comparing `from` and `to` dates hides a weird CSS style when you select the same date twice in a date range.
const useDateRangeStyle = isRangeVariant && isValidValue && value.from?.getTime() !== value.to?.getTime();
// Return the same value if it is already dayjs object or has range variant otherwise format it to dayJs object
- const selectedDays = value && (dayjs.isDayjs(value) || isRangeVariant ? value : dayjs(value).toDate());
+ const selectedDays = value && (dayjs.isDayjs(value) || isRangeVariant ? value : now(value, timezoneName).toDate());
return (
<>
@@ -208,6 +223,7 @@ export const DatePicker = ({
handleEndMonthChange={handleEndMonthChange}
handleTodayClick={handleTodayClick}
selectedDays={selectedDays}
+ timezoneName={timezoneName}
{...rest}
/>
) : (
@@ -236,14 +252,17 @@ export const DatePicker = ({
{components.Footer ? : null}
- {useDateRangeStyle && shouldShowRelativeRanges && (
-
-
+ {shouldShowRelativeRanges && (
+
)}
>
diff --git a/src/components/DatePicker/RangeDatePicker.jsx b/src/components/DatePicker/RangeDatePicker.jsx
index 75a1d846f..06d83994f 100644
--- a/src/components/DatePicker/RangeDatePicker.jsx
+++ b/src/components/DatePicker/RangeDatePicker.jsx
@@ -2,9 +2,9 @@ import React from "react";
import PropTypes from "prop-types";
import DayPicker from "react-day-picker";
import clsx from "clsx";
-import dayjs from "dayjs";
import { isArray, isFunction } from "lodash";
import { Tooltip } from "../Tooltip";
+import { now } from "../../helpers/date";
import { NavbarElement } from "./NavbarElement";
import { MonthYearSelector } from "./MonthYearSelector";
import { Day } from "./Day";
@@ -24,10 +24,11 @@ const RangeDatePicker = ({
handleStartMonthChange,
handleEndMonthChange,
handleTodayClick,
+ timezoneName,
...rest
}) => {
- const isStartDateIsTheSameMonth = dayjs(value?.from).isSame(dayjs(value?.to), "month");
- const isSingleDayDateRange = dayjs(value?.from).isSame(dayjs(value.to), "day");
+ const isStartDateIsTheSameMonth = now(value?.from, timezoneName).isSame(now(value?.to, timezoneName), "month");
+ const isSingleDayDateRange = now(value?.from, timezoneName).isSame(now(value?.to, timezoneName), "day");
const createCaptionElement = (currentMonth, handleChange) =>
shouldShowYearPicker && currentMonth
@@ -43,7 +44,7 @@ const RangeDatePicker = ({
}
if (isArray(disabledDays)) {
- return disabledDays.some((_date) => dayjs(_date).isSame(date, "day"));
+ return disabledDays.some((_date) => now(_date, timezoneName).isSame(date, "day"));
}
return false;
@@ -54,7 +55,7 @@ const RangeDatePicker = ({
};
const isDisabledEndDays = (date) => {
- const isDateBeforeStartDate = dayjs(date).isBefore(value?.from, "day");
+ const isDateBeforeStartDate = now(date, timezoneName).isBefore(value?.from, "day");
return isDateDisabledFromOutside(date) || (isDateBeforeStartDate && !isSingleDayDateRange);
};
@@ -148,6 +149,7 @@ RangeDatePicker.propTypes = {
handleStartMonthChange: PropTypes.func,
handleEndMonthChange: PropTypes.func,
handleTodayClick: PropTypes.func,
+ timezoneName: PropTypes.string,
};
export default RangeDatePicker;
diff --git a/src/components/DatePicker/RelativeDateRange.jsx b/src/components/DatePicker/RelativeDateRange.jsx
index 1fa660696..6e0d83e7a 100644
--- a/src/components/DatePicker/RelativeDateRange.jsx
+++ b/src/components/DatePicker/RelativeDateRange.jsx
@@ -1,6 +1,6 @@
-import dayjs from "dayjs";
import PropTypes from "prop-types";
import React from "react";
+import { now, toDate } from "../../helpers/date";
import { Button, Select } from "../..";
const options = {
@@ -113,98 +113,102 @@ export const dateRanges = {
};
const handlers = {
- [options.YESTERDAY]: () => {
- const yesterday = dayjs().subtract(1, "day");
+ [options.YESTERDAY]: (timezone) => {
+ const yesterday = now(null, timezone).subtract(1, "day");
return {
- from: yesterday.startOf("day").toDate(),
- to: yesterday.endOf("day").toDate(),
+ from: toDate(yesterday.startOf("day")),
+ to: toDate(yesterday.endOf("day"), false),
};
},
- [options.TODAY]: () => ({
- from: dayjs().startOf("day").toDate(),
- to: dayjs().endOf("day").toDate(),
- }),
+ [options.TODAY]: (timezone) => {
+ return {
+ from: toDate(now(null, timezone).startOf("day")),
+ to: toDate(now(null, timezone).endOf("day"), false),
+ };
+ },
- [options.LAST_WEEK]: () => {
- const lastWeek = dayjs().subtract(7, "day");
+ [options.LAST_WEEK]: (timezone) => {
+ const lastWeek = now(null, timezone).subtract(7, "day");
return {
- from: lastWeek.startOf("week").toDate(),
- to: lastWeek.endOf("week").toDate(),
+ from: toDate(lastWeek.startOf("week")),
+ to: toDate(lastWeek.endOf("week"), false),
};
},
- [options.TRAILING_WEEK]: () => {
+ [options.TRAILING_WEEK]: (timezone) => {
return {
- from: dayjs().subtract(7, "day").startOf("dat").toDate(),
- to: dayjs().subtract(1, "day").endOf("day").toDate(),
+ from: toDate(now(null, timezone).subtract(7, "day").startOf("day")),
+ to: toDate(now(null, timezone).subtract(1, "day").endOf("day"), false),
};
},
- [options.THIS_WEEK]: () => ({
- from: dayjs().startOf("week").toDate(),
- to: dayjs().endOf("week").toDate(),
- }),
+ [options.THIS_WEEK]: (timezone) => {
+ return {
+ from: toDate(now(null, timezone).startOf("week")),
+ to: toDate(now(null, timezone).endOf("week"), false),
+ };
+ },
- [options.LAST_MONTH]: () => {
- const lastMonth = dayjs().subtract(1, "month");
+ [options.LAST_MONTH]: (timezone) => {
+ const lastMonth = now(null, timezone).subtract(1, "month");
return {
- from: lastMonth.startOf("month").toDate(),
- to: lastMonth.endOf("month").toDate(),
+ from: toDate(lastMonth.startOf("month")),
+ to: toDate(lastMonth.endOf("month"), false),
};
},
- [options.TRAILING_MONTH]: () => {
+ [options.TRAILING_MONTH]: (timezone) => {
return {
- from: dayjs().subtract(1, "month").startOf("day").toDate(),
- to: dayjs().subtract(1, "day").endOf("day").toDate(),
+ from: toDate(now(null, timezone).subtract(1, "month").startOf("day")),
+ to: toDate(now(null, timezone).subtract(1, "day").endOf("day"), false),
};
},
- [options.THIS_MONTH]: () => ({
- from: dayjs().startOf("month").toDate(),
- to: dayjs().endOf("month").toDate(),
+ [options.THIS_MONTH]: (timezone) => ({
+ from: toDate(now(null, timezone).startOf("month")),
+ to: toDate(now(null, timezone).endOf("month"), false),
}),
- [options.LAST_QUARTER]: () => {
+ [options.LAST_QUARTER]: (timezone) => {
return {
- from: dayjs().startOf("month").subtract(3, "month").toDate(),
- to: dayjs().startOf("month").subtract(1, "day").toDate(),
+ from: toDate(now(null, timezone).startOf("quarter").subtract(3, "month").startOf("month")),
+ to: toDate(now(null, timezone).endOf("quarter").subtract(3, "month").endOf("month"), false),
};
},
- [options.TRAILING_QUARTER]: () => {
+ [options.TRAILING_QUARTER]: (timezone) => {
return {
- from: dayjs().subtract(3, "month").startOf("day").toDate(),
- to: dayjs().subtract(1, "day").endOf("day").toDate(),
+ from: toDate(now(null, timezone).subtract(3, "month").startOf("day")),
+ to: toDate(now(null, timezone).subtract(1, "day").endOf("day"), false),
};
},
- [options.THIS_QUARTER]: () => {
+ [options.THIS_QUARTER]: (timezone) => {
return {
- from: dayjs().startOf("Q").toDate(),
- to: dayjs().endOf("Q").toDate(),
+ from: toDate(now(null, timezone).startOf("Q")),
+ to: toDate(now(null, timezone).endOf("Q"), false),
};
},
- [options.LAST_YEAR]: () => {
- const lastYear = dayjs().subtract(1, "year");
+ [options.LAST_YEAR]: (timezone) => {
+ const lastYear = now(null, timezone).subtract(1, "year");
return {
- from: lastYear.startOf("year").toDate(),
- to: lastYear.endOf("year").toDate(),
+ from: toDate(lastYear.startOf("year")),
+ to: toDate(lastYear.endOf("year"), false),
};
},
- [options.TRAILING_YEAR]: () => {
+ [options.TRAILING_YEAR]: (timezone) => {
return {
- from: dayjs().subtract(1, "year").startOf("day").toDate(),
- to: dayjs().subtract(1, "day").endOf("day").toDate(),
+ from: toDate(now(null, timezone).subtract(1, "year").startOf("day")),
+ to: toDate(now(null, timezone).subtract(1, "day").endOf("day"), false),
};
},
- [options.THIS_YEAR]: () => ({
- from: dayjs().startOf("year").toDate(),
- to: dayjs().endOf("year").toDate(),
+ [options.THIS_YEAR]: (timezone) => ({
+ from: toDate(now(null, timezone).startOf("year")),
+ to: toDate(now(null, timezone).endOf("year"), false),
}),
};
@@ -215,10 +219,11 @@ export const RelativeDateRange = ({
showApply = true,
onChange,
onSubmit,
+ timezoneName,
}) => {
const handleChange = (e) => {
const rangeName = e.target.value;
- const range = handlers[rangeName]();
+ const range = handlers[rangeName](timezoneName);
onChange(rangeName, range);
};
@@ -252,4 +257,5 @@ RelativeDateRange.propTypes = {
// eslint-disable-next-line react/boolean-prop-naming
showApply: PropTypes.bool,
value: PropTypes.string,
+ timezoneName: PropTypes.string,
};
diff --git a/src/helpers/date.js b/src/helpers/date.js
index 06cb79a0d..1cf1fe063 100644
--- a/src/helpers/date.js
+++ b/src/helpers/date.js
@@ -1,13 +1,31 @@
-import dayjs from "dayjs";
+import dayjs, { isDayjs } from "dayjs";
import LocalizedFormat from "dayjs/plugin/localizedFormat";
import customParseFormat from "dayjs/plugin/customParseFormat";
import quarterOfYear from "dayjs/plugin/quarterOfYear";
+import utc from "dayjs/plugin/utc";
+import timezone from "dayjs/plugin/timezone";
dayjs.extend(customParseFormat);
dayjs.extend(LocalizedFormat);
dayjs.extend(quarterOfYear);
+dayjs.extend(utc);
+dayjs.extend(timezone);
-export const formatDate = (date, format = "YYYY-MM-DD") => {
+export const DateFormat = {
+ DATE_ISO: "YYYY-MM-DD",
+};
+
+export const isValidTimeZoneName = (timezoneName) => {
+ try {
+ dayjs.tz(new Date(), timezoneName);
+ } catch {
+ return false;
+ }
+
+ return true;
+};
+
+export const formatDate = (date, format = DateFormat.DATE_ISO) => {
return dayjs(date).format(format);
};
@@ -19,3 +37,58 @@ export const formatTime = (time, format = "h:mm a") => {
export const dateFromObjectId = (id) => {
return dayjs(new Date(Number.parseInt(id.slice(0, 8), 16) * 1000));
};
+
+export const now = (date, timezone) => {
+ if (!date) {
+ return timezone ? dayjs().tz(timezone).startOf("day") : dayjs();
+ }
+
+ if (typeof date === "number") {
+ const timestamp = date <= 2_147_483_647 ? date * 1000 : date;
+ return timezone ? dayjs(timestamp).tz(timezone) : dayjs(timestamp);
+ }
+
+ if (typeof date === "string") {
+ return timezone ? dayjs(date).tz(timezone) : dayjs();
+ }
+
+ if (isDayjs(date)) {
+ // We do this late because under some conditions this is expensive (see: X2-9122)
+ return date;
+ }
+
+ if (date instanceof Date && !Number.isNaN(date.getTime())) {
+ return timezone ? dayjs.tz(dateToString(date), timezone) : dayjs(dateToString(date));
+ }
+
+ return timezone ? dayjs().tz(timezone) : dayjs();
+};
+
+const padNumber = (value) => value.toString().padStart(2, "0");
+
+export const dateToString = (date) => {
+ const dateString = `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}`;
+ const timeString = `${padNumber(date.getHours())}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}`;
+ return `${dateString} ${timeString}`;
+};
+
+const DayTimeStart = "T00:00:00";
+const DayTimeEnd = "T23:59:59";
+
+export const toDate = (date, isStartDate = true) => {
+ const suffix = isStartDate ? DayTimeStart : DayTimeEnd;
+
+ if (isDayjs(date)) {
+ return new Date(date.format(DateFormat.DATE_ISO) + suffix);
+ }
+
+ return new Date(formatDate(date) + suffix);
+};
+
+export const isSame = (date1, date2, unit = "day") => {
+ if (isDayjs(date1) && isDayjs(date2)) {
+ return date1.isSame(date2, unit);
+ }
+
+ return false;
+};
diff --git a/src/stories/DataDisplay/DateRangePicker.stories.js b/src/stories/DataDisplay/DateRangePicker.stories.js
index 7017e8558..f77a1e8f3 100644
--- a/src/stories/DataDisplay/DateRangePicker.stories.js
+++ b/src/stories/DataDisplay/DateRangePicker.stories.js
@@ -35,7 +35,10 @@ const DateRangePickerStories = {
},
};
-const today = dayjs("2022-10-10").toDate();
+const today = dayjs.tz("2022-10-10").toDate();
+const handleSubmitDateRange = (e) => {
+ console.log("handleSubmitDateRange", { event: e });
+}
export const Default = () => {
const [value, setValue] = useState({ from: new Date("2022-02-03"), to: new Date("2022-03-08") });
@@ -54,7 +57,25 @@ export const RelativeDateRanges = () => {
value={value}
variant="range"
onChange={setValue}
- onSubmitDateRange={console.log}
+ onSubmitDateRange={handleSubmitDateRange}
+ />
+
+ );
+};
+
+export const RelativeDateRangesWithTimeZone = () => {
+ const [value, setValue] = useState({ from: new Date("2022-03-03"), to: new Date("2022-04-08") });
+
+ return (
+