diff --git a/src/app/queries/projects.ts b/src/app/queries/projects.ts index 1f30106..10f4261 100644 --- a/src/app/queries/projects.ts +++ b/src/app/queries/projects.ts @@ -32,18 +32,25 @@ export function useYear(yearId: string) { }); } +export const PROJECTS_PAGE_SIZE = 250; + export function useProjects( yearId: string, kind?: 'project' | 'idea', group?: string, search?: string, + cursor?: string, ) { - const query = new URLSearchParams({year: yearId, limit: '50'}); + const query = new URLSearchParams({ + year: yearId, + limit: String(PROJECTS_PAGE_SIZE), + }); if (kind) query.set('kind', kind); if (group) query.set('group', group); if (search) query.set('q', search); + if (cursor) query.set('cursor', cursor); return useQuery({ - queryKey: ['projects', yearId, kind, group, search], + queryKey: ['projects', yearId, kind, group, search, cursor ?? null], queryFn: () => apiRequest(`/projects?${query}`), placeholderData: keepPreviousData, }); diff --git a/src/app/routes/ProjectsPage.tsx b/src/app/routes/ProjectsPage.tsx index 3f25485..6aed01d 100644 --- a/src/app/routes/ProjectsPage.tsx +++ b/src/app/routes/ProjectsPage.tsx @@ -1,4 +1,4 @@ -import {useEffect, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; import {Link, useParams} from 'wouter'; import type {BallotStatusResponse} from '../../shared/administration'; @@ -37,7 +37,11 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { const [group, setGroup] = useState(''); const [searchInput, setSearchInput] = useState(''); const [search, setSearch] = useState(''); + const [cursor, setCursor] = useState(); + const [cursorHistory, setCursorHistory] = useState>([]); const [view, setView] = useState(getProjectsView); + const resultStart = useRef(null); + const paginationRequestPending = useRef(false); const year = useYear(yearId); const ballot = useBallotStatus(yearId, year.data?.year.votingEnabled ?? false); const projects = useProjects( @@ -45,9 +49,27 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { kind, kind === 'project' ? group || undefined : undefined, search || undefined, + cursor, ); const error = year.error ?? projects.error; const voteCategoriesByProject = selectedCategoriesByProject(ballot.data); + const pageProjects = projects.data?.projects ?? []; + const nextCursor = projects.data?.nextCursor ?? null; + const pageOffset = cursor ? Number(cursor) : 0; + const pageStart = pageOffset + 1; + const pageEnd = pageOffset + pageProjects.length; + const showPagination = Boolean(cursor || nextCursor); + const pageStatus = projects.isPlaceholderData + ? 'loading page…' + : pageProjects.length + ? `showing ${pageStart}–${pageEnd}${nextCursor ? '+' : ''}` + : `no ${kind === 'idea' ? 'ideas' : 'projects'} found on this page`; + + const resetPagination = () => { + paginationRequestPending.current = false; + setCursor(undefined); + setCursorHistory([]); + }; useEffect(() => { const timeout = window.setTimeout(() => { @@ -56,6 +78,22 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { return () => window.clearTimeout(timeout); }, [searchInput]); + useEffect(() => { + resetPagination(); + }, [yearId, search]); + + useEffect(() => { + if ( + !paginationRequestPending.current || + projects.isFetching || + projects.isPlaceholderData + ) { + return; + } + paginationRequestPending.current = false; + resultStart.current?.focus(); + }, [cursor, projects.isFetching, projects.isPlaceholderData]); + return ( {!year.data ? ( @@ -118,13 +156,17 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { value={searchInput} maxLength={100} placeholder="Search titles and descriptions" - onChange={(event) => setSearchInput(event.target.value)} + onChange={(event) => { + paginationRequestPending.current = false; + setSearchInput(event.target.value); + }} /> {search && ( )} {projects.isFetching && ( - + updating… )} @@ -143,13 +188,19 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) {
@@ -160,7 +211,10 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { Group