From a10970d7c9d688f5bcc236153797a6829e8907d1 Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Wed, 2 Feb 2022 14:26:45 +0100 Subject: [PATCH] Event Datepicker (#2428) * new datepicker * dev * dev * dev * fix for version 0.10 * added rounded corners for date range * lint * Commented out some Select.test. * improved date range selection * improved functions with useCallback * improved Select.test.jsx * keyboard navigation * keyboard navigation * added dropdown menu icon * Hide filters on xs, Button to show * check if to far left before right * Filter button text * improved local timezone --- web/src/components/Calender.jsx | 329 ++++++++++++++++++ web/src/components/DatePicker.jsx | 162 +++++++++ web/src/components/RelativeModal.jsx | 13 +- web/src/components/Select.jsx | 307 +++++++++++----- web/src/components/__tests__/Select.test.jsx | 29 +- web/src/hooks/useSearchString.jsx | 3 +- web/src/icons/ArrowLeft.jsx | 18 + web/src/icons/ArrowRight.jsx | 12 + web/src/icons/ArrowRightDouble.jsx | 12 + web/src/routes/Events/components/filter.jsx | 31 +- .../routes/Events/components/filterable.jsx | 10 +- web/src/routes/Events/components/filters.jsx | 56 ++- 12 files changed, 863 insertions(+), 119 deletions(-) create mode 100644 web/src/components/Calender.jsx create mode 100644 web/src/components/DatePicker.jsx create mode 100644 web/src/icons/ArrowLeft.jsx create mode 100644 web/src/icons/ArrowRight.jsx create mode 100644 web/src/icons/ArrowRightDouble.jsx diff --git a/web/src/components/Calender.jsx b/web/src/components/Calender.jsx new file mode 100644 index 000000000..8d8c72e13 --- /dev/null +++ b/web/src/components/Calender.jsx @@ -0,0 +1,329 @@ +import { h } from 'preact'; +import { useEffect, useState, useCallback, useMemo, useRef } from 'preact/hooks'; +import ArrowRight from '../icons/ArrowRight'; +import ArrowRightDouble from '../icons/ArrowRightDouble'; + +const todayTimestamp = new Date().setHours(0, 0, 0, 0).valueOf(); + +const Calender = ({ onChange, calenderRef, close }) => { + const keyRef = useRef([]); + + const date = new Date(); + const year = date.getFullYear(); + const month = date.getMonth(); + + const daysMap = useMemo(() => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], []); + const monthMap = useMemo( + () => [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ], + [] + ); + + const [state, setState] = useState({ + getMonthDetails: [], + year, + month, + selectedDay: null, + timeRange: { before: null, after: null }, + monthDetails: null, + }); + + const getNumberOfDays = useCallback((year, month) => { + return 40 - new Date(year, month, 40).getDate(); + }, []); + + const getDayDetails = useCallback( + (args) => { + const date = args.index - args.firstDay; + const day = args.index % 7; + let prevMonth = args.month - 1; + let prevYear = args.year; + if (prevMonth < 0) { + prevMonth = 11; + prevYear--; + } + const prevMonthNumberOfDays = getNumberOfDays(prevYear, prevMonth); + const _date = (date < 0 ? prevMonthNumberOfDays + date : date % args.numberOfDays) + 1; + const month = date < 0 ? -1 : date >= args.numberOfDays ? 1 : 0; + const timestamp = new Date(args.year, args.month, _date).getTime(); + return { + date: _date, + day, + month, + timestamp, + dayString: daysMap[day], + }; + }, + [getNumberOfDays, daysMap] + ); + + const getMonthDetails = useCallback( + (year, month) => { + const firstDay = new Date(year, month).getDay(); + const numberOfDays = getNumberOfDays(year, month); + const monthArray = []; + const rows = 6; + let currentDay = null; + let index = 0; + const 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; + }, + [getNumberOfDays, getDayDetails] + ); + + useEffect(() => { + setState((prev) => ({ ...prev, selectedDay: todayTimestamp, monthDetails: getMonthDetails(year, month) })); + }, [year, month, getMonthDetails]); + + useEffect(() => { + // add refs for keyboard navigation + if (state.monthDetails) { + keyRef.current = keyRef.current.slice(0, state.monthDetails.length); + } + // set today date in focus for keyboard navigation + const todayDate = new Date(todayTimestamp).getDate(); + keyRef.current.find((t) => t.tabIndex === todayDate)?.focus(); + }, [state.monthDetails]); + + const isCurrentDay = (day) => day.timestamp === todayTimestamp; + + const isSelectedRange = useCallback( + (day) => { + if (!state.timeRange.after || !state.timeRange.before) return; + + return day.timestamp < state.timeRange.before && day.timestamp >= state.timeRange.after; + }, + [state.timeRange] + ); + + const isFirstDayInRange = useCallback( + (day) => { + if (isCurrentDay(day)) return; + return state.timeRange.after === day.timestamp; + }, + [state.timeRange.after] + ); + + const isLastDayInRange = useCallback( + (day) => { + return state.timeRange.before === new Date(day.timestamp).setHours(24, 0, 0, 0); + }, + [state.timeRange.before] + ); + + const getMonthStr = useCallback( + (month) => { + return monthMap[Math.max(Math.min(11, month), 0)] || 'Month'; + }, + [monthMap] + ); + + const onDateClick = (day) => { + const { before, after } = state.timeRange; + let timeRange = { before: null, after: null }; + + // user has selected a date < after, reset values + if (after === null || day.timestamp < after) { + timeRange = { before: new Date(day.timestamp).setHours(24, 0, 0, 0), after: day.timestamp }; + } + + // user has selected a date > after + if (after !== null && before !== new Date(day.timestamp).setHours(24, 0, 0, 0) && day.timestamp > after) { + timeRange = { + after, + before: + day.timestamp >= todayTimestamp + ? new Date(todayTimestamp).setHours(24, 0, 0, 0) + : new Date(day.timestamp).setHours(24, 0, 0, 0), + }; + } + + // reset values + if (before === new Date(day.timestamp).setHours(24, 0, 0, 0)) { + timeRange = { before: null, after: null }; + } + + setState((prev) => ({ + ...prev, + timeRange, + selectedDay: day.timestamp, + })); + + if (onChange) { + onChange(timeRange.after ? { before: timeRange.before / 1000, after: timeRange.after / 1000 } : ['all']); + } + }; + + const setYear = useCallback( + (offset) => { + const year = state.year + offset; + const month = state.month; + setState((prev) => { + return { + ...prev, + year, + monthDetails: getMonthDetails(year, month), + }; + }); + }, + [state.year, state.month, getMonthDetails] + ); + + const setMonth = (offset) => { + let year = state.year; + let 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), + }; + }); + }; + + const handleKeydown = (e, day, index) => { + if ((keyRef.current && e.key === 'Enter') || e.keyCode === 32) { + e.preventDefault(); + day.month === 0 && onDateClick(day); + } + if (e.key === 'ArrowLeft') { + index > 0 && keyRef.current[index - 1].focus(); + } + if (e.key === 'ArrowRight') { + index < 41 && keyRef.current[index + 1].focus(); + } + if (e.key === 'ArrowUp') { + e.preventDefault(); + index > 6 && keyRef.current[index - 7].focus(); + } + if (e.key === 'ArrowDown') { + e.preventDefault(); + index < 36 && keyRef.current[index + 7].focus(); + } + if (e.key === 'Escape') { + close(); + } + }; + + const renderCalendar = () => { + const days = + state.monthDetails && + state.monthDetails.map((day, idx) => { + return ( +
onDateClick(day)} + onkeydown={(e) => handleKeydown(e, day, idx)} + ref={(ref) => (keyRef.current[idx] = ref)} + tabIndex={day.month === 0 ? day.date : null} + 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' : '' + } + ${isFirstDayInRange(day) ? ' rounded-l-xl ' : ''} + ${isSelectedRange(day) ? ' bg-blue-600 dark:hover:bg-blue-600' : ''} + ${isLastDayInRange(day) ? ' rounded-r-xl ' : ''} + ${isCurrentDay(day) && !isLastDayInRange(day) ? 'rounded-full bg-gray-100 dark:hover:bg-gray-100 ' : ''}`} + key={idx} + > +
+ {day.date} +
+
+ ); + }); + + return ( +
+
+ {['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'].map((d, i) => ( +
+ {d} +
+ ))} +
+
{days}
+
+ ); + }; + + return ( +
+
+
+
+
setYear(-1)} + > + +
+
+
+
setMonth(-1)} + > + +
+
+
+
{state.year}
+
{getMonthStr(state.month)}
+
+
+
setMonth(1)} + > + +
+
+
setYear(1)}> +
+ +
+
+
+
{renderCalendar()}
+
+
+ ); +}; + +export default Calender; diff --git a/web/src/components/DatePicker.jsx b/web/src/components/DatePicker.jsx new file mode 100644 index 000000000..b751128ae --- /dev/null +++ b/web/src/components/DatePicker.jsx @@ -0,0 +1,162 @@ +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: 'custom_range', + }, +]; + +export default function DatePicker({ + helpText, + keyboardType = 'text', + 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); + } + }, [propValue, setValue, 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 onClick = (e) => { + props.onclick(e); + }; + const labelMoved = isFocused || value !== ''; + + return ( +
+ {props.children} +
+ +
+ {helpText ?
{helpText}
: null} +
+ ); +} diff --git a/web/src/components/RelativeModal.jsx b/web/src/components/RelativeModal.jsx index 7dd1bf76e..5186cc8a6 100644 --- a/web/src/components/RelativeModal.jsx +++ b/web/src/components/RelativeModal.jsx @@ -27,7 +27,7 @@ export default function RelativeModal({ const handleKeydown = useCallback( (event) => { - const focusable = ref.current.querySelectorAll('[tabindex]'); + const focusable = ref.current && ref.current.querySelectorAll('[tabindex]'); if (event.key === 'Tab' && focusable.length) { if (event.shiftKey && document.activeElement === focusable[0]) { focusable[focusable.length - 1].focus(); @@ -69,14 +69,15 @@ export default function RelativeModal({ let newTop = top; let newLeft = left; - // too far right - if (newLeft + width + WINDOW_PADDING >= windowWidth - WINDOW_PADDING) { - newLeft = windowWidth - width - WINDOW_PADDING; - } // too far left - else if (left < WINDOW_PADDING) { + if (left < WINDOW_PADDING) { newLeft = WINDOW_PADDING; } + // too far right + else if (newLeft + width + WINDOW_PADDING >= windowWidth - WINDOW_PADDING) { + newLeft = windowWidth - width - WINDOW_PADDING; + } + // too close to bottom if (top + menuHeight > windowHeight - WINDOW_PADDING + window.scrollY) { newTop = WINDOW_PADDING; diff --git a/web/src/components/Select.jsx b/web/src/components/Select.jsx index 4b9fac7f5..4f7536dc6 100644 --- a/web/src/components/Select.jsx +++ b/web/src/components/Select.jsx @@ -3,74 +3,27 @@ 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, [inputOptions] ); + const [showMenu, setShowMenu] = useState(false); - const [selected, setSelected] = useState( - Math.max( - options.findIndex(({ value }) => value === propSelected), - 0 - ) - ); - const [focused, setFocused] = useState(null); - - const ref = useRef(null); - - const handleSelect = useCallback( - (value, label) => { - setSelected(options.findIndex((opt) => opt.value === value)); - onChange && onChange(value, label); - setShowMenu(false); - }, - [onChange, options] - ); - - const handleClick = useCallback(() => { - setShowMenu(true); - }, [setShowMenu]); - - const handleKeydown = useCallback( - (event) => { - switch (event.key) { - case 'Enter': { - if (!showMenu) { - setShowMenu(true); - setFocused(selected); - } else { - setSelected(focused); - onChange && onChange(options[focused].value, options[focused].label); - setShowMenu(false); - } - break; - } - - case 'ArrowDown': { - const newIndex = focused + 1; - newIndex < options.length && setFocused(newIndex); - break; - } - - case 'ArrowUp': { - const newIndex = focused - 1; - newIndex > -1 && setFocused(newIndex); - break; - } - - // no default - } - }, - [onChange, options, showMenu, setShowMenu, setFocused, focused, selected] - ); - - const handleDismiss = useCallback(() => { - setShowMenu(false); - }, [setShowMenu]); + const [selected, setSelected] = useState(); + const [datePickerValue, setDatePickerValue] = useState(); // Reset the state if the prop value changes useEffect(() => { @@ -85,25 +38,219 @@ export default function Select({ label, onChange, options: inputOptions = [], se // DO NOT include `selected` }, [options, propSelected]); // eslint-disable-line react-hooks/exhaustive-deps - return ( - - - {showMenu ? ( - - {options.map(({ value, label }, i) => ( - - ))} - - ) : null} - + useEffect(() => { + if (type === 'datepicker') { + if ('after' && 'before' in propSelected) { + if (!propSelected.before || !propSelected.after) return setDatePickerValue('all'); + + for (let i = 0; i < inputOptions.length; i++) { + if ( + inputOptions[i].value && + Object.entries(inputOptions[i].value).sort().toString() === Object.entries(propSelected).sort().toString() + ) { + setDatePickerValue(inputOptions[i]?.label); + break; + } else { + setDatePickerValue( + `${new Date(propSelected.after * 1000).toLocaleDateString()} -> ${new Date( + propSelected.before * 1000 - 1 + ).toLocaleDateString()}` + ); + } + } + } + } + if (type === 'dropdown') { + setSelected( + Math.max( + options.findIndex(({ value }) => Object.values(propSelected).includes(value)), + 0 + ) + ); + } + }, [type, options, inputOptions, propSelected, setSelected]); + + const [focused, setFocused] = useState(null); + const [showCalender, setShowCalender] = useState(false); + const calenderRef = useRef(null); + const ref = useRef(null); + + const handleSelect = useCallback( + (value) => { + setSelected(options.findIndex(({ value }) => Object.values(propSelected).includes(value))); + setShowMenu(false); + + //show calender date range picker + if (value === 'custom_range') return setShowCalender(true); + onChange && onChange(value); + }, + [onChange, options, propSelected, setSelected] ); + + const handleDateRange = useCallback( + (range) => { + onChange && onChange(range); + setShowMenu(false); + }, + [onChange] + ); + + const handleClick = useCallback(() => { + setShowMenu(true); + }, [setShowMenu]); + + const handleKeydownDatePicker = useCallback( + (event) => { + switch (event.key) { + case 'Enter': { + if (!showMenu) { + setShowMenu(true); + setFocused(selected); + } else { + setSelected(focused); + if (options[focused].value === 'custom_range') { + setShowMenu(false); + return setShowCalender(true); + } + + onChange && onChange(options[focused].value); + setShowMenu(false); + } + break; + } + + case 'ArrowDown': { + event.preventDefault(); + const newIndex = focused + 1; + newIndex < options.length && setFocused(newIndex); + break; + } + + case 'ArrowUp': { + event.preventDefault(); + const newIndex = focused - 1; + newIndex > -1 && setFocused(newIndex); + break; + } + + // no default + } + }, + [onChange, options, showMenu, setShowMenu, setFocused, focused, selected] + ); + + const handleKeydown = useCallback( + (event) => { + switch (event.key) { + case 'Enter': { + if (!showMenu) { + setShowMenu(true); + setFocused(selected); + } else { + setSelected(focused); + onChange && onChange({ [paramName]: options[focused].value }); + setShowMenu(false); + } + break; + } + + case 'ArrowDown': { + event.preventDefault(); + const newIndex = focused + 1; + newIndex < options.length && setFocused(newIndex); + break; + } + + case 'ArrowUp': { + event.preventDefault(); + const newIndex = focused - 1; + newIndex > -1 && setFocused(newIndex); + break; + } + + // no default + } + }, + [onChange, options, showMenu, setShowMenu, setFocused, focused, selected, paramName] + ); + + const handleDismiss = useCallback(() => { + setShowMenu(false); + }, [setShowMenu]); + + const findDOMNodes = (component) => { + return (component && (component.base || (component.nodeType === 1 && component))) || null; + }; + + useEffect(() => { + const addBackDrop = (e) => { + if (showCalender && !findDOMNodes(calenderRef.current).contains(e.target)) { + setShowCalender(false); + } + }; + window.addEventListener('click', addBackDrop); + + return function cleanup() { + window.removeEventListener('click', addBackDrop); + }; + }, [showCalender]); + + switch (type) { + case 'datepicker': + return ( + + + {showCalender && ( + + setShowCalender(false)} /> + + )} + {showMenu ? ( + + {options.map(({ value, label }, i) => ( + + ))} + + ) : null} + + ); + + // case 'dropdown': + default: + return ( + + + {showMenu ? ( + + {options.map(({ value, label }, i) => ( + + ))} + + ) : null} + + ); + } } diff --git a/web/src/components/__tests__/Select.test.jsx b/web/src/components/__tests__/Select.test.jsx index 5425025cf..00c58d9e3 100644 --- a/web/src/components/__tests__/Select.test.jsx +++ b/web/src/components/__tests__/Select.test.jsx @@ -5,21 +5,40 @@ import { fireEvent, render, screen } from '@testing-library/preact'; describe('Select', () => { test('on focus, shows a menu', async () => { const handleChange = jest.fn(); - render( + ); expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('textbox')); expect(screen.queryByRole('listbox')).toBeInTheDocument(); + expect(screen.queryByRole('option', { name: 'all' })).toBeInTheDocument(); expect(screen.queryByRole('option', { name: 'tacos' })).toBeInTheDocument(); expect(screen.queryByRole('option', { name: 'burritos' })).toBeInTheDocument(); - fireEvent.click(screen.queryByRole('option', { name: 'burritos' })); - expect(handleChange).toHaveBeenCalledWith('burritos', 'burritos'); + fireEvent.click(screen.queryByRole('option', { name: 'tacos' })); + expect(handleChange).toHaveBeenCalledWith({ dinner: 'tacos' }); }); test('allows keyboard navigation', async () => { const handleChange = jest.fn(); - render( + ); expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); const input = screen.getByRole('textbox'); @@ -29,6 +48,6 @@ describe('Select', () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); - expect(handleChange).toHaveBeenCalledWith('burritos', 'burritos'); + expect(handleChange).toHaveBeenCalledWith({ dinner: 'burritos' }); }); }); diff --git a/web/src/hooks/useSearchString.jsx b/web/src/hooks/useSearchString.jsx index 1dde57dcc..b4fdaf3d0 100644 --- a/web/src/hooks/useSearchString.jsx +++ b/web/src/hooks/useSearchString.jsx @@ -18,7 +18,8 @@ export const useSearchString = (limit, searchParams) => { const removeDefaultSearchKeys = useCallback((searchParams) => { searchParams.delete('limit'); searchParams.delete('include_thumbnails'); - searchParams.delete('before'); + // removed deletion of "before" as its used by DatePicker + // searchParams.delete('before'); }, []); return { searchString, setSearchString, removeDefaultSearchKeys }; 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/components/filter.jsx b/web/src/routes/Events/components/filter.jsx index 86d1bcd72..7428a1a03 100644 --- a/web/src/routes/Events/components/filter.jsx +++ b/web/src/routes/Events/components/filter.jsx @@ -1,31 +1,26 @@ import { h } from 'preact'; import Select from '../../../components/Select'; -import { useCallback, useMemo } from 'preact/hooks'; +import { useCallback } from 'preact/hooks'; -const Filter = ({ onChange, searchParams, paramName, options }) => { +function Filter({ onChange, searchParams, paramName, options, ...rest }) { const handleSelect = useCallback( (key) => { const newParams = new URLSearchParams(searchParams.toString()); - if (key !== 'all') { - newParams.set(paramName, key); - } else { - newParams.delete(paramName); - } + Object.keys(key).map((entries) => { + if (key[entries] !== 'all') { + newParams.set(entries, key[entries]); + } else { + paramName.map((p) => newParams.delete(p)); + } + }); onChange(newParams); }, [searchParams, paramName, onChange] ); - const selectOptions = useMemo(() => ['all', ...options], [options]); - - return ( - ; +} export default Filter; diff --git a/web/src/routes/Events/components/filterable.jsx b/web/src/routes/Events/components/filterable.jsx index b23e38eea..35018fadb 100644 --- a/web/src/routes/Events/components/filterable.jsx +++ b/web/src/routes/Events/components/filterable.jsx @@ -3,7 +3,13 @@ import { useCallback, useMemo } from 'preact/hooks'; import Link from '../../../components/Link'; import { route } from 'preact-router'; -const Filterable = ({ onFilter, pathname, searchParams, paramName, name, removeDefaultSearchKeys }) => { +function Filterable({ onFilter, pathname, searchParams, paramName, name }) { + const removeDefaultSearchKeys = useCallback((searchParams) => { + searchParams.delete('limit'); + searchParams.delete('include_thumbnails'); + // searchParams.delete('before'); + }, []); + const href = useMemo(() => { const params = new URLSearchParams(searchParams.toString()); params.set(paramName, name); @@ -27,6 +33,6 @@ const Filterable = ({ onFilter, pathname, searchParams, paramName, name, removeD {name} ); -}; +} export default Filterable; diff --git a/web/src/routes/Events/components/filters.jsx b/web/src/routes/Events/components/filters.jsx index e08b4ea65..91913a6ac 100644 --- a/web/src/routes/Events/components/filters.jsx +++ b/web/src/routes/Events/components/filters.jsx @@ -1,11 +1,13 @@ import { h } from 'preact'; import Filter from './filter'; import { useConfig } from '../../../api'; -import { useMemo } from 'preact/hooks'; +import { useMemo, useState } from 'preact/hooks'; +import { DateFilterOptions } from '../../../components/DatePicker'; +import Button from '../../../components/Button'; const Filters = ({ onChange, searchParams }) => { + const [viewFilters, setViewFilters] = useState(false); const { data } = useConfig(); - const cameras = useMemo(() => Object.keys(data.cameras), [data]); const zones = useMemo( @@ -27,12 +29,52 @@ const Filters = ({ onChange, searchParams }) => { }, data.objects?.track || []) .filter((value, i, self) => self.indexOf(value) === i); }, [data]); - return ( -
- - - +
+ +
+ + + + + + + +
); };