diff --git a/pages/index.js b/pages/index.js
index 5ce6034..1a9b3e7 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -11,10 +11,11 @@ export default function Playground() {
endDate: null
});
const [primaryColor, setPrimaryColor] = useState("blue");
- const [useRange, setUseRange] = useState(true);
- const [showFooter, setShowFooter] = useState(false);
+ const [useRange, setUseRange] = useState(false);
+ const [showFooter, setShowFooter] = useState(true);
const [showShortcuts, setShowShortcuts] = useState(false);
- const [asSingle, setAsSingle] = useState(false);
+ const [asTimePicker, setAsTimePicker] = useState(true);
+ const [asSingle, setAsSingle] = useState(true);
const [placeholder, setPlaceholder] = useState("");
const [separator, setSeparator] = useState("~");
const [i18n, setI18n] = useState("en");
@@ -22,7 +23,7 @@ export default function Playground() {
const [inputClassName, setInputClassName] = useState("");
const [containerClassName, setContainerClassName] = useState("");
const [toggleClassName, setToggleClassName] = useState("");
- const [displayFormat, setDisplayFormat] = useState("YYYY-MM-DD");
+ const [displayFormat, setDisplayFormat] = useState("DD/MM/YYYY hh:mm A");
const [readOnly, setReadOnly] = useState(false);
const [minDate, setMinDate] = useState("");
const [maxDate, setMaxDate] = useState("");
@@ -92,6 +93,7 @@ export default function Playground() {
}
}}
asSingle={asSingle}
+ asTimePicker={asTimePicker}
placeholder={placeholder}
separator={separator}
startFrom={
@@ -187,6 +189,20 @@ export default function Playground() {
+
+
+ setAsTimePicker(e.target.checked)}
+ />
+
+
+
= ({
}) => {
// Contexts
const {
+ hour,
+ minute,
+ periodDay,
period,
changePeriod,
changeDayHover,
@@ -57,6 +61,7 @@ const Calendar: React.FC
= ({
changeDatepickerValue,
hideDatepicker,
asSingle,
+ asTimePicker,
i18n,
startWeekOn,
input
@@ -124,12 +129,16 @@ const Calendar: React.FC = ({
const ipt = input?.current;
changeDatepickerValue(
{
- startDate: dayjs(start).format(DATE_FORMAT),
- endDate: dayjs(end).format(DATE_FORMAT)
+ startDate: asTimePicker
+ ? formatDateTimeToISO(start, hour, minute, periodDay)
+ : dayjs(start).format(DATE_FORMAT),
+ endDate: asTimePicker
+ ? formatDateTimeToISO(end, hour, minute, periodDay)
+ : dayjs(end).format(DATE_FORMAT)
},
ipt
);
- hideDatepicker();
+ if (!asTimePicker) hideDatepicker();
}
if (period.start && period.end) {
@@ -185,16 +194,20 @@ const Calendar: React.FC = ({
}
},
[
- asSingle,
- changeDatepickerValue,
- changeDayHover,
- changePeriod,
date,
- hideDatepicker,
- period.end,
period.start,
+ period.end,
showFooter,
- input
+ input,
+ changeDatepickerValue,
+ asTimePicker,
+ hour,
+ minute,
+ periodDay,
+ hideDatepicker,
+ changeDayHover,
+ changePeriod,
+ asSingle
]
);
diff --git a/src/components/Datepicker.tsx b/src/components/Datepicker.tsx
index 54e90b7..f83021a 100644
--- a/src/components/Datepicker.tsx
+++ b/src/components/Datepicker.tsx
@@ -5,11 +5,12 @@ import Calendar from "../components/Calendar";
import Footer from "../components/Footer";
import Input from "../components/Input";
import Shortcuts from "../components/Shortcuts";
+import Time from "../components/Time";
import { COLORS, DATE_FORMAT, DEFAULT_COLOR, LANGUAGE } from "../constants";
import DatepickerContext from "../contexts/DatepickerContext";
import { formatDate, nextMonth, previousMonth } from "../helpers";
import useOnClickOutside from "../hooks";
-import { Period, DatepickerType, ColorKeys } from "../types";
+import { Period, DatepickerType, ColorKeys, PeriodDay } from "../types";
import { Arrow, VerticalDash } from "./utils";
@@ -22,6 +23,7 @@ const Datepicker: React.FC = ({
showShortcuts = false,
configs = undefined,
asSingle = false,
+ asTimePicker = false,
placeholder = null,
separator = "~",
startFrom = null,
@@ -61,6 +63,10 @@ const Datepicker: React.FC = ({
const [inputText, setInputText] = useState("");
const [inputRef, setInputRef] = useState(React.createRef());
+ const [hour, setHour] = useState("1");
+ const [minute, setMinute] = useState("00");
+ const [periodDay, setPeriodDay] = useState("PM");
+
// Custom Hooks use
useOnClickOutside(containerRef, () => {
const container = containerRef.current;
@@ -93,6 +99,14 @@ const Datepicker: React.FC = ({
}
}, []);
+ /* Start Time */
+ const changeHour = useCallback((hour: string) => setHour(hour), []);
+
+ const changeMinute = useCallback((minute: string) => setMinute(minute), []);
+
+ const changePeriodDay = useCallback((periodDay: PeriodDay) => setPeriodDay(periodDay), []);
+ /* End Time */
+
/* Start First */
const firstGotoDate = useCallback(
(date: dayjs.Dayjs) => {
@@ -247,6 +261,7 @@ const Datepicker: React.FC = ({
const contextValues = useMemo(() => {
return {
asSingle,
+ asTimePicker,
primaryColor: safePrimaryColor,
configs,
calendarContainer: calendarContainerRef,
@@ -260,6 +275,12 @@ const Datepicker: React.FC = ({
changeInputText: (newText: string) => setInputText(newText),
updateFirstDate: (newDate: dayjs.Dayjs) => firstGotoDate(newDate),
changeDatepickerValue: onChange,
+ hour,
+ minute,
+ periodDay,
+ changeHour,
+ changeMinute,
+ changePeriodDay,
showFooter,
placeholder,
separator,
@@ -286,6 +307,7 @@ const Datepicker: React.FC = ({
};
}, [
asSingle,
+ asTimePicker,
safePrimaryColor,
configs,
hideDatepicker,
@@ -293,6 +315,12 @@ const Datepicker: React.FC = ({
dayHover,
inputText,
onChange,
+ hour,
+ minute,
+ periodDay,
+ changeHour,
+ changeMinute,
+ changePeriodDay,
showFooter,
placeholder,
separator,
@@ -347,15 +375,18 @@ const Datepicker: React.FC = ({
showShortcuts ? "md:pl-2" : "md:pl-1"
} pr-2 lg:pr-1`}
>
-
+
+
+ {asSingle && asTimePicker && }
+
{useRange && (
<>
@@ -376,7 +407,6 @@ const Datepicker: React.FC = ({
)}
-
{showFooter && }
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
index 40f8fd2..8a84d59 100644
--- a/src/components/Footer.tsx
+++ b/src/components/Footer.tsx
@@ -8,7 +8,7 @@ import { PrimaryButton, SecondaryButton } from "./utils";
const Footer: React.FC = () => {
// Contexts
- const { hideDatepicker, period, changeDatepickerValue, configs, classNames } =
+ const { asTimePicker, period, configs, classNames, changeDatepickerValue, hideDatepicker } =
useContext(DatepickerContext);
// Functions
@@ -34,8 +34,12 @@ const Footer: React.FC = () => {
onClick={() => {
if (period.start && period.end) {
changeDatepickerValue({
- startDate: dayjs(period.start).format(DATE_FORMAT),
- endDate: dayjs(period.end).format(DATE_FORMAT)
+ startDate: asTimePicker
+ ? dayjs(period.start).toISOString()
+ : dayjs(period.start).format(DATE_FORMAT),
+ endDate: asTimePicker
+ ? dayjs(period.end).toISOString()
+ : dayjs(period.start).format(DATE_FORMAT)
});
hideDatepicker();
}
diff --git a/src/components/Input.tsx b/src/components/Input.tsx
index 38fc856..59f3edf 100644
--- a/src/components/Input.tsx
+++ b/src/components/Input.tsx
@@ -25,6 +25,7 @@ const Input: React.FC = (e: Props) => {
hideDatepicker,
changeDatepickerValue,
asSingle,
+ asTimePicker,
placeholder,
separator,
disabled,
@@ -70,7 +71,7 @@ const Input: React.FC = (e: Props) => {
const dates = [];
- if (asSingle) {
+ if (asSingle || asTimePicker) {
const date = parseFormattedDate(inputValue, displayFormat);
if (dateIsValid(date.toDate())) {
dates.push(date.format(DATE_FORMAT));
@@ -114,7 +115,15 @@ const Input: React.FC = (e: Props) => {
changeInputText(e.target.value);
},
- [asSingle, displayFormat, separator, changeDatepickerValue, changeDayHover, changeInputText]
+ [
+ asSingle,
+ asTimePicker,
+ changeInputText,
+ displayFormat,
+ separator,
+ changeDatepickerValue,
+ changeDayHover
+ ]
);
const handleInputKeyDown = useCallback(
diff --git a/src/components/Time.tsx b/src/components/Time.tsx
new file mode 100644
index 0000000..2cd212c
--- /dev/null
+++ b/src/components/Time.tsx
@@ -0,0 +1,148 @@
+import React, { ChangeEvent, useContext } from "react";
+
+import { RING_COLOR } from "../constants";
+import DatepickerContext from "../contexts/DatepickerContext";
+import { classNames as cn, formatDateTimeToISO } from "../helpers";
+import { PeriodDay } from "../types";
+
+const Time: React.FC = () => {
+ // Contexts
+ const {
+ hour,
+ minute,
+ periodDay,
+ period,
+ primaryColor,
+ changeDatepickerValue,
+ changeHour,
+ changeMinute,
+ changePeriodDay
+ } = useContext(DatepickerContext);
+
+ const ringFocusColor = RING_COLOR.focus[primaryColor as keyof typeof RING_COLOR.focus];
+
+ const svgString = `
+
+ `;
+ const dataUri = `data:image/svg+xml;base64,${Buffer.from(svgString).toString("base64")}`;
+
+ const selectClassname = cn(
+ "!bg-[length:0.75rem_0.75rem]",
+ "bg-[right_0.5rem_center]",
+ "!bg-no-repeat !appearance-none !bg-transparent !text-sm !text-center !outline-none !focus:outline-none",
+ "!pl-2 !pr-6 !py-1 rounded-[8px] !w-fit",
+ "!border border-gray-300 focus:border-none",
+ `${ringFocusColor}`
+ );
+
+ const HOURS = Array.from({ length: 12 });
+ const MINUTES = Array.from({ length: 12 });
+
+ const handleChangeHour = (e: ChangeEvent) => {
+ changeHour(e.target.value);
+
+ if (period.start && period.end)
+ changeDatepickerValue({
+ startDate: formatDateTimeToISO(period.start, e.target.value, minute, periodDay),
+ endDate: formatDateTimeToISO(period.end, e.target.value, minute, periodDay)
+ });
+ };
+
+ const handleChangeMinute = (e: ChangeEvent) => {
+ changeMinute(e.target.value);
+
+ if (period.start && period.end)
+ changeDatepickerValue({
+ startDate: formatDateTimeToISO(period.start, hour, e.target.value, periodDay),
+ endDate: formatDateTimeToISO(period.end, hour, e.target.value, periodDay)
+ });
+ };
+
+ const handleChangePeriodDay = (e: ChangeEvent) => {
+ changePeriodDay(e.target.value as PeriodDay);
+
+ if (period.start && period.end)
+ changeDatepickerValue({
+ startDate: formatDateTimeToISO(
+ period.start,
+ hour,
+ minute,
+ e.target.value as PeriodDay
+ ),
+ endDate: formatDateTimeToISO(period.end, hour, minute, e.target.value as PeriodDay)
+ });
+ };
+
+ return (
+
+
+
+ :
+
+
+
+
+ );
+};
+
+export default Time;
diff --git a/src/components/utils.tsx b/src/components/utils.tsx
index 6bdb31a..f403b23 100644
--- a/src/components/utils.tsx
+++ b/src/components/utils.tsx
@@ -4,7 +4,7 @@ import { BG_COLOR, BORDER_COLOR, BUTTON_COLOR, RING_COLOR } from "../constants";
import DatepickerContext from "../contexts/DatepickerContext";
interface IconProps {
- className: string;
+ className?: string;
}
interface Button {
diff --git a/src/constants/index.ts b/src/constants/index.ts
index 190da86..0d3e0ae 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -27,6 +27,8 @@ export const LANGUAGE = "en";
export const DATE_FORMAT = "YYYY-MM-DD";
+export const DATE_TIME_FORMAT = "DD/MM/YYYY hh:mm A";
+
export const START_WEEK = "sun";
export const DATE_LOOKING_OPTIONS = ["forward", "backward", "middle"];
diff --git a/src/contexts/DatepickerContext.ts b/src/contexts/DatepickerContext.ts
index d42aadd..2c36c72 100644
--- a/src/contexts/DatepickerContext.ts
+++ b/src/contexts/DatepickerContext.ts
@@ -10,18 +10,26 @@ import {
DateRangeType,
ClassNamesTypeProp,
PopoverDirectionType,
- ColorKeys
+ ColorKeys,
+ PeriodDay
} from "../types";
interface DatepickerStore {
input?: React.RefObject;
asSingle?: boolean;
+ asTimePicker?: boolean;
primaryColor: ColorKeys;
configs?: Configs;
calendarContainer: React.RefObject | null;
arrowContainer: React.RefObject | null;
hideDatepicker: () => void;
period: Period;
+ hour: string;
+ minute: string;
+ periodDay: PeriodDay;
+ changeHour: (hour: string) => void;
+ changeMinute: (minute: string) => void;
+ changePeriodDay: (periodDay: PeriodDay) => void;
changePeriod: (period: Period) => void;
dayHover: string | null;
changeDayHover: (day: string | null) => void;
@@ -69,6 +77,15 @@ const DatepickerContext = createContext({
inputText: "",
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
changeInputText: text => {},
+ hour: "",
+ minute: "",
+ periodDay: "PM",
+ // eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
+ changeHour: text => {},
+ // eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
+ changeMinute: text => {},
+ // eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
+ changePeriodDay: text => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
updateFirstDate: date => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
diff --git a/src/helpers/index.ts b/src/helpers/index.ts
index d59d919..b2c65f1 100644
--- a/src/helpers/index.ts
+++ b/src/helpers/index.ts
@@ -2,10 +2,12 @@ import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import weekday from "dayjs/plugin/weekday";
+import { DATE_FORMAT, LANGUAGE } from "../constants";
+
dayjs.extend(weekday);
dayjs.extend(customParseFormat);
-import { DATE_FORMAT, LANGUAGE } from "../constants";
+import { PeriodDay } from "types";
export function classNames(...classes: (false | null | undefined | string)[]) {
return classes.filter(Boolean).join(" ");
@@ -626,3 +628,30 @@ export function loadLanguageModule(language = LANGUAGE) {
export function dateIsValid(date: Date | number) {
return date instanceof Date && !isNaN(date.getTime());
}
+
+export function formatDateTimeToISO(
+ dateIncoming: Date | string,
+ hourIncoming: string,
+ minute: string,
+ periodDay: PeriodDay
+): string {
+ // Adjust hour based on period (AM/PM)
+ const calculateHour = (hourIncoming: string, periodDay: PeriodDay): string => {
+ if (periodDay === "PM" && hourIncoming !== String(12))
+ return String(Number(hourIncoming) + 12);
+
+ if (periodDay === "AM" && hourIncoming === String(12)) return "0";
+
+ return hourIncoming;
+ };
+
+ const hour = calculateHour(hourIncoming, periodDay);
+
+ // Create a new Date object and set the components
+ const date = dayjs(dateIncoming).add(Number(hour), "hours").add(Number(minute), "minutes");
+
+ // Format date to ISO 8601
+ const isoString = date.toISOString();
+
+ return isoString;
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index b9e561c..d136804 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -2,6 +2,8 @@ import React from "react";
import { COLORS } from "../constants";
+export type PeriodDay = "AM" | "PM";
+
export interface Period {
start: string | null;
end: string | null;
@@ -62,6 +64,7 @@ export interface DatepickerType {
showShortcuts?: boolean;
configs?: Configs;
asSingle?: boolean;
+ asTimePicker?: boolean;
placeholder?: string;
separator?: string;
startFrom?: Date | null;
diff --git a/tsconfig.json b/tsconfig.json
index cf845ae..0e8307d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -20,7 +20,8 @@
"skipLibCheck": true,
"noEmit": true,
"resolveJsonModule": true,
- "isolatedModules": true
+ "isolatedModules": true,
+ "incremental": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]