From 019bf00ad4024f6be25a1d92187a15e1139ed787 Mon Sep 17 00:00:00 2001 From: Nick Mowen Date: Tue, 26 Sep 2023 10:34:51 -0600 Subject: [PATCH] Add time picker --- web/src/components/TimePicker.jsx | 260 +++++++----------------------- web/src/routes/Events.jsx | 89 +++++----- 2 files changed, 114 insertions(+), 235 deletions(-) diff --git a/web/src/components/TimePicker.jsx b/web/src/components/TimePicker.jsx index edb39bd52..4a70e9c29 100644 --- a/web/src/components/TimePicker.jsx +++ b/web/src/components/TimePicker.jsx @@ -1,182 +1,18 @@ import { h } from 'preact'; -import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'; +import { useState } from 'preact/hooks'; import { ArrowDropdown } from '../icons/ArrowDropdown'; import { ArrowDropup } from '../icons/ArrowDropup'; +import Heading from './Heading'; -const TimePicker = ({ dateRange, onChange }) => { - const [error, setError] = useState(null); - const [timeRange, setTimeRange] = useState(new Set()); - const [hoverIdx, setHoverIdx] = useState(null); - const [reset, setReset] = useState(false); +const TimePicker = ({ timeRange, onChange }) => { + const times = timeRange.split(','); + const [after, setAfter] = useState(times[0]); + const [before, setBefore] = useState(times[1]); - /** - * Initializes two variables before and after with date objects, - * If they are not null, it creates a new Date object with the value of the property and if not, - * it creates a new Date object with the current hours to 0 and 24 respectively. - */ - const before = useMemo(() => { - return dateRange.before ? new Date(dateRange.before) : new Date(new Date().setHours(24, 0, 0, 0)); - }, [dateRange]); - - const after = useMemo(() => { - return dateRange.after ? new Date(dateRange.after) : new Date(new Date().setHours(0, 0, 0, 0)); - }, [dateRange]); - - useEffect(() => { - /** - * This will reset hours when user selects another date in the calendar. - */ - if (before.getHours() === 0 && after.getHours() === 0 && timeRange.size > 1) return setTimeRange(new Set()); - }, [after, before, timeRange]); - - useEffect(() => { - if (reset || !after) return; - /** - * calculates the number of hours between two dates, by finding the difference in days, - * converting it to hours and adding the hours from the before date. - */ - const days = Math.max(before.getDate() - after.getDate()); - const hourOffset = days * 24; - const beforeOffset = before.getHours() ? hourOffset + before.getHours() : 0; - - /** - * Fills the timeRange by iterating over the hours between 'after' and 'before' during component mount, to keep the selected hours persistent. - */ - for (let hour = after.getHours(); hour < beforeOffset; hour++) { - setTimeRange((timeRange) => timeRange.add(hour)); - } - - /** - * find an element by the id timeIndex- concatenated with the minimum value from timeRange array, - * and if that element is present, it will scroll into view if needed - */ - if (timeRange.size > 1) { - const element = document.getElementById(`timeIndex-${Math.max(...timeRange)}`); - if (element) { - element.scrollIntoViewIfNeeded(true); - } - } - }, [after, before, timeRange, reset]); - - /** - * numberOfDaysSelected is a set that holds the number of days selected in the dateRange. - * The loop iterates through the days starting from the after date's day to the before date's day. - * If the before date's hour is 0, it skips it. - */ - const numberOfDaysSelected = useMemo(() => { - return new Set([...Array(Math.max(1, before.getDate() - after.getDate() + 1))].map((_, i) => after.getDate() + i)); - }, [before, after]); - - if (before.getHours() === 0) numberOfDaysSelected.delete(before.getDate()); - - // Create repeating array with the number of hours for each day selected ...23,24,0,1,2... - const hoursInDays = useMemo(() => { - return Array.from({ length: numberOfDaysSelected.size * 24 }, (_, i) => i % 24); - }, [numberOfDaysSelected]); - - // function for handling the selected time from the provided list - const handleTime = useCallback( - (hour) => { - if (isNaN(hour)) return; - - const _timeRange = new Set([...timeRange]); - _timeRange.add(hour); - - // reset error messages - setError(null); - - /** - * Check if the variable "hour" exists in the "timeRange" set. - * If it does, reset the timepicker - */ - if (timeRange.has(hour)) { - setTimeRange(new Set()); - setReset(true); - const resetBefore = before.setDate(after.getDate() + numberOfDaysSelected.size - 1); - return onChange({ - after: after.setHours(0, 0, 0, 0) / 1000, - before: new Date(resetBefore).setHours(24, 0, 0, 0) / 1000, - }); - } - - //update after - if (_timeRange.size === 1) { - // check if the first selected value is within first day - const firstSelectedHour = Math.ceil(Math.max(..._timeRange)); - if (firstSelectedHour > 23) { - return setError('Select a time on the initial day!'); - } - - // calculate days offset - const dayOffsetAfter = new Date(after).setHours(Math.min(..._timeRange)); - - let dayOffsetBefore = before; - if (numberOfDaysSelected.size === 1) { - dayOffsetBefore = new Date(after).setHours(Math.min(..._timeRange) + 1); - } - - onChange({ - after: dayOffsetAfter / 1000, - before: dayOffsetBefore / 1000, - }); - } - - //update before - if (_timeRange.size > 1) { - let selectedDay = Math.ceil(Math.max(..._timeRange) / 24); - - // if user selects time 00:00 for the next day, add one day - if (hour === 24 && selectedDay === numberOfDaysSelected.size - 1) { - selectedDay += 1; - } - - // Check if end time is on the last day - if (selectedDay !== numberOfDaysSelected.size) { - return setError('Ending must occur on final day!'); - } - - // Check if end time is later than start time - const startHour = Math.min(..._timeRange); - if (hour <= startHour) { - return setError('Ending hour must be greater than start time!'); - } - - // Add all hours between start and end times to the set - for (let x = startHour; x <= hour; x++) { - _timeRange.add(x); - } - - // calculate days offset - const dayOffsetBefore = new Date(dateRange.after); - onChange({ - after: dateRange.after / 1000, - // we add one hour to get full 60min of last selected hour - before: dayOffsetBefore.setHours(Math.max(..._timeRange) + 1) / 1000, - }); - } - - for (let i = 0; i < _timeRange.size; i++) { - setTimeRange((timeRange) => timeRange.add(Array.from(_timeRange)[i])); - } - }, - [after, before, timeRange, dateRange.after, numberOfDaysSelected.size, onChange] - ); - const isSelected = useCallback( - (idx) => { - return !!timeRange.has(idx); - }, - [timeRange] - ); - - const isHovered = useCallback( - (idx) => { - return timeRange.size === 1 && idx > Math.max(...timeRange) && idx <= hoverIdx; - }, - [timeRange, hoverIdx] - ); + // Create repeating array with the number of hours for 1 day ...23,24,0,1,2... + const hoursInDays = Array.from({ length: 24 }, (_, i) => String(i % 24).padStart(2, '0')); // background colors for each day - const isSelectedCss = 'bg-blue-600 transition duration-300 ease-in-out hover:rounded-none'; function randomGrayTone(shade) { const grayTones = [ 'bg-[#212529]/50', @@ -193,44 +29,72 @@ const TimePicker = ({ dateRange, onChange }) => { return grayTones[shade % grayTones.length]; } + const isSelected = (idx, current) => { + return current == `${idx}:00`; + }; + + const isSelectedCss = 'bg-blue-600 transition duration-300 ease-in-out hover:rounded-none'; + const handleTime = (after, before) => { + setAfter(after); + setBefore(before); + onChange(`${after},${before}`); + }; + return ( <> - {error ? {error} : null}
-
-
- {hoursInDays.map((_, idx) => ( -
1 && Math.max(...timeRange) === idx ? 'rounded-b-lg' : ''}`} - onMouseEnter={() => setHoverIdx(idx)} - onMouseLeave={() => setHoverIdx(null)} - > -
+
+ + After + +
+ {hoursInDays.map((time, idx) => ( +
+
handleTime(idx)} - > - {hoursInDays[idx]}:00 + onClick={() => handleTime(`${time}:00`, before)} + > + {hoursInDays[idx]}:00 +
-
- ))} + ))} +
-
- +
+ + Before + +
+ {hoursInDays.map((time, idx) => ( +
+
handleTime(after, `${time}:00`)} + > + {hoursInDays[idx]}:00 +
+
+ ))} +
+
+ +
); diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index c09a83caa..f9b9ea24d 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -48,6 +48,7 @@ const monthsAgo = (num) => { export default function Events({ path, ...props }) { const apiHost = useApiHost(); + const { data: config } = useSWR('config'); const timezone = useMemo(() => config.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [config]); const [searchParams, setSearchParams] = useState({ before: null, @@ -90,14 +91,17 @@ export default function Events({ path, ...props }) { showDeleteFavorite: false, }); - const eventsFetcher = useCallback((path, params) => { - if (searchParams.event) { - path = `${path}/${searchParams.event}`; - return axios.get(path).then((res) => [res.data]); - } - params = { ...params, include_thumbnails: 0, limit: API_LIMIT }; - return axios.get(path, { params }).then((res) => res.data); - }, [searchParams]); + const eventsFetcher = useCallback( + (path, params) => { + if (searchParams.event) { + path = `${path}/${searchParams.event}`; + return axios.get(path).then((res) => [res.data]); + } + params = { ...params, include_thumbnails: 0, limit: API_LIMIT }; + return axios.get(path, { params }).then((res) => res.data); + }, + [searchParams], + ); const getKey = useCallback( (index, prevData) => { @@ -109,13 +113,11 @@ export default function Events({ path, ...props }) { return ['events', searchParams]; }, - [searchParams] + [searchParams], ); const { data: eventPages, mutate, size, setSize, isValidating } = useSWRInfinite(getKey, eventsFetcher); - const { data: config } = useSWR('config'); - const { data: allLabels } = useSWR(['labels']); const { data: allSubLabels } = useSWR(['sub_labels', { split_joined: 1 }]); @@ -134,7 +136,7 @@ export default function Events({ path, ...props }) { labels: Object.values(allLabels || {}), sub_labels: (allSubLabels || []).length > 0 ? [...Object.values(allSubLabels), 'None'] : [], }), - [config, allLabels, allSubLabels] + [config, allLabels, allSubLabels], ); const onSave = async (e, eventId, save) => { @@ -239,7 +241,14 @@ export default function Events({ path, ...props }) { setSearchParams({ ...searchParams, before: dates.before, after: dates.after }); setState({ ...state, showDatePicker: false }); }, - [searchParams, setSearchParams, state, setState] + [searchParams, setSearchParams, state, setState], + ); + + const handleSelectTimeRange = useCallback( + (timeRange) => { + setSearchParams({ ...searchParams, time_range: timeRange }); + }, + [searchParams], ); const onFilter = useCallback( @@ -257,7 +266,7 @@ export default function Events({ path, ...props }) { .join('&'); route(`${path}?${queryString}`); }, - [path, searchParams, setSearchParams] + [path, searchParams, setSearchParams], ); const isDone = (eventPages?.[eventPages.length - 1]?.length ?? 0) < API_LIMIT; @@ -275,7 +284,7 @@ export default function Events({ path, ...props }) { }); if (node) observer.current.observe(node); }, - [size, setSize, isValidating, isDone] + [size, setSize, isValidating, isDone], ); const onSendToPlus = async (id, false_positive, validBox) => { @@ -298,9 +307,9 @@ export default function Events({ path, ...props }) { return { ...event, plus_id: response.data.plus_id }; } return event; - }) + }), ), - false + false, ); } @@ -364,7 +373,7 @@ export default function Events({ path, ...props }) { /> )} {searchParams.event && ( - )} @@ -402,14 +411,17 @@ export default function Events({ path, ...props }) { download /> )} - {(event?.data?.type || "object") == "object" && downloadEvent.end_time && downloadEvent.has_snapshot && !downloadEvent.plus_id && ( - showSubmitToPlus(downloadEvent.id, downloadEvent.label, downloadEvent.box)} - /> - )} + {(event?.data?.type || 'object') == 'object' && + downloadEvent.end_time && + downloadEvent.has_snapshot && + !downloadEvent.plus_id && ( + showSubmitToPlus(downloadEvent.id, downloadEvent.label, downloadEvent.box)} + /> + )} {downloadEvent.plus_id && ( setState({ ...state, showCalendar: false })} > - + @@ -569,7 +578,11 @@ export default function Events({ path, ...props }) {

Confirm deletion of saved event.

-
- {event.zones.length ?
- - {event.zones.join(', ').replaceAll('_', ' ')} -
: null} + {event.zones.length ? ( +
+ + {event.zones.join(', ').replaceAll('_', ' ')} +
+ ) : null}
{(event?.data?.top_score || event.top_score || 0) == 0 @@ -653,7 +668,7 @@ export default function Events({ path, ...props }) {