Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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("");
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -361,6 +363,20 @@ export default function Playground() {
))}
</select>
</div>

<div className="mb-2">
<label className="block" htmlFor="timezone">
Timezone
</label>
<input
className="rounded border px-4 py-2 w-full border-gray-200"
id="timezone"
value={timezone}
onChange={e => {
setTimezone(e.target.value);
}}
/>
</div>
</div>

<div className="w-full sm:w-1/3 pr-2 flex flex-col">
Expand Down
7 changes: 4 additions & 3 deletions src/components/Calendar/Days.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
21 changes: 15 additions & 6 deletions src/components/Datepicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
dateUpdateMonth,
dateUpdateYear,
firstDayOfMonth,
isValidTimezone,
nextMonthBy,
previousMonthBy
} from "../libs/date";
Expand Down Expand Up @@ -74,7 +75,8 @@ const Datepicker = (props: DatepickerType) => {
toggleIcon = undefined,

useRange = true,
value = null
value = null,
timezone = undefined
} = props;

// Refs
Expand Down Expand Up @@ -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)}`
}`
);
}
Expand All @@ -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)) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -352,7 +359,8 @@ const Datepicker = (props: DatepickerType) => {
toggleClassName,
toggleIcon,
updateFirstDate: (newDate: Date) => firstGotoDate(newDate),
value
value,
timezone
};
}, [
minDate,
Expand Down Expand Up @@ -386,7 +394,8 @@ const Datepicker = (props: DatepickerType) => {
toggleClassName,
toggleIcon,
value,
firstGotoDate
firstGotoDate,
timezone
]);

const containerClassNameOverload = useMemo(() => {
Expand Down
14 changes: 8 additions & 6 deletions src/components/Shortcuts.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -129,7 +131,7 @@ const Shortcuts = () => {

return [];
});
}, [configs]);
}, [configs, timezone]);

const printItemText = useCallback((item: ShortcutsItem) => {
return item?.text ?? null;
Expand Down
101 changes: 56 additions & 45 deletions src/constants/shortcuts.ts
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions src/contexts/DatepickerContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ interface DatepickerStore {

toggleClassName?: ((className: string) => string) | string | null;
toggleIcon?: (open: boolean) => ReactNode;
timezone?: string;

updateFirstDate: (date: Date) => void;

Expand Down Expand Up @@ -120,6 +121,7 @@ const DatepickerContext = createContext<DatepickerStore>({

toggleClassName: "",
toggleIcon: undefined,
timezone: undefined,

updateFirstDate: () => {},

Expand Down
56 changes: 54 additions & 2 deletions src/libs/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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();
}
Loading