import { h } 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'; 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 } = useEvents(searchString); useEffect(() => { if (data && !(searchString in searchStrings)) { dispatch({ type: 'APPEND_EVENTS', payload: data, meta: { searchString } }); } if (data && Array.isArray(data) && data.length < limit) { dispatch({ type: 'REACHED_END', meta: { searchString } }); } }, [data, limit, searchString, searchStrings]); 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 (