event component added to events list. New delete reducer

This commit is contained in:
Bernt Christian Egeland 2021-08-21 11:18:42 +02:00
parent 2d7d766b0b
commit 5e82da8ee3

View File

@ -1,10 +1,11 @@
import { h } from 'preact'; import { h, Fragment } from 'preact';
import ActivityIndicator from '../components/ActivityIndicator'; import ActivityIndicator from '../components/ActivityIndicator';
import Heading from '../components/Heading'; import Heading from '../components/Heading';
import Link from '../components/Link'; import Link from '../components/Link';
import Select from '../components/Select'; import Select from '../components/Select';
import produce from 'immer'; import produce from 'immer';
import { route } from 'preact-router'; import { route } from 'preact-router';
import Event from './Event';
import { useIntersectionObserver } from '../hooks'; import { useIntersectionObserver } from '../hooks';
import { FetchStatus, useApiHost, useConfig, useEvents } from '../api'; import { FetchStatus, useApiHost, useConfig, useEvents } from '../api';
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from '../components/Table'; import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from '../components/Table';
@ -12,9 +13,20 @@ import { useCallback, useEffect, useMemo, useReducer, useState } from 'preact/ho
const API_LIMIT = 25; const API_LIMIT = 25;
const initialState = Object.freeze({ events: [], reachedEnd: false, searchStrings: {} }); const initialState = Object.freeze({ events: [], reachedEnd: false, searchStrings: {}, deleted: 0 });
const reducer = (state = initialState, action) => { const reducer = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case 'DELETE_EVENT': {
const { deletedId } = action;
return produce(state, (draftState) => {
const idx = draftState.events.findIndex((e) => e.id === deletedId);
if (idx === -1) return state;
draftState.events.splice(idx, 1);
draftState.deleted++;
});
}
case 'APPEND_EVENTS': { case 'APPEND_EVENTS': {
const { const {
meta: { searchString }, meta: { searchString },
@ -54,11 +66,13 @@ function removeDefaultSearchKeys(searchParams) {
export default function Events({ path: pathname, limit = API_LIMIT } = {}) { export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
const apiHost = useApiHost(); const apiHost = useApiHost();
const [{ events, reachedEnd, searchStrings }, dispatch] = useReducer(reducer, initialState); const [{ events, reachedEnd, searchStrings, deleted }, dispatch] = useReducer(reducer, initialState);
const { searchParams: initialSearchParams } = new URL(window.location); const { searchParams: initialSearchParams } = new URL(window.location);
const [viewEvent, setViewEvent] = useState(null);
const [searchString, setSearchString] = useState(`${defaultSearchString(limit)}&${initialSearchParams.toString()}`); const [searchString, setSearchString] = useState(`${defaultSearchString(limit)}&${initialSearchParams.toString()}`);
const { data, status, deleted } = useEvents(searchString); const { data, status, deletedId } = useEvents(searchString);
let scrollToRef = {};
useEffect(() => { useEffect(() => {
if (data && !(searchString in searchStrings)) { if (data && !(searchString in searchStrings)) {
dispatch({ type: 'APPEND_EVENTS', payload: data, meta: { searchString } }); dispatch({ type: 'APPEND_EVENTS', payload: data, meta: { searchString } });
@ -67,7 +81,11 @@ export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
if (data && Array.isArray(data) && data.length + deleted < limit) { if (data && Array.isArray(data) && data.length + deleted < limit) {
dispatch({ type: 'REACHED_END', meta: { searchString } }); dispatch({ type: 'REACHED_END', meta: { searchString } });
} }
}, [data, limit, searchString, searchStrings, deleted]);
if (deletedId) {
dispatch({ type: 'DELETE_EVENT', deletedId });
}
}, [data, limit, searchString, searchStrings, deleted, deletedId]);
const [entry, setIntersectNode] = useIntersectionObserver(); const [entry, setIntersectNode] = useIntersectionObserver();
@ -99,8 +117,13 @@ export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
}, },
[limit, pathname, setSearchString] [limit, pathname, setSearchString]
); );
const viewEventHandler = (id) => {
if (viewEvent === id) return setViewEvent(null);
if (id in scrollToRef) scrollToRef[id].scrollIntoView();
setViewEvent(id);
};
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]); const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
return ( return (
<div className="space-y-4 w-full"> <div className="space-y-4 w-full">
<Heading>Events</Heading> <Heading>Events</Heading>
@ -131,55 +154,73 @@ export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
const end = new Date(parseInt(endTime * 1000, 10)); const end = new Date(parseInt(endTime * 1000, 10));
const ref = i === events.length - 1 ? lastCellRef : undefined; const ref = i === events.length - 1 ? lastCellRef : undefined;
return ( return (
<Tr data-testid={`event-${id}`} key={id}> <Fragment key={id}>
<Td className="w-40"> <Tr data-testid={`event-${id}`} className={`${viewEvent === id ? 'border-none' : ''}`}>
<a href={`/events/${id}`} ref={ref} data-start-time={startTime} data-reached-end={reachedEnd}> <Td className="w-40">
<img <a
width="150" onClick={() => viewEventHandler(id)}
height="150" ref={ref}
style="min-height: 48px; min-width: 48px;" data-start-time={startTime}
src={`${apiHost}/api/events/${id}/thumbnail.jpg`} data-reached-end={reachedEnd}
>
<img
ref={(el) => (scrollToRef[id] = el)}
width="150"
height="150"
className="cursor-pointer"
style="min-height: 48px; min-width: 48px;"
src={`${apiHost}/api/events/${id}/thumbnail.jpg`}
/>
</a>
</Td>
<Td>
<Filterable
onFilter={handleFilter}
pathname={pathname}
searchParams={searchParams}
paramName="camera"
name={camera}
/> />
</a> </Td>
</Td> <Td>
<Td> <Filterable
<Filterable onFilter={handleFilter}
onFilter={handleFilter} pathname={pathname}
pathname={pathname} searchParams={searchParams}
searchParams={searchParams} paramName="label"
paramName="camera" name={label}
name={camera} />
/> </Td>
</Td> <Td>{(score * 100).toFixed(2)}%</Td>
<Td> <Td>
<Filterable <ul>
onFilter={handleFilter} {zones.map((zone) => (
pathname={pathname} <li>
searchParams={searchParams} <Filterable
paramName="label" onFilter={handleFilter}
name={label} pathname={pathname}
/> searchParams={searchString}
</Td> paramName="zone"
<Td>{(score * 100).toFixed(2)}%</Td> name={zone}
<Td> />
<ul> </li>
{zones.map((zone) => ( ))}
<li> </ul>
<Filterable </Td>
onFilter={handleFilter} <Td>{start.toLocaleDateString()}</Td>
pathname={pathname} <Td>{start.toLocaleTimeString()}</Td>
searchParams={searchString} <Td>{end.toLocaleTimeString()}</Td>
paramName="zone" </Tr>
name={zone} {viewEvent === id ? (
/> <Tr className="border-b-1">
</li> <Td colspan="8">
))} <div>
</ul> <Event eventId={viewEvent} close={() => setViewEvent(null)} />
</Td> </div>
<Td>{start.toLocaleDateString()}</Td> </Td>
<Td>{start.toLocaleTimeString()}</Td> </Tr>
<Td>{end.toLocaleTimeString()}</Td> ) : null}
</Tr> </Fragment>
); );
} }
)} )}