diff --git a/web/src/components/Calender.jsx b/web/src/components/Calender.jsx
new file mode 100644
index 000000000..4505b8283
--- /dev/null
+++ b/web/src/components/Calender.jsx
@@ -0,0 +1,297 @@
+import { h } from 'preact';
+import { useEffect, useState, useRef } from 'preact/hooks';
+import ArrowRight from '../icons/ArrowRight';
+import ArrowRightDouble from '../icons/ArrowRightDouble';
+
+let oneDay = 60 * 60 * 24 * 1000;
+let todayTimestamp = Date.now() - (Date.now() % oneDay) + new Date().getTimezoneOffset() * 1000 * 60;
+
+const Calender = ({ onChange, calenderRef }) => {
+ const inputRef = useRef();
+ // const inputRefs = useRef();
+ let date = new Date();
+ let year = date.getFullYear();
+ let month = date.getMonth();
+
+ const [state, setState] = useState({
+ getMonthDetails: [],
+ year,
+ month,
+ selectedDay: null,
+ selectedRange: { before: null, after: null },
+ selectedRangeBefore: false,
+ monthDetails: null,
+ });
+ const [showDatePicker, setShowDatePicker] = useState(false);
+
+ useEffect(() => {
+ setState((prev) => ({ ...prev, selectedDay: todayTimestamp, monthDetails: getMonthDetails(year, month) }));
+ }, []);
+
+ const daysMap = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+ const monthMap = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+ ];
+
+ const getDayDetails = (args) => {
+ let date = args.index - args.firstDay;
+ let day = args.index % 7;
+ let prevMonth = args.month - 1;
+ let prevYear = args.year;
+ if (prevMonth < 0) {
+ prevMonth = 11;
+ prevYear--;
+ }
+ let prevMonthNumberOfDays = getNumberOfDays(prevYear, prevMonth);
+ let _date = (date < 0 ? prevMonthNumberOfDays + date : date % args.numberOfDays) + 1;
+ let month = date < 0 ? -1 : date >= args.numberOfDays ? 1 : 0;
+ let timestamp = new Date(args.year, args.month, _date).getTime();
+ return {
+ date: _date,
+ day,
+ month,
+ timestamp,
+ dayString: daysMap[day],
+ };
+ };
+
+ const getNumberOfDays = (year, month) => {
+ return 40 - new Date(year, month, 40).getDate();
+ };
+
+ const getMonthDetails = (year, month) => {
+ let firstDay = new Date(year, month).getDay();
+ let numberOfDays = getNumberOfDays(year, month);
+ let monthArray = [];
+ let rows = 6;
+ let currentDay = null;
+ let index = 0;
+ let cols = 7;
+
+ for (let row = 0; row < rows; row++) {
+ for (let col = 0; col < cols; col++) {
+ currentDay = getDayDetails({
+ index,
+ numberOfDays,
+ firstDay,
+ year,
+ month,
+ });
+ monthArray.push(currentDay);
+ index++;
+ }
+ }
+ return monthArray;
+ };
+
+ const isCurrentDay = (day) => {
+ return day.timestamp === todayTimestamp;
+ };
+
+ const isSelectedDay = (day) => {
+ return day.timestamp === state.selectedDay;
+ };
+ const isSelectedRange = (day) => {
+ if (!state.selectedRange.after || !state.selectedRange.before) return;
+
+ return day.timestamp < state.selectedRange.before * 1000 && day.timestamp >= state.selectedRange.after * 1000;
+ };
+
+ const getDateFromDateString = (dateValue) => {
+ const dateData = dateValue.split('-').map((d) => parseInt(d, 10));
+ if (dateData.length < 3) return null;
+
+ const year = dateData[0];
+ const month = dateData[1];
+ const date = dateData[2];
+ return { year, month, date };
+ };
+
+ const getMonthStr = (month) => monthMap[Math.max(Math.min(11, month), 0)] || 'Month';
+
+ const getDateStringFromTimestamp = (timestamp) => {
+ let dateObject = new Date(timestamp);
+ let month = dateObject.getMonth() + 1;
+ let date = dateObject.getDate();
+ return dateObject.getFullYear() + '-' + (month < 10 ? '0' + month : month) + '-' + (date < 10 ? '0' + date : date);
+ };
+
+ const setDate = (dateData) => {
+ let selectedDay = new Date(dateData.year, dateData.month - 1, dateData.date).getTime();
+ setState((prev) => ({ ...prev, selectedDay }));
+
+ if (onChange) {
+ // onChange(selectedDay);
+ }
+ };
+
+ const updateDateFromInput = () => {
+ let dateValue = inputRef.current.value;
+ let dateData = getDateFromDateString(dateValue);
+ if (dateData !== null) {
+ setDate(dateData);
+ setState(
+ (prev = {
+ ...prev,
+ year: dateData.year,
+ month: dateData.month - 1,
+ monthDetails: getMonthDetails(dateData.year, dateData.month - 1),
+ })
+ );
+ }
+ };
+
+ // const setDateToInput = (timestamp) => {
+ // const dateString = getDateStringFromTimestamp(timestamp);
+ // inputRef.current.value = dateString;
+ // };
+
+ const onDateClick = (day) => {
+ const range = {
+ selectedRange: state.selectedRangeBefore
+ ? { ...state.selectedRange, before: new Date(day.timestamp).setHours(24, 0, 0, 0) / 1000 }
+ : { ...state.selectedRange, after: day.timestamp / 1000 },
+ };
+
+ setState((prev) => ({
+ ...prev,
+ ...range,
+ selectedDay: day.timestamp,
+ selectedRangeBefore: !state.selectedRangeBefore,
+ }));
+
+ // setDateToInput(day.timestamp);
+ if (onChange) {
+ onChange([{ before: range.selectedRange.before, after: range.selectedRange.after }]);
+ }
+ };
+
+ const setYear = (offset) => {
+ const year = state.year + offset;
+ const month = state.month;
+ setState((prev) => {
+ return {
+ ...prev,
+ year,
+ monthDetails: getMonthDetails(year, month),
+ };
+ });
+ };
+
+ const setMonth = (offset) => {
+ const year = state.year;
+ const month = state.month + offset;
+ if (month === -1) {
+ month = 11;
+ year--;
+ } else if (month === 12) {
+ month = 0;
+ year++;
+ }
+ setState((prev) => {
+ return {
+ ...prev,
+ year,
+ month,
+ monthDetails: getMonthDetails(year, month),
+ };
+ });
+ };
+
+ /**
+ * Renderers
+ */
+
+ const renderCalendar = () => {
+ let days =
+ state.monthDetails &&
+ state.monthDetails.map((day, index) => {
+ return (
+
onDateClick(day)}
+ className={
+ 'h-12 w-12 float-left flex flex-shrink justify-center items-center cursor-pointer ' +
+ (day.month !== 0 ? ' opacity-50 bg-gray-700 dark:bg-gray-700 pointer-events-none' : '') +
+ (isCurrentDay(day) ? 'rounded-full bg-gray-100 dark:hover:bg-gray-100 ' : '') +
+ (isSelectedDay(day) ? 'bg-gray-100 dark:hover:bg-gray-100' : '') +
+ (isSelectedRange(day) ? ' bg-blue-500 dark:hover:bg-blue-500' : '')
+ }
+ key={index}
+ >
+
+ {day.date}
+
+
+ );
+ });
+
+ return (
+
+
+ {['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'].map((d, i) => (
+
+ {d}
+
+ ))}
+
+
{days}
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
{state.year}
+
{getMonthStr(state.month)}
+
+
+
+
+
{renderCalendar()}
+
+
+ );
+};
+
+export default Calender;
diff --git a/web/src/components/DatePicker.jsx b/web/src/components/DatePicker.jsx
new file mode 100644
index 000000000..54d00bef5
--- /dev/null
+++ b/web/src/components/DatePicker.jsx
@@ -0,0 +1,192 @@
+import { h } from 'preact';
+import { useCallback, useEffect, useState } from 'preact/hooks';
+
+export const DateFilterOptions = [
+ {
+ label: 'All',
+ value: ['all'],
+ },
+ {
+ label: 'Today',
+ value: [
+ {
+ //Before
+ before: new Date().setHours(24, 0, 0, 0) / 1000,
+ //After
+ after: new Date().setHours(0, 0, 0, 0) / 1000,
+ },
+ ],
+ },
+ {
+ label: 'Yesterday',
+ value: [
+ {
+ //Before
+ before: new Date(new Date().setDate(new Date().getDate() - 1)).setHours(24, 0, 0, 0) / 1000,
+ //After
+ after: new Date(new Date().setDate(new Date().getDate() - 1)).setHours(0, 0, 0, 0) / 1000,
+ },
+ ],
+ },
+ {
+ label: 'Last 7 Days',
+ value: [
+ {
+ //Before
+ before: new Date().setHours(24, 0, 0, 0) / 1000,
+ //After
+ after: new Date(new Date().setDate(new Date().getDate() - 7)).setHours(0, 0, 0, 0) / 1000,
+ },
+ ],
+ },
+ {
+ label: 'This Month',
+ value: [
+ {
+ //Before
+ before: new Date().setHours(24, 0, 0, 0) / 1000,
+ //After
+ after: new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime() / 1000,
+ },
+ ],
+ },
+ {
+ label: 'Last Month',
+ value: [
+ {
+ //Before
+ before: new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime() / 1000,
+ //After
+ after: new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1).getTime() / 1000,
+ },
+ ],
+ },
+ {
+ label: 'Custom Range',
+ value: null,
+ },
+];
+
+export default function DatePicker({
+ helpText,
+ keyboardType = 'date',
+ inputRef,
+ label,
+ leadingIcon: LeadingIcon,
+ onBlur,
+ onChangeText,
+ onFocus,
+ readonly,
+ trailingIcon: TrailingIcon,
+ value: propValue = '',
+ ...props
+}) {
+ const [isFocused, setFocused] = useState(false);
+ const [value, setValue] = useState(propValue);
+
+ useEffect(() => {
+ if (propValue !== value) {
+ setValue(propValue);
+ }
+ // DO NOT include `value`
+ }, [propValue, setValue]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ useEffect(() => {
+ setDateToInput(value);
+ }, []);
+
+ const handleFocus = useCallback(
+ (event) => {
+ setFocused(true);
+ onFocus && onFocus(event);
+ },
+ [onFocus]
+ );
+
+ const handleBlur = useCallback(
+ (event) => {
+ setFocused(false);
+ onBlur && onBlur(event);
+ },
+ [onBlur]
+ );
+
+ const handleChange = useCallback(
+ (event) => {
+ const { value } = event.target;
+ setValue(value);
+ onChangeText && onChangeText(value);
+ },
+ [onChangeText, setValue]
+ );
+
+ const getDateFromDateString = (dateValue) => {
+ const dateData = dateValue.split('-').map((d) => parseInt(d, 10));
+ if (dateData.length < 3) return null;
+
+ const year = dateData[0];
+ const month = dateData[1];
+ const date = dateData[2];
+ return { year, month, date };
+ };
+
+ const getDateStringFromTimestamp = (timestamp) => {
+ const dateObject = new Date(timestamp * 1000);
+ const month = dateObject.getMonth() + 1;
+ const date = dateObject.getDate();
+ return dateObject.getFullYear() + '-' + (month < 10 ? '0' + month : month) + '-' + (date < 10 ? '0' + date : date);
+ };
+
+ const setDateClick = (dateData) => {
+ const selectedDay = new Date(dateData.year, dateData.month - 1, dateData.date).getTime();
+ if (props.onchange) {
+ props.onchange(new Date(selectedDay).getTime() / 1000);
+ }
+ };
+
+ const setDateToInput = (timestamp) => {
+ const dateString = getDateStringFromTimestamp(timestamp);
+ // inputRef.current.value = dateString;
+ };
+ const onClick = (e) => {
+ props.onclick(e);
+ };
+ const labelMoved = isFocused || value !== '';
+
+ return (
+
+ );
+}
diff --git a/web/src/components/RelativeModal.jsx b/web/src/components/RelativeModal.jsx
index 2398f643f..7dd1bf76e 100644
--- a/web/src/components/RelativeModal.jsx
+++ b/web/src/components/RelativeModal.jsx
@@ -79,7 +79,7 @@ export default function RelativeModal({
}
// too close to bottom
if (top + menuHeight > windowHeight - WINDOW_PADDING + window.scrollY) {
- newTop = relativeToY - menuHeight;
+ newTop = WINDOW_PADDING;
}
if (top <= WINDOW_PADDING + window.scrollY) {
diff --git a/web/src/components/Select.jsx b/web/src/components/Select.jsx
index 4b9fac7f5..456b68597 100644
--- a/web/src/components/Select.jsx
+++ b/web/src/components/Select.jsx
@@ -3,29 +3,56 @@ import ArrowDropdown from '../icons/ArrowDropdown';
import ArrowDropup from '../icons/ArrowDropup';
import Menu, { MenuItem } from './Menu';
import TextField from './TextField';
+import DatePicker from './DatePicker';
+import Calender from './Calender';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
-export default function Select({ label, onChange, options: inputOptions = [], selected: propSelected }) {
+export default function Select({
+ type,
+ label,
+ onChange,
+ paramName,
+ options: inputOptions = [],
+ selected: propSelected,
+}) {
const options = useMemo(
() =>
- typeof inputOptions[0] === 'string' ? inputOptions.map((opt) => ({ value: opt, label: opt })) : inputOptions,
+ typeof inputOptions[1] === 'string' ? inputOptions.map((opt) => ({ value: opt, label: opt })) : inputOptions,
[inputOptions]
);
- const [showMenu, setShowMenu] = useState(false);
- const [selected, setSelected] = useState(
- Math.max(
- options.findIndex(({ value }) => value === propSelected),
- 0
- )
- );
- const [focused, setFocused] = useState(null);
+ const [showMenu, setShowMenu] = useState(false);
+ const [selected, setSelected] = useState();
+
+ useEffect(() => {
+ setSelected(
+ Math.max(
+ options.findIndex(({ value }) => propSelected.includes(value)),
+ 0
+ )
+ );
+ }, [options, propSelected]);
+
+ const [focused, setFocused] = useState(null);
+ const [showDatePicker, setShowDatePicker] = useState(false);
+ const calenderRef = useRef(null);
const ref = useRef(null);
const handleSelect = useCallback(
(value, label) => {
setSelected(options.findIndex((opt) => opt.value === value));
+ setShowMenu(false);
+
+ if (!value) return setShowDatePicker(true);
onChange && onChange(value, label);
+ },
+ [onChange, options]
+ );
+ const handleDateRange = useCallback(
+ (range) => {
+ // setSelected(options.findIndex((opt) => opt.value === value));
+
+ onChange && onChange(range, 'range');
setShowMenu(false);
},
[onChange, options]
@@ -85,25 +112,80 @@ export default function Select({ label, onChange, options: inputOptions = [], se
// DO NOT include `selected`
}, [options, propSelected]); // eslint-disable-line react-hooks/exhaustive-deps
- return (
-
-
- {showMenu ? (
-
- ) : null}
-
- );
+ useEffect(() => {
+ window.addEventListener('click', addBackDrop);
+ // setDateToInput(state.selectedDay);
+ return function cleanup() {
+ window.removeEventListener('click', addBackDrop);
+ };
+ }, [showDatePicker]);
+
+ const findDOMNode = (component) => {
+ return (component && (component.base || (component.nodeType === 1 && component))) || null;
+ };
+
+ const addBackDrop = (e) => {
+ if (showDatePicker && !findDOMNode(calenderRef.current).contains(e.target)) {
+ setShowDatePicker(false);
+ }
+ };
+
+ switch (type) {
+ case 'datepicker':
+ return (
+
+
+ {showDatePicker && (
+
+ )}
+ {showMenu ? (
+
+ ) : null}
+
+ );
+ case 'dropdown':
+ return (
+
+
+ {showMenu ? (
+
+ ) : null}
+
+ );
+ default:
+ return ;
+ }
}
diff --git a/web/src/icons/ArrowLeft.jsx b/web/src/icons/ArrowLeft.jsx
new file mode 100644
index 000000000..6ff892695
--- /dev/null
+++ b/web/src/icons/ArrowLeft.jsx
@@ -0,0 +1,18 @@
+import { h } from 'preact';
+import { memo } from 'preact/compat';
+
+export function ArrowLeft({ className = '' }) {
+ return (
+
+ );
+}
+
+export default memo(ArrowLeft);
diff --git a/web/src/icons/ArrowRight.jsx b/web/src/icons/ArrowRight.jsx
new file mode 100644
index 000000000..455548887
--- /dev/null
+++ b/web/src/icons/ArrowRight.jsx
@@ -0,0 +1,12 @@
+import { h } from 'preact';
+import { memo } from 'preact/compat';
+
+export function ArrowRight({ className = '' }) {
+ return (
+
+ );
+}
+
+export default memo(ArrowRight);
diff --git a/web/src/icons/ArrowRightDouble.jsx b/web/src/icons/ArrowRightDouble.jsx
new file mode 100644
index 000000000..7487a4d5c
--- /dev/null
+++ b/web/src/icons/ArrowRightDouble.jsx
@@ -0,0 +1,12 @@
+import { h } from 'preact';
+import { memo } from 'preact/compat';
+
+export function ArrowRightDouble({ className = '' }) {
+ return (
+
+ );
+}
+
+export default memo(ArrowRightDouble);
diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx
index e74bbadc4..86a51d20b 100644
--- a/web/src/routes/Events.jsx
+++ b/web/src/routes/Events.jsx
@@ -1,4 +1,4 @@
-import { h } from 'preact';
+import { h, Fragment } from 'preact';
import ActivityIndicator from '../components/ActivityIndicator';
import Heading from '../components/Heading';
import Link from '../components/Link';
@@ -9,6 +9,7 @@ import { useIntersectionObserver } from '../hooks';
import { FetchStatus, useApiHost, useConfig, useEvents } from '../api';
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from '../components/Table';
import { useCallback, useEffect, useMemo, useReducer, useState } from 'preact/hooks';
+import { DateFilterOptions } from '../components/DatePicker';
const API_LIMIT = 25;
@@ -49,7 +50,7 @@ const defaultSearchString = (limit) => `include_thumbnails=0&limit=${limit}`;
function removeDefaultSearchKeys(searchParams) {
searchParams.delete('limit');
searchParams.delete('include_thumbnails');
- searchParams.delete('before');
+ // searchParams.delete('before');
}
export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
@@ -101,6 +102,7 @@ export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
);
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
+
return (
Events
@@ -249,23 +251,34 @@ function Filters({ onChange, searchParams }) {
}, [data]);
return (
-
-
-
-
-
+
+
+
+
+
);
}
-function Filter({ onChange, searchParams, paramName, options }) {
+function Filter({ onChange, searchParams, paramName, options, type, ...rest }) {
const handleSelect = useCallback(
(key) => {
const newParams = new URLSearchParams(searchParams.toString());
- if (key !== 'all') {
- newParams.set(paramName, key);
- } else {
- newParams.delete(paramName);
- }
+ key.map((queryArray) => {
+ if (queryArray[paramName] !== 'all') {
+ for (let query in queryArray) {
+ newParams.set(query, queryArray[query]);
+ }
+ } else {
+ paramName.map((p) => newParams.delete(p));
+ }
+ });
onChange(newParams);
},
@@ -273,13 +286,16 @@ function Filter({ onChange, searchParams, paramName, options }) {
);
const selectOptions = useMemo(() => ['all', ...options], [options]);
+ const selected = useMemo(() => paramName.map((p) => searchParams.get(p) || 'all'));
return (
);
}