diff --git a/app/page.tsx b/app/page.tsx
index cdcaef8b..73930723 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -30,6 +30,7 @@ export default function Playground() {
const [placeholder, setPlaceholder] = useState("");
const [separator, setSeparator] = useState("~");
const [i18n, setI18n] = useState("en");
+ const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
const [disabled, setDisabled] = useState(false);
const [inputClassName, setInputClassName] = useState("");
const [containerClassName, setContainerClassName] = useState("");
@@ -112,6 +113,7 @@ export default function Playground() {
separator={separator}
startFrom={dateIsValid(new Date(startFrom)) ? new Date(startFrom) : null}
i18n={i18n}
+ timezone={timezone}
disabled={disabled}
inputClassName={inputClassName}
containerClassName={containerClassName}
@@ -361,6 +363,20 @@ export default function Playground() {
))}
+
+
+
+ {
+ setTimezone(e.target.value);
+ }}
+ />
+
diff --git a/src/components/Calendar/Days.tsx b/src/components/Calendar/Days.tsx
index 10a0dbe6..a7dfd9ef 100644
--- a/src/components/Calendar/Days.tsx
+++ b/src/components/Calendar/Days.tsx
@@ -38,17 +38,18 @@ const Days = (props: Props) => {
changeDayHover,
minDate,
maxDate,
- disabledDates
+ disabledDates,
+ timezone
} = useContext(DatepickerContext);
// Functions
const currentDateClass = useCallback(
(day: Date) => {
- if (isCurrentDay(day))
+ if (isCurrentDay(day, timezone))
return TEXT_COLOR["500"][primaryColor as keyof (typeof TEXT_COLOR)["500"]];
return "";
},
- [primaryColor]
+ [primaryColor, timezone]
);
const activeDateData = useCallback(
diff --git a/src/components/Datepicker.tsx b/src/components/Datepicker.tsx
index 5f923306..fd0bc3d4 100644
--- a/src/components/Datepicker.tsx
+++ b/src/components/Datepicker.tsx
@@ -24,6 +24,7 @@ import {
dateUpdateMonth,
dateUpdateYear,
firstDayOfMonth,
+ isValidTimezone,
nextMonthBy,
previousMonthBy
} from "../libs/date";
@@ -74,7 +75,8 @@ const Datepicker = (props: DatepickerType) => {
toggleIcon = undefined,
useRange = true,
- value = null
+ value = null,
+ timezone = undefined
} = props;
// Refs
@@ -224,10 +226,10 @@ const Datepicker = (props: DatepickerType) => {
});
setInputText(
- `${dateFormat(value.startDate, displayFormat, i18n)}${
+ `${dateFormat(value.startDate, displayFormat, i18n, timezone)}${
asSingle
? ""
- : ` ${separator} ${dateFormat(value.endDate, displayFormat, i18n)}`
+ : ` ${separator} ${dateFormat(value.endDate, displayFormat, i18n, timezone)}`
}`
);
}
@@ -241,7 +243,7 @@ const Datepicker = (props: DatepickerType) => {
setInputText("");
}
- }, [asSingle, value, displayFormat, separator, i18n]);
+ }, [asSingle, value, displayFormat, separator, i18n, timezone]);
useEffect(() => {
if (startFrom && dateIsValid(startFrom)) {
@@ -313,6 +315,11 @@ const Datepicker = (props: DatepickerType) => {
/* eslint-enable */
}
+ if (timezone && timezone.trim() !== "" && !isValidTimezone(timezone)) {
+ /* eslint-disable-next-line no-console */
+ console.error(`timezone (${timezone}) is invalid`);
+ }
+
return {
arrowContainer: arrowRef,
asSingle,
@@ -352,7 +359,8 @@ const Datepicker = (props: DatepickerType) => {
toggleClassName,
toggleIcon,
updateFirstDate: (newDate: Date) => firstGotoDate(newDate),
- value
+ value,
+ timezone
};
}, [
minDate,
@@ -386,7 +394,8 @@ const Datepicker = (props: DatepickerType) => {
toggleClassName,
toggleIcon,
value,
- firstGotoDate
+ firstGotoDate,
+ timezone
]);
const containerClassNameOverload = useMemo(() => {
diff --git a/src/components/Shortcuts.tsx b/src/components/Shortcuts.tsx
index 5fbeea23..775e00fd 100644
--- a/src/components/Shortcuts.tsx
+++ b/src/components/Shortcuts.tsx
@@ -1,7 +1,7 @@
import { memo, ReactNode, useCallback, useContext, useMemo } from "react";
import { TEXT_COLOR } from "../constants";
-import DEFAULT_SHORTCUTS from "../constants/shortcuts";
+import { getDefaultShortcuts } from "../constants/shortcuts";
import DatepickerContext from "../contexts/DatepickerContext";
import { dateIsSameOrBefore } from "../libs/date";
import { Period, ShortcutsItem } from "../types";
@@ -88,20 +88,22 @@ ItemTemplate.displayName = "ItemTemplate";
const Shortcuts = () => {
// Contexts
- const { configs } = useContext(DatepickerContext);
+ const { configs, timezone } = useContext(DatepickerContext);
const callPastFunction = useCallback((data: unknown, numberValue: number) => {
return typeof data === "function" ? data(numberValue) : null;
}, []);
const shortcutOptions = useMemo<[string, ShortcutsItem | ShortcutsItem[]][]>(() => {
+ const defaultShortcuts = getDefaultShortcuts(timezone);
+
if (!configs?.shortcuts) {
- return Object.entries(DEFAULT_SHORTCUTS);
+ return Object.entries(defaultShortcuts);
}
return Object.entries(configs.shortcuts).flatMap(([key, customConfig]) => {
- if (Object.prototype.hasOwnProperty.call(DEFAULT_SHORTCUTS, key)) {
- return [[key, DEFAULT_SHORTCUTS[key]]];
+ if (Object.prototype.hasOwnProperty.call(defaultShortcuts, key)) {
+ return [[key, defaultShortcuts[key]]];
}
const { text, period } = customConfig as ShortcutsItem;
@@ -129,7 +131,7 @@ const Shortcuts = () => {
return [];
});
- }, [configs]);
+ }, [configs, timezone]);
const printItemText = useCallback((item: ShortcutsItem) => {
return item?.text ?? null;
diff --git a/src/constants/shortcuts.ts b/src/constants/shortcuts.ts
index 1be42a52..9799cef8 100644
--- a/src/constants/shortcuts.ts
+++ b/src/constants/shortcuts.ts
@@ -1,57 +1,68 @@
-import { dateAdd, endDayOfMonth, firstDayOfMonth, previousMonthBy } from "../libs/date";
+import {
+ dateAdd,
+ endDayOfMonth,
+ firstDayOfMonth,
+ getCurrentDate,
+ previousMonthBy
+} from "../libs/date";
import { ShortcutsItem } from "../types";
-const CURRENT_DATE = new Date();
-
-const DEFAULT_SHORTCUTS: {
+export function getDefaultShortcuts(timezone?: string): {
[key in string]: ShortcutsItem | ShortcutsItem[];
-} = {
- today: {
- text: "Today",
- period: {
- start: CURRENT_DATE,
- end: CURRENT_DATE
- }
- },
- yesterday: {
- text: "Yesterday",
- period: {
- start: dateAdd(CURRENT_DATE, -1, "day"),
- end: dateAdd(CURRENT_DATE, -1, "day")
- }
- },
- past: [
- {
- daysNumber: 7,
- text: "Last 7 days",
+} {
+ const CURRENT_DATE = getCurrentDate(timezone);
+
+ return {
+ today: {
+ text: "Today",
period: {
- start: dateAdd(CURRENT_DATE, -7, "day"),
+ start: CURRENT_DATE,
end: CURRENT_DATE
}
},
- {
- daysNumber: 30,
- text: "Last 30 days",
+ yesterday: {
+ text: "Yesterday",
period: {
- start: dateAdd(CURRENT_DATE, -30, "day"),
- end: CURRENT_DATE
+ start: dateAdd(CURRENT_DATE, -1, "day"),
+ end: dateAdd(CURRENT_DATE, -1, "day")
+ }
+ },
+ past: [
+ {
+ daysNumber: 7,
+ text: "Last 7 days",
+ period: {
+ start: dateAdd(CURRENT_DATE, -7, "day"),
+ end: CURRENT_DATE
+ }
+ },
+ {
+ daysNumber: 30,
+ text: "Last 30 days",
+ period: {
+ start: dateAdd(CURRENT_DATE, -30, "day"),
+ end: CURRENT_DATE
+ }
+ }
+ ],
+ currentMonth: {
+ text: "This month",
+ period: {
+ start: firstDayOfMonth(CURRENT_DATE),
+ end: endDayOfMonth(CURRENT_DATE)
+ }
+ },
+ pastMonth: {
+ text: "Last month",
+ period: {
+ start: firstDayOfMonth(previousMonthBy(CURRENT_DATE)),
+ end: endDayOfMonth(previousMonthBy(CURRENT_DATE))
}
}
- ],
- currentMonth: {
- text: "This month",
- period: {
- start: firstDayOfMonth(CURRENT_DATE),
- end: endDayOfMonth(CURRENT_DATE)
- }
- },
- pastMonth: {
- text: "Last month",
- period: {
- start: firstDayOfMonth(previousMonthBy(CURRENT_DATE)),
- end: endDayOfMonth(previousMonthBy(CURRENT_DATE))
- }
- }
-};
+ };
+}
+
+// Keep backward compatibility
+const DEFAULT_SHORTCUTS = getDefaultShortcuts();
export default DEFAULT_SHORTCUTS;
diff --git a/src/contexts/DatepickerContext.ts b/src/contexts/DatepickerContext.ts
index 1110859f..9226c5d9 100644
--- a/src/contexts/DatepickerContext.ts
+++ b/src/contexts/DatepickerContext.ts
@@ -67,6 +67,7 @@ interface DatepickerStore {
toggleClassName?: ((className: string) => string) | string | null;
toggleIcon?: (open: boolean) => ReactNode;
+ timezone?: string;
updateFirstDate: (date: Date) => void;
@@ -120,6 +121,7 @@ const DatepickerContext = createContext({
toggleClassName: "",
toggleIcon: undefined,
+ timezone: undefined,
updateFirstDate: () => {},
diff --git a/src/libs/date.ts b/src/libs/date.ts
index d5887a0f..3ef4a452 100644
--- a/src/libs/date.ts
+++ b/src/libs/date.ts
@@ -3,6 +3,8 @@ import isBetween from "dayjs/plugin/isBetween";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
import isToday from "dayjs/plugin/isToday";
+import timezone from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
import { LANGUAGE } from "../constants";
import { DateType, WeekDaysIndexType, WeekStringType } from "../types";
@@ -11,6 +13,8 @@ dayjs.extend(isBetween);
dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);
dayjs.extend(isToday);
+dayjs.extend(utc);
+dayjs.extend(timezone);
export function loadLanguageModule(language = LANGUAGE) {
switch (language) {
@@ -449,9 +453,23 @@ export function dateIsValid(date: DateType) {
return dayjs(date).isValid();
}
-export function isCurrentDay(date: Date) {
+export function isCurrentDay(date: Date, timezone?: string) {
if (!dateIsValid(date)) return false;
+ if (timezone) {
+ try {
+ // Format the date as YYYY-MM-DD and parse it in the target timezone
+ const dateStr = dayjs(date).format("YYYY-MM-DD");
+ const dateInTimezone = dayjs.tz(dateStr, timezone);
+ const todayInTimezone = dayjs().tz(timezone);
+ return dateInTimezone.isSame(todayInTimezone, "day");
+ } catch {
+ // If timezone is invalid, fall back to local timezone comparison
+ /* eslint-disable-next-line no-console */
+ console.warn(`Invalid timezone: ${timezone}. Falling back to local timezone.`);
+ return dayjs(date).isSame(dayjs(), "day");
+ }
+ }
return dayjs(date).isToday();
}
@@ -504,9 +522,19 @@ export function dateIsBetween(
);
}
-export function dateFormat(date: DateType, format: string, local = "en") {
+export function dateFormat(date: DateType, format: string, local = "en", timezone?: string) {
if (!dateIsValid(date)) return null;
+ if (timezone) {
+ try {
+ return dayjs(date).locale(local).tz(timezone).format(format);
+ } catch {
+ // If timezone is invalid, fall back to local timezone
+ /* eslint-disable-next-line no-console */
+ console.warn(`Invalid timezone: ${timezone}. Falling back to local timezone.`);
+ return dayjs(date).locale(local).format(format);
+ }
+ }
return dayjs(date).locale(local).format(format);
}
@@ -669,3 +697,27 @@ export function getNextDates(date: Date, limit: number) {
return nexDates;
}
+
+export function isValidTimezone(timezone: string): boolean {
+ try {
+ dayjs().tz(timezone);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function getCurrentDate(timezone?: string) {
+ if (timezone) {
+ try {
+ // Get current time in the specified timezone, but set to start of day
+ return dayjs().tz(timezone).startOf("day").toDate();
+ } catch {
+ // If timezone is invalid, fall back to local timezone
+ /* eslint-disable-next-line no-console */
+ console.warn(`Invalid timezone: ${timezone}. Falling back to local timezone.`);
+ return new Date();
+ }
+ }
+ return new Date();
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index cac5be8b..7158c720 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -92,6 +92,7 @@ export interface DatepickerType {
startWeekOn?: WeekStringType;
popoverDirection?: PopoverDirectionType;
required?: boolean;
+ timezone?: string;
}
export type ColorKeys = (typeof COLORS)[number]; // "blue" | "orange"