diff --git a/package-lock.json b/package-lock.json index 1bb68ddc3..c5702e318 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "qs": "^6.7.0", "randomstring": "^1.2.1", "react": "^17.0.2", + "react-calendar": "^3.7.0", "react-country-flag": "^2.3.1", "react-dom": "^17.0.2", "react-hook-form": "^7.15.0", @@ -3749,6 +3750,14 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@wojtekmaj/date-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.0.3.tgz", + "integrity": "sha512-1VPkkTBk07gMR1fjpBtse4G+oJqpmE+0gUFB0dg3VIL7qJmUVaBoD/vlzMm/jNeOPfvlmerl1lpnsZyBUFIRuw==", + "funding": { + "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -9025,6 +9034,14 @@ "node": ">=6" } }, + "node_modules/get-user-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-1.4.0.tgz", + "integrity": "sha512-gQo03lP1OArHLKlnoglqrGGl7b04u2EP9Xutmp72cMdtrrSD7ZgIsCsUKZynYWLDkVJW33Cj3pliP7uP0UonHQ==", + "dependencies": { + "lodash.once": "^4.1.1" + } + }, "node_modules/get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -11842,6 +11859,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "node_modules/lodash.template": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", @@ -12231,6 +12253,14 @@ "readable-stream": "^2.0.1" } }, + "node_modules/merge-class-names": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz", + "integrity": "sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==", + "funding": { + "url": "https://github.com/wojtekmaj/merge-class-names?sponsor=1" + } + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -15562,6 +15592,24 @@ "node": ">=10" } }, + "node_modules/react-calendar": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-3.7.0.tgz", + "integrity": "sha512-zkK95zWLWLC6w3O7p3SHx/FJXEyyD2UMd4jr3CrKD+G73N+G5vEwrXxYQCNivIPoFNBjqoyYYGlkHA+TBDPLCw==", + "dependencies": { + "@wojtekmaj/date-utils": "^1.0.2", + "get-user-locale": "^1.2.0", + "merge-class-names": "^1.1.1", + "prop-types": "^15.6.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-country-flag": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-2.3.1.tgz", @@ -24371,6 +24419,11 @@ "@xtuc/long": "4.2.2" } }, + "@wojtekmaj/date-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.0.3.tgz", + "integrity": "sha512-1VPkkTBk07gMR1fjpBtse4G+oJqpmE+0gUFB0dg3VIL7qJmUVaBoD/vlzMm/jNeOPfvlmerl1lpnsZyBUFIRuw==" + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -28796,6 +28849,14 @@ "pump": "^3.0.0" } }, + "get-user-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-1.4.0.tgz", + "integrity": "sha512-gQo03lP1OArHLKlnoglqrGGl7b04u2EP9Xutmp72cMdtrrSD7ZgIsCsUKZynYWLDkVJW33Cj3pliP7uP0UonHQ==", + "requires": { + "lodash.once": "^4.1.1" + } + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -31078,6 +31139,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.template": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", @@ -31447,6 +31513,11 @@ "readable-stream": "^2.0.1" } }, + "merge-class-names": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz", + "integrity": "sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==" + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -34215,6 +34286,17 @@ "whatwg-fetch": "^3.4.1" } }, + "react-calendar": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-3.7.0.tgz", + "integrity": "sha512-zkK95zWLWLC6w3O7p3SHx/FJXEyyD2UMd4jr3CrKD+G73N+G5vEwrXxYQCNivIPoFNBjqoyYYGlkHA+TBDPLCw==", + "requires": { + "@wojtekmaj/date-utils": "^1.0.2", + "get-user-locale": "^1.2.0", + "merge-class-names": "^1.1.1", + "prop-types": "^15.6.0" + } + }, "react-country-flag": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-2.3.1.tgz", diff --git a/package.json b/package.json index aee3d7a19..926b06736 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "qs": "^6.7.0", "randomstring": "^1.2.1", "react": "^17.0.2", + "react-calendar": "^3.7.0", "react-country-flag": "^2.3.1", "react-dom": "^17.0.2", "react-hook-form": "^7.15.0", diff --git a/src/App.js b/src/App.js index 94b06f238..179777927 100644 --- a/src/App.js +++ b/src/App.js @@ -22,6 +22,7 @@ import { } from "./pages"; import { DropdownDemoPage } from "./pages/Examples/dropdown-demo"; import { ModalPage } from "./pages/Examples/Modal/modal-page"; +import { TimeRangePickerDemo } from "./pages/Examples/TimeRangePickerDemo"; import { TypologyDropdownDemo } from "./pages/Examples/topology-dropdown"; import { RsDemoPage } from "./pages/Examples/rs-demo"; import { TopologyPage as ExamplesTopologyPage } from "./pages/Examples/Topology/topology-page"; @@ -113,6 +114,7 @@ export function App() { } /> } /> + } /> } diff --git a/src/components/TimeRangePicker/RecentlyRanges.js b/src/components/TimeRangePicker/RecentlyRanges.js new file mode 100644 index 000000000..78dbf6652 --- /dev/null +++ b/src/components/TimeRangePicker/RecentlyRanges.js @@ -0,0 +1,35 @@ +import PropTypes from "prop-types"; +import dayjs from "dayjs"; +import { displayTimeFormat } from "./rangeOptions"; + +export const RecentlyRanges = ({ recentRanges, applyTimeRange }) => ( +
+
+ Recently used absolute ranges: +
+
+ {recentRanges?.map((range) => ( + + ))} +
+
+); + +RecentlyRanges.propTypes = { + recentRanges: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.string)), + applyTimeRange: PropTypes.func +}; + +RecentlyRanges.defaultProps = { + recentRanges: [], + applyTimeRange: () => {} +}; diff --git a/src/components/TimeRangePicker/TimePickerCalendar.js b/src/components/TimeRangePicker/TimePickerCalendar.js new file mode 100644 index 000000000..c4c34306e --- /dev/null +++ b/src/components/TimeRangePicker/TimePickerCalendar.js @@ -0,0 +1,52 @@ +import PropTypes from "prop-types"; +import Calendar from "react-calendar"; +import "./index.css"; +import { GrClose } from "react-icons/gr"; +import { FaAngleLeft, FaAngleRight } from "react-icons/fa"; + +export const TimePickerCalendar = ({ + calendarValue, + onChangeCalendarRange, + setShowCalendar +}) => ( +
+
+
Select a time range
+
+ +
+
+
+ } + prevLabel={} + className="react-calendar-custom" + tileClassName="react-calendar-custom__tile" + value={calendarValue} + onChange={onChangeCalendarRange} + /> +
+
+); + +TimePickerCalendar.propTypes = { + calendarValue: PropTypes.arrayOf(PropTypes.instanceOf(Date)), + onChangeCalendarRange: PropTypes.func, + setShowCalendar: PropTypes.func +}; + +TimePickerCalendar.defaultProps = { + calendarValue: null, + onChangeCalendarRange: () => {}, + setShowCalendar: () => {} +}; diff --git a/src/components/TimeRangePicker/TimePickerInput.js b/src/components/TimeRangePicker/TimePickerInput.js new file mode 100644 index 000000000..d18bc40b6 --- /dev/null +++ b/src/components/TimeRangePicker/TimePickerInput.js @@ -0,0 +1,59 @@ +import PropTypes from "prop-types"; +import { FaRegCalendarAlt } from "react-icons/fa"; +import { FiAlertTriangle } from "react-icons/fi"; +import clsx from "clsx"; +import "./index.css"; + +export const TimePickerInput = ({ + inputValue, + setInputValue, + setShowCalendar, + error +}) => ( +
+
+
+ setInputValue(e.target.value)} + onClick={() => setShowCalendar(false)} + className={clsx( + "px-1 py-0.5 border border-gray-300 rounded-sm focus:border-blue-50 focus:outline-none focus:ring-2 focus:ring-blue-300", + { "border-red-300": error } + )} + /> +
+
+ +
+
+ {error && ( +
+
+ +
+
{error}
+
+ )} +
+); + +TimePickerInput.propTypes = { + inputValue: PropTypes.string, + setInputValue: PropTypes.func, + error: PropTypes.string, + setShowCalendar: PropTypes.func +}; + +TimePickerInput.defaultProps = { + inputValue: "", + setInputValue: () => {}, + error: null, + setShowCalendar: () => {} +}; diff --git a/src/components/TimeRangePicker/TimeRangeList.js b/src/components/TimeRangePicker/TimeRangeList.js new file mode 100644 index 000000000..3eb7f0675 --- /dev/null +++ b/src/components/TimeRangePicker/TimeRangeList.js @@ -0,0 +1,80 @@ +import PropTypes from "prop-types"; +import { v4 as uuidv4 } from "uuid"; +import clsx from "clsx"; +import { useCallback } from "react"; +import { rangeOptions } from "./rangeOptions"; + +export const TimeRangeList = ({ + closePicker, + currentRange, + changeRangeValue, + setShowCalendar +}) => { + const isChecked = useCallback((option, value) => { + if (!option || !value) { + return false; + } + + return option.from === value.from && option.to === value.to; + }, []); + + const setOption = useCallback( + (option) => { + const { from, to } = option; + changeRangeValue({ + from, + to + }); + closePicker(); + setShowCalendar(false); + }, + [changeRangeValue, closePicker, setShowCalendar] + ); + + return ( +
+ {rangeOptions.map((option) => { + const id = uuidv4(); + return ( + + ); + })} +
+ ); +}; + +TimeRangeList.propTypes = { + closePicker: PropTypes.func, + currentRange: PropTypes.objectOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]) + ), + changeRangeValue: PropTypes.func, + setShowCalendar: PropTypes.func +}; + +TimeRangeList.defaultProps = { + closePicker: () => {}, + currentRange: {}, + changeRangeValue: () => {}, + setShowCalendar: () => {} +}; diff --git a/src/components/TimeRangePicker/TimeRangePicker.js b/src/components/TimeRangePicker/TimeRangePicker.js new file mode 100644 index 000000000..4a07f881c --- /dev/null +++ b/src/components/TimeRangePicker/TimeRangePicker.js @@ -0,0 +1,94 @@ +import PropTypes from "prop-types"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { FiClock } from "react-icons/fi"; +import { MdOutlineKeyboardArrowDown } from "react-icons/md"; +import clsx from "clsx"; +import dayjs from "dayjs"; +import { TimeRangePickerBody } from "./TimeRangePickerBody"; +import "./index.css"; +import { areDatesSame, convertRangeValue, createDisplayValue } from "./helpers"; + +export const TimeRangePicker = ({ onChange, from, to }) => { + const [isPickerOpen, setIsPickerOpen] = useState(false); + const pickerRef = useRef(); + const [sentRange, setSentRange] = useState(null); + const [memoRange, setMemoRange] = useState(null); + + const currentRange = useMemo(() => { + if ( + sentRange && + areDatesSame(from, sentRange.from) && + areDatesSame(to, sentRange.to) + ) { + return memoRange; + } + return { from, to }; + }, [from, to]); + + const updateDisplayValue = useMemo( + () => createDisplayValue(currentRange), + [currentRange] + ); + + const changeRangeValue = useCallback( + (range) => { + const { from, to } = range; + setMemoRange({ from, to }); + setSentRange({ + from: convertRangeValue(from, "jsDate"), + to: convertRangeValue(to, "jsDate") + }); + onChange( + convertRangeValue(from, "jsDate"), + convertRangeValue(to, "jsDate") + ); + }, + [onChange] + ); + + return ( +
+ + setIsPickerOpen(false)} + currentRange={currentRange} + changeRangeValue={changeRangeValue} + /> +
+ ); +}; + +TimeRangePicker.propTypes = { + onChange: PropTypes.func, + from: PropTypes.shape({}), + to: PropTypes.shape({}) +}; + +TimeRangePicker.defaultProps = { + onChange: (from, to) => { + console.log("FROM: ", from, "\n", "TO: ", to); + }, + from: dayjs(new Date()).subtract(1, "h").toDate(), + to: new Date() +}; diff --git a/src/components/TimeRangePicker/TimeRangePickerBody.js b/src/components/TimeRangePicker/TimeRangePickerBody.js new file mode 100644 index 000000000..2239b1144 --- /dev/null +++ b/src/components/TimeRangePicker/TimeRangePickerBody.js @@ -0,0 +1,219 @@ +import PropTypes from "prop-types"; +import clsx from "clsx"; +import { useCallback, useEffect, useState } from "react"; +import dayjs from "dayjs"; +import { TimeRangeList } from "./TimeRangeList"; +import "./index.css"; +import { storage, createValueForInput, convertRangeValue } from "./helpers"; +import { RecentlyRanges } from "./RecentlyRanges"; +import { displayTimeFormat } from "./rangeOptions"; +import { TimePickerCalendar } from "./TimePickerCalendar"; +import { TimePickerInput } from "./TimePickerInput"; + +export const TimeRangePickerBody = ({ + isOpen, + closePicker, + currentRange, + changeRangeValue, + pickerRef +}) => { + const [recentRanges, setRecentRanges] = useState( + storage.getItem("timePickerRanges") || [] + ); + const [showCalendar, setShowCalendar] = useState(false); + const [calendarValue, setCalendarValue] = useState(null); + const [inputValueFrom, setInputValueFrom] = useState( + createValueForInput(currentRange.from) + ); + const [inputValueTo, setInputValueTo] = useState( + createValueForInput(currentRange.to) + ); + const [errorInputFrom, setErrorInputFrom] = useState(null); + const [errorInputTo, setErrorInputTo] = useState(null); + + const changeRecentRangesList = useCallback( + (range) => { + if ( + !recentRanges.find( + (el) => + dayjs(el.from).toISOString() === dayjs(range.from).toISOString() && + dayjs(el.to).toISOString() === dayjs(range.to).toISOString() + ) + ) { + let newRanges; + if (recentRanges.length < 4) { + newRanges = [range, ...recentRanges]; + } else { + newRanges = [range, ...recentRanges.slice(0, 3)]; + } + setRecentRanges([...newRanges]); + storage.setItem("timePickerRanges", newRanges); + } + }, + [recentRanges] + ); + + const onChangeCalendarRange = useCallback((value) => { + const from = dayjs(value[0]).format(displayTimeFormat); + const to = dayjs(value[1]).format(displayTimeFormat); + + setCalendarValue(value); + setInputValueFrom(from); + setInputValueTo(to); + }, []); + + const applyTimeRange = useCallback( + (range) => { + changeRangeValue(range); + if (dayjs(range.from).isValid() && dayjs(range.to).isValid()) { + changeRecentRangesList({ + from: dayjs(range.from).toISOString(), + to: dayjs(range.to).toISOString() + }); + } + setShowCalendar(false); + closePicker(); + }, + [changeRangeValue, changeRecentRangesList, closePicker] + ); + + const confirmValidRange = useCallback( + (range) => { + if (!errorInputFrom && !errorInputTo) { + applyTimeRange(range); + } + }, + [applyTimeRange, errorInputFrom, errorInputTo] + ); + + const validateInputRange = useCallback((range) => { + const from = convertRangeValue(range.from, "jsDate"); + const to = convertRangeValue(range.to, "jsDate"); + if (!dayjs(from).isValid()) { + setErrorInputFrom("Invaid date!"); + } else { + setErrorInputFrom(null); + } + if (!dayjs(to).isValid()) { + setErrorInputTo("Invaid date!"); + } else { + setErrorInputTo(null); + } + if (dayjs(from).isValid() && dayjs(to).isValid()) { + if (from > to) { + setErrorInputFrom('"From" can\'t be after "To"'); + } else { + setErrorInputFrom(null); + } + } + }, []); + + useEffect(() => { + if ( + dayjs(currentRange.from).isValid() && + dayjs(currentRange.to).isValid() + ) { + setCalendarValue([ + convertRangeValue(currentRange.from, "jsDate"), + convertRangeValue(currentRange.to, "jsDate") + ]); + } else { + setCalendarValue(null); + } + setInputValueFrom(createValueForInput(currentRange.from)); + setInputValueTo(createValueForInput(currentRange.to)); + }, [currentRange]); + + useEffect(() => { + validateInputRange({ from: inputValueFrom, to: inputValueTo }); + }, [inputValueFrom, inputValueTo]); + + const pickerLeft = pickerRef?.current?.getBoundingClientRect()?.left || 0; + + return ( +
600 } + )} + > +
+ +
+
+
+
Absolute time range
+
+
+
+
From
+ +
+
+
To
+ +
+
+ +
+
+ +
+
+ +
+
+ ); +}; + +TimeRangePickerBody.propTypes = { + isOpen: PropTypes.bool, + closePicker: PropTypes.func, + currentRange: PropTypes.shape({}), + changeRangeValue: PropTypes.func +}; + +TimeRangePickerBody.defaultProps = { + isOpen: false, + closePicker: () => {}, + currentRange: {}, + changeRangeValue: () => {} +}; diff --git a/src/components/TimeRangePicker/helpers.js b/src/components/TimeRangePicker/helpers.js new file mode 100644 index 000000000..6f6dbe490 --- /dev/null +++ b/src/components/TimeRangePicker/helpers.js @@ -0,0 +1,118 @@ +import dayjs from "dayjs"; +import { getLocalItem, setLocalItem } from "../../utils/storage"; +import { displayTimeFormat } from "./rangeOptions"; + +const rangeRegexp = /^now-\d{1,4}[mhdwMy]$/; + +export const getIntervalData = (interval) => { + if (interval === "now") return [0, "h"]; + if (typeof interval !== "string" || !rangeRegexp.test(interval)) + return "invalid interval"; + const data = interval.replace("now-", ""); + const intervalName = data.slice(-1); + const intervalTime = Number(data.slice(0, -1)); + return [intervalTime, intervalName]; +}; + +export const createIntervalName = (interval, letter) => { + const dictionary = { + m: "minute", + h: "hour", + d: "day", + w: "week", + M: "month", + y: "year" + }; + + return dictionary[letter] + ? `${interval.toString()} ${dictionary[letter]}${interval > 1 ? "s" : ""}` + : "invalid format"; +}; + +export const convertRangeValue = (value, format = "jsDate") => { + if ( + (typeof value === "string" && + dayjs(value).isValid() && + !value.includes("now-")) || + (typeof value !== "string" && dayjs(value).isValid()) + ) { + return format === "jsDate" + ? dayjs(value).toDate() + : format === "iso" + ? dayjs(value).toISOString() + : dayjs(value).format(format); + } + if (format === "jsDate") { + return dayjs() + .subtract(...getIntervalData(value)) + .toDate(); + } + if (format === "iso") { + return dayjs() + .subtract(...getIntervalData(value)) + .toISOString(); + } + if (format === "default") { + return dayjs().subtract(...getIntervalData(value)); + } + return dayjs() + .subtract(...getIntervalData(value)) + .format(format); +}; + +export const createValueForInput = (value) => + dayjs(value).isValid() ? dayjs(value).format(displayTimeFormat) : value; + +export const createDisplayValue = (range) => { + if ( + typeof range.from === "string" && + range.from.includes("now") && + typeof range.to === "string" && + range.to === "now" + ) { + return createIntervalName(...getIntervalData(range.from)); + } + if (dayjs(range.from).isValid() && range.to === "now") { + return `${dayjs(range.from).format(displayTimeFormat)} to now`; + } + if (dayjs(range.from).isValid() && dayjs(range.to).isValid()) { + return `${dayjs(range.from).format(displayTimeFormat)} to ${dayjs( + range.to + ).format(displayTimeFormat)}`; + } + if ( + typeof range.from === "string" && + range.from.includes("now") && + typeof range.to === "string" && + range.to.includes("now") + ) { + return `${convertRangeValue( + range.from, + displayTimeFormat + )} to ${convertRangeValue(range.to, displayTimeFormat)}`; + } + return "invalid date"; +}; + +export const areDatesSame = (...dates) => { + const date1 = dayjs(dates[0]); + const date2 = dayjs(dates[1]); + return date1.diff(date2) < 1500; +}; + +export const storage = { + setItem: (name, item) => { + if (item) { + const jsonItem = JSON.stringify(item); + setLocalItem(name, jsonItem); + } + }, + + getItem: (name) => { + const item = getLocalItem(name); + if (item && item !== "undefined") { + return JSON.parse(item); + } + return null; + } +}; diff --git a/src/components/TimeRangePicker/index.css b/src/components/TimeRangePicker/index.css new file mode 100644 index 000000000..a57093c94 --- /dev/null +++ b/src/components/TimeRangePicker/index.css @@ -0,0 +1,151 @@ +/*w-fit not work in tailwind */ +.time-picker-main { + width: fit-content; +} + +.time-range-picker-widget { + width: fit-content; +} + +/* rotate-180 not work in tailwind */ +.timepicker-arrow-indicator.active { + transform: rotate(180deg); +} + +.time-range-picker-body { + top: calc(100% + 2px); + width: 480px; + height: 384px; +} + +.calendar-wrapper { + top: -1px; + right: calc(100% + 1px); +} + +.calendar-wrapper.calendarRight { + right: -21px; +} + + +.input-range-box { + width: fit-content; +} + +.error-range-box { + width: fit-content; +} + +.error-range-box:before { + content: ""; + position: absolute; + left: 10px; + top: -4px; + width: 0px; + height: 0px; + border-width: 0px 4px 4px; + border-color: transparent transparent #f9ccde; + border-style: solid; +} + +/*calendar*/ +.react-calendar-custom__tile { + color: #000; + background-color: #f9fafb; + font-size: 14px; + border: 1px solid transparent; +} + +.react-calendar-custom__tile:hover { + position: relative; +} + +.react-calendar-custom { + z-index: 40; + background-color: #f9fafb; + width: 268px; + font-size: 14px; +} + +.react-calendar__navigation { + display: flex; +} + +.react-calendar__navigation__label, +.react-calendar__navigation__arrow, +.react-calendar__navigation { + padding-top: 4px; + background-color: inherit; + color: #000; + border: 0; + font-weight: 500; +} + +.react-calendar__month-view__weekdays { + background-color: inherit; + text-align: center; + color: #230e99; +} + +.react-calendar__month-view__weekdays abbr { + border: 0; + text-decoration: none; + cursor: default; + display: block; + padding: 4px 0 4px 0; + } + +.react-calendar__month-view__days { + background-color: inherit; +} + +.react-calendar__tile, +.react-calendar__tile--now { + margin-bottom: 4px; + background-color: inherit; + height: 26px; +} + +.react-calendar__navigation__label, +.react-calendar__navigation > button:focus, +.time-picker-calendar-tile:focus { + outline: 0; +} + +.react-calendar__tile--active, +.react-calendar__tile--active:hover { + color: #fff; + font-weight: 400; + background: rgb(61, 113, 217); + box-shadow: none; + border: 0px; +} + +.react-calendar__tile--rangeEnd, +.react-calendar__tile--rangeStart { + padding: 0; + border: 0px; + color: #fff; + font-weight: 400; + background: rgb(61, 113, 217); +} + +.react-calendar__tile--rangeEnd abbr, +.react-calendar__tile--rangeStart abbr { + background-color: rgb(61, 113, 217); + border-radius: 100px; + display: block; + padding-top: 2px; + height: 26px; +} + +.react-calendar__tile--rangeStart { + border-top-left-radius: 20px; + border-bottom-left-radius: 20px; +} + +.react-calendar__tile--rangeEnd { + border-top-right-radius: 20px; + border-bottom-right-radius: 20px; +} + diff --git a/src/components/TimeRangePicker/index.js b/src/components/TimeRangePicker/index.js new file mode 100644 index 000000000..900a2ae52 --- /dev/null +++ b/src/components/TimeRangePicker/index.js @@ -0,0 +1 @@ +export { TimeRangePicker } from "./TimeRangePicker"; diff --git a/src/components/TimeRangePicker/rangeOptions.js b/src/components/TimeRangePicker/rangeOptions.js new file mode 100644 index 000000000..2d8c94f49 --- /dev/null +++ b/src/components/TimeRangePicker/rangeOptions.js @@ -0,0 +1,26 @@ +export const displayTimeFormat = "YYYY-MM-DD HH:mm"; + +export const rangeOptions = [ + { display: "10 minutes", from: "now-10m", to: "now" }, + { display: "30 minutes", from: "now-30m", to: "now" }, + { display: "1 hour", from: "now-1h", to: "now" }, + { display: "2 hours", from: "now-2h", to: "now" }, + { display: "3 hours", from: "now-3h", to: "now" }, + { display: "4 hours", from: "now-4h", to: "now" }, + { display: "5 hours", from: "now-5h", to: "now" }, + { display: "6 hours", from: "now-6h", to: "now" }, + { display: "12 hours", from: "now-12h", to: "now" }, + { display: "24 hours", from: "now-24h", to: "now" }, + { display: "2 days", from: "now-2d", to: "now" }, + { display: "3 days", from: "now-3d", to: "now" }, + { display: "4 days", from: "now-4d", to: "now" }, + { display: "5 days", from: "now-5d", to: "now" }, + { display: "6 days", from: "now-6d", to: "now" }, + { display: "7 days", from: "now-7d", to: "now" }, + { display: "2 weeks", from: "now-2w", to: "now" }, + { display: "3 weeks", from: "now-3w", to: "now" }, + { display: "1 month", from: "now-1M", to: "now" }, + { display: "3 months", from: "now-3M", to: "now" }, + { display: "6 months", from: "now-6M", to: "now" }, + { display: "1 year", from: "now-1y", to: "now" } +]; diff --git a/src/pages/Examples/TimeRangePickerDemo.js b/src/pages/Examples/TimeRangePickerDemo.js new file mode 100644 index 000000000..238becd9f --- /dev/null +++ b/src/pages/Examples/TimeRangePickerDemo.js @@ -0,0 +1,24 @@ +import { useState } from "react"; +import { SearchLayout } from "../../components/Layout"; +import { TimeRangePicker } from "../../components/TimeRangePicker"; + +export const TimeRangePickerDemo = () => { + const [value, setValue] = useState([new Date(), new Date()]); + // const [value2, setValue2] = useState([new Date(), new Date()]); + const onChange = (from, to) => { + setValue([from, to]); + }; + // const onChange2 = (from, to) => { + // setValue2([from, to]); + // }; + + return ( + +
+ + {/* */} + +
+
+ ); +};