From 86f54bb4ffcd007f799fa649dd9f8cf69b1d0d5a Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Sat, 11 Dec 2021 01:14:53 +0100 Subject: [PATCH] improved date range selection --- web/src/components/Calender.jsx | 51 ++++-- web/src/routes/Events.jsx | 316 -------------------------------- 2 files changed, 31 insertions(+), 336 deletions(-) delete mode 100644 web/src/routes/Events.jsx diff --git a/web/src/components/Calender.jsx b/web/src/components/Calender.jsx index e6639cba1..c0cf865ab 100644 --- a/web/src/components/Calender.jsx +++ b/web/src/components/Calender.jsx @@ -35,8 +35,7 @@ const Calender = ({ onChange, calenderRef }) => { year, month, selectedDay: null, - selectedRange: { before: null, after: null }, - selectedRangeBefore: false, + timeRange: { before: null, after: null }, monthDetails: null, }); const getNumberOfDays = useCallback((year, month) => { @@ -104,42 +103,55 @@ const Calender = ({ onChange, calenderRef }) => { return day.timestamp === todayTimestamp; }; - const isSelectedDay = (day) => { - return day.timestamp === state.selectedDay; - }; const isSelectedRange = (day) => { - if (!state.selectedRange.after || !state.selectedRange.before) return; + if (!state.timeRange.after || !state.timeRange.before) return; - return day.timestamp < state.selectedRange.before * 1000 && day.timestamp >= state.selectedRange.after * 1000; + return day.timestamp < state.timeRange.before && day.timestamp >= state.timeRange.after; }; const isFirstDayInRange = (day) => { if (isCurrentDay(day)) return; - return state.selectedRange.after * 1000 === day.timestamp; + return state.timeRange.after === day.timestamp; }; const isLastDayInRange = (day) => { - if (state.selectedRange.after * 1000 === todayTimestamp) return; - return state.selectedRange.before * 1000 === day.timestamp + 86400000; + return state.timeRange.before === new Date(day.timestamp).setHours(24, 0, 0, 0); }; const getMonthStr = (month) => monthMap[Math.max(Math.min(11, month), 0)] || 'Month'; 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 }, - }; + 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, - ...range, + timeRange, selectedDay: day.timestamp, - selectedRangeBefore: !state.selectedRangeBefore, })); if (onChange) { - onChange({ before: range.selectedRange.before, after: range.selectedRange.after }); + onChange(timeRange.after ? { before: timeRange.before / 1000, after: timeRange.after / 1000 } : ['all']); } }; @@ -185,9 +197,8 @@ const Calender = ({ onChange, calenderRef }) => { 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' : '' } - ${isSelectedDay(day) ? 'bg-gray-100 dark:hover:bg-gray-100' : ''} ${isFirstDayInRange(day) ? ' rounded-l-xl ' : ''} - ${isSelectedRange(day) ? ' bg-blue-500 dark:hover:bg-blue-600' : ''} + ${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={index} diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx deleted file mode 100644 index 70816fe00..000000000 --- a/web/src/routes/Events.jsx +++ /dev/null @@ -1,316 +0,0 @@ -import { h, Fragment } from 'preact'; -import ActivityIndicator from '../components/ActivityIndicator'; -import Heading from '../components/Heading'; -import Link from '../components/Link'; -import Select from '../components/Select'; -import produce from 'immer'; -import { route } from 'preact-router'; -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; - -const initialState = Object.freeze({ events: [], reachedEnd: false, searchStrings: {} }); -const reducer = (state = initialState, action) => { - switch (action.type) { - case 'APPEND_EVENTS': { - const { - meta: { searchString }, - payload, - } = action; - - return produce(state, (draftState) => { - draftState.searchStrings[searchString] = true; - draftState.events.push(...payload); - }); - } - - case 'REACHED_END': { - const { - meta: { searchString }, - } = action; - return produce(state, (draftState) => { - draftState.reachedEnd = true; - draftState.searchStrings[searchString] = true; - }); - } - - case 'RESET': - return initialState; - - default: - return state; - } -}; - -const defaultSearchString = (limit) => `include_thumbnails=0&limit=${limit}`; -function removeDefaultSearchKeys(searchParams) { - searchParams.delete('limit'); - searchParams.delete('include_thumbnails'); - // searchParams.delete('before'); -} - -export default function Events({ path: pathname, limit = API_LIMIT } = {}) { - const apiHost = useApiHost(); - const [{ events, reachedEnd, searchStrings }, dispatch] = useReducer(reducer, initialState); - const { searchParams: initialSearchParams } = new URL(window.location); - const [searchString, setSearchString] = useState(`${defaultSearchString(limit)}&${initialSearchParams.toString()}`); - const { data, status, deleted } = useEvents(searchString); - - useEffect(() => { - if (data && !(searchString in searchStrings)) { - dispatch({ type: 'APPEND_EVENTS', payload: data, meta: { searchString } }); - } - - if (data && Array.isArray(data) && data.length + deleted < limit) { - dispatch({ type: 'REACHED_END', meta: { searchString } }); - } - }, [data, limit, searchString, searchStrings, deleted]); - - const [entry, setIntersectNode] = useIntersectionObserver(); - - useEffect(() => { - if (entry && entry.isIntersecting) { - const { startTime } = entry.target.dataset; - const { searchParams } = new URL(window.location); - searchParams.set('before', parseFloat(startTime) - 0.0001); - - setSearchString(`${defaultSearchString(limit)}&${searchParams.toString()}`); - } - }, [entry, limit]); - - const lastCellRef = useCallback( - (node) => { - if (node !== null && !reachedEnd) { - setIntersectNode(node); - } - }, - [setIntersectNode, reachedEnd] - ); - - const handleFilter = useCallback( - (searchParams) => { - dispatch({ type: 'RESET' }); - removeDefaultSearchKeys(searchParams); - setSearchString(`${defaultSearchString(limit)}&${searchParams.toString()}`); - route(`${pathname}?${searchParams.toString()}`); - }, - [limit, pathname, setSearchString] - ); - - const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]); - - return ( -
- Events - - - -
- - - - - - - - - - - - - - {events.map( - ( - { camera, id, label, start_time: startTime, end_time: endTime, thumbnail, top_score: score, zones }, - i - ) => { - const start = new Date(parseInt(startTime * 1000, 10)); - const end = new Date(parseInt(endTime * 1000, 10)); - const ref = i === events.length - 1 ? lastCellRef : undefined; - return ( - - - - - - - - - - - ); - } - )} - - - - - - -
- CameraLabelScoreZonesDateStartEnd
- - - - - - - - {(score * 100).toFixed(2)}% -
    - {zones.map((zone) => ( -
  • - -
  • - ))} -
-
{start.toLocaleDateString()}{start.toLocaleTimeString()}{end.toLocaleTimeString()}
- {status === FetchStatus.LOADING ? : reachedEnd ? 'No more events' : null} -
-
-
- ); -} - -function Filterable({ onFilter, pathname, searchParams, paramName, name }) { - const href = useMemo(() => { - const params = new URLSearchParams(searchParams.toString()); - params.set(paramName, name); - removeDefaultSearchKeys(params); - return `${pathname}?${params.toString()}`; - }, [searchParams, paramName, pathname, name]); - - const handleClick = useCallback( - (event) => { - event.preventDefault(); - route(href, true); - const params = new URLSearchParams(searchParams.toString()); - params.set(paramName, name); - onFilter(params); - }, - [href, searchParams, onFilter, paramName, name] - ); - - return ( - - {name} - - ); -} - -function Filters({ onChange, searchParams }) { - const { data } = useConfig(); - - const cameras = useMemo(() => Object.keys(data.cameras), [data]); - - const zones = useMemo( - () => - Object.values(data.cameras) - .reduce((memo, camera) => { - memo = memo.concat(Object.keys(camera.zones)); - return memo; - }, []) - .filter((value, i, self) => self.indexOf(value) === i), - [data] - ); - - const labels = useMemo(() => { - return Object.values(data.cameras) - .reduce((memo, camera) => { - memo = memo.concat(camera.objects?.track || []); - return memo; - }, data.objects?.track || []) - .filter((value, i, self) => self.indexOf(value) === i); - }, [data]); - - return ( - -
- - - - -
-
- ); -} - -function Filter({ onChange, searchParams, paramName, options, type, ...rest }) { - const handleSelect = useCallback( - (key) => { - const newParams = new URLSearchParams(searchParams.toString()); - 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 obj = {}; - paramName.map((p) => Object.assign(obj, { [p]: searchParams.get(p) }), [searchParams]); - - return ( -