From cb396baa2e80e372c6fac429ff5920785f9c4f5f Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Sun, 23 Aug 2026 10:47:15 +0200 Subject: [PATCH 1/3] fix(projects): preserve all filters in URL Make project kind, search, group, award category, video-only, and layout state derive from query parameters so filtered views survive navigation and can be shared. Carry the complete query string through project links and back navigation, while retaining the debounced search request behavior and the stored layout preference as the default when no URL override exists. Cover restoration from a fully filtered URL and preservation across project and idea navigation. --- src/app/routes/ProjectDetailsPage.tsx | 4 +- src/app/routes/ProjectsPage.tsx | 81 ++++++++++++++++----------- test/app/routes.test.tsx | 60 ++++++++++++++------ 3 files changed, 93 insertions(+), 52 deletions(-) diff --git a/src/app/routes/ProjectDetailsPage.tsx b/src/app/routes/ProjectDetailsPage.tsx index eeaf67a..da2e94a 100644 --- a/src/app/routes/ProjectDetailsPage.tsx +++ b/src/app/routes/ProjectDetailsPage.tsx @@ -25,8 +25,8 @@ export function ProjectDetailsPage() { }>(); const [, navigate] = useLocation(); const [searchParams] = useSearchParams(); - const group = searchParams.get('group'); - const projectsHref = `/years/${yearId}/projects${group ? `?group=${encodeURIComponent(group)}` : ''}`; + const projectsSearch = searchParams.toString(); + const projectsHref = `/years/${yearId}/projects${projectsSearch ? `?${projectsSearch}` : ''}`; const project = useProject(projectId); const ballotYearId = project.data?.project.yearId ?? yearId; const ballot = useBallotStatus(ballotYearId, project.data?.project.kind === 'project'); diff --git a/src/app/routes/ProjectsPage.tsx b/src/app/routes/ProjectsPage.tsx index af85667..5ac1938 100644 --- a/src/app/routes/ProjectsPage.tsx +++ b/src/app/routes/ProjectsPage.tsx @@ -36,15 +36,18 @@ function saveProjectsView(view: ProjectsView) { export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { const {yearId} = useParams<{yearId: string}>(); const [searchParams, setSearchParams] = useSearchParams(); - const [kind, setKind] = useState<'project' | 'idea'>('project'); + const [defaultView] = useState(getProjectsView); + const kind = searchParams.get('kind') === 'idea' ? 'idea' : 'project'; const group = searchParams.get('group') ?? ''; - const [category, setCategory] = useState(''); - const [hasVideoOnly, setHasVideoOnly] = useState(false); - const [searchInput, setSearchInput] = useState(''); - const [search, setSearch] = useState(''); + const category = searchParams.get('category') ?? ''; + const hasVideoOnly = searchParams.get('hasVideo') === 'true'; + const searchInput = searchParams.get('q') ?? ''; + const [search, setSearch] = useState(searchInput.trim()); + const viewParam = searchParams.get('view'); + const view: ProjectsView = + viewParam === 'grid' || viewParam === 'list' ? viewParam : defaultView; 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); @@ -80,14 +83,17 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { setCursorHistory([]); }; - const selectGroup = (groupId: string) => { + const setFilter = ( + name: string, + value: string | undefined, + options?: {replace?: boolean}, + ) => { setSearchParams((current) => { const next = new URLSearchParams(current); - if (groupId) next.set('group', groupId); - else next.delete('group'); + if (value) next.set(name, value); + else next.delete(name); return next; - }); - resetPagination(); + }, options); }; useEffect(() => { @@ -99,7 +105,7 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { useEffect(() => { resetPagination(); - }, [yearId, search, group]); + }, [yearId, kind, search, group, category, hasVideoOnly]); useEffect(() => { if ( @@ -135,7 +141,10 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { ? 'browse the finished projects, teams, and award winners.' : 'see what everyone is building, join a team, or share an idea.'}

- +
{(isAdmin || year.data.year.submissionsClosed) && ( @@ -161,6 +170,7 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { data={ballot.data} error={ballot.error} loading={ballot.isLoading} + detailsSearch={searchParams.toString()} /> )}
{ paginationRequestPending.current = false; - setSearchInput(event.target.value); + setFilter('q', event.target.value || undefined, {replace: true}); }} /> {search && ( @@ -187,7 +197,7 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { className="textAction" onClick={() => { resetPagination(); - setSearchInput(''); + setFilter('q', undefined, {replace: true}); setSearch(''); }} > @@ -209,7 +219,7 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) {