diff --git a/web/src/api/index.jsx b/web/src/api/index.jsx index c433c147e..919ecd60f 100644 --- a/web/src/api/index.jsx +++ b/web/src/api/index.jsx @@ -34,7 +34,27 @@ function reducer(state, { type, payload, meta }) { draftState.queries[url] = { status: ok ? FetchStatus.LOADED : FetchStatus.ERROR, data, fetchId }; }); } + case 'DELETE': { + const { eventId } = payload; + return produce(state, (draftState) => { + Object.keys(draftState.queries).map(function (url, index) { + // If no url or data has no array length then just return state. + if (!(url in draftState.queries) || !draftState.queries[url].data.length) return state; + + //Find the index to remove + const removeIndex = draftState.queries[url].data.map((event) => event.id).indexOf(eventId); + if (removeIndex === -1) return; + + // We need to keep track of deleted items, This will be used to calculate "ReachEnd" for auto load new events. Events.jsx + const totDeleted = state.queries[url].deleted || 0; + + // Splice the deleted index. + draftState.queries[url].data.splice(removeIndex, 1); + draftState.queries[url].deleted = totDeleted + 1; + }); + }); + } default: return state; } @@ -91,8 +111,23 @@ export function useFetch(url, fetchId) { const data = state.queries[url].data || null; const status = state.queries[url].status; + const deleted = state.queries[url].deleted || 0; - return { data, status }; + return { data, status, deleted }; +} + +export function useDelete() { + const { dispatch, state } = useContext(Api); + + async function deleteEvent(eventId) { + if (!eventId) return { success: false }; + + const response = await fetch(`${state.host}/api/events/${eventId}`, { method: 'DELETE' }); + await dispatch({ type: 'DELETE', payload: { eventId } }); + return await (response.status < 300 ? response.json() : { success: true }); + } + + return deleteEvent; } export function useApiHost() { diff --git a/web/src/routes/Event.jsx b/web/src/routes/Event.jsx index bf778ec18..7d3ccaa78 100644 --- a/web/src/routes/Event.jsx +++ b/web/src/routes/Event.jsx @@ -10,7 +10,7 @@ import Dialog from '../components/Dialog'; import Heading from '../components/Heading'; import Link from '../components/Link'; import VideoPlayer from '../components/VideoPlayer'; -import { FetchStatus, useApiHost, useEvent } from '../api'; +import { FetchStatus, useApiHost, useEvent, useDelete } from '../api'; import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table'; export default function Event({ eventId }) { @@ -18,6 +18,7 @@ export default function Event({ eventId }) { const { data, status } = useEvent(eventId); const [showDialog, setShowDialog] = useState(false); const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE); + const setDeleteEvent = useDelete(); const handleClickDelete = () => { setShowDialog(true); @@ -30,8 +31,7 @@ export default function Event({ eventId }) { const handleClickDeleteDialog = useCallback(async () => { let success; try { - const response = await fetch(`${apiHost}/api/events/${eventId}`, { method: 'DELETE' }); - success = await (response.status < 300 ? response.json() : { success: true }); + success = await setDeleteEvent(eventId); setDeleteStatus(success ? FetchStatus.LOADED : FetchStatus.ERROR); } catch (e) { setDeleteStatus(FetchStatus.ERROR); @@ -42,7 +42,7 @@ export default function Event({ eventId }) { setShowDialog(false); route('/events', true); } - }, [apiHost, eventId, setShowDialog]); + }, [eventId, setShowDialog]); if (status !== FetchStatus.LOADED) { return ; @@ -64,7 +64,11 @@ export default function Event({ eventId }) { { meta: { searchString }, payload, } = action; + return produce(state, (draftState) => { draftState.searchStrings[searchString] = true; draftState.events.push(...payload); @@ -56,17 +57,17 @@ export default function Events({ path: pathname, limit = API_LIMIT } = {}) { 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); + 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 < limit) { + if (data && Array.isArray(data) && data.length + deleted < limit) { dispatch({ type: 'REACHED_END', meta: { searchString } }); } - }, [data, limit, searchString, searchStrings]); + }, [data, limit, searchString, searchStrings, deleted]); const [entry, setIntersectNode] = useIntersectionObserver(); @@ -100,7 +101,6 @@ export default function Events({ path: pathname, limit = API_LIMIT } = {}) { ); const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]); - return (
Events