Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/app/components/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ export function ProjectCard({
project,
view = 'grid',
voteCategories = [],
detailsSearch,
}: {
project: ProjectSummary;
view?: 'grid' | 'list';
voteCategories?: string[];
detailsSearch?: string;
}) {
const projectLink = `/years/${project.yearId}/projects/${project.id}`;
const projectLink = `/years/${project.yearId}/projects/${project.id}${detailsSearch ? `?${detailsSearch}` : ''}`;

if (view === 'list') {
return (
Expand Down
9 changes: 6 additions & 3 deletions src/app/routes/ProjectDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useState, type ChangeEvent} from 'react';
import {useQuery} from '@tanstack/react-query';
import {Link, useLocation, useParams} from 'wouter';
import {Link, useLocation, useParams, useSearchParams} from 'wouter';

import {QueryState} from '../components/AppLayout';
import {Markdown} from '../components/Markdown';
Expand All @@ -21,6 +21,9 @@ export function ProjectDetailsPage() {
projectId: string;
}>();
const [, navigate] = useLocation();
const [searchParams] = useSearchParams();
const group = searchParams.get('group');
const projectsHref = `/years/${yearId}/projects${group ? `?group=${encodeURIComponent(group)}` : ''}`;
const project = useProject(projectId);
const ballotYearId = project.data?.project.yearId ?? yearId;
const ballot = useBallotStatus(ballotYearId, project.data?.project.kind === 'project');
Expand Down Expand Up @@ -49,7 +52,7 @@ export function ProjectDetailsPage() {
<main className="detailPage">
<header className="detailHero">
<div>
<Link className="backLink" href={`/years/${yearId}/projects`}>
<Link className="backLink" href={projectsHref}>
← {yearId} projects
</Link>
<div className="detailTags">
Expand Down Expand Up @@ -88,7 +91,7 @@ export function ProjectDetailsPage() {
if (!window.confirm('Withdraw this project from the archive?'))
return;
withdraw.mutate(projectId, {
onSuccess: () => navigate(`/years/${yearId}/projects`),
onSuccess: () => navigate(projectsHref),
onError: (error) => setActionError(error.message),
});
}}
Expand Down
23 changes: 16 additions & 7 deletions src/app/routes/ProjectsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {useEffect, useRef, useState} from 'react';
import {Link, useParams} from 'wouter';
import {Link, useParams, useSearchParams} from 'wouter';

import type {BallotStatusResponse} from '../../shared/administration';
import {getAwardCategoryDescription} from '../awardCategories';
Expand Down Expand Up @@ -34,8 +34,9 @@ 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 [group, setGroup] = useState('');
const group = searchParams.get('group') ?? '';
const [searchInput, setSearchInput] = useState('');
const [search, setSearch] = useState('');
const [cursor, setCursor] = useState<string | undefined>();
Expand Down Expand Up @@ -72,6 +73,16 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) {
setCursorHistory([]);
};

const selectGroup = (groupId: string) => {
setSearchParams((current) => {
const next = new URLSearchParams(current);
if (groupId) next.set('group', groupId);
else next.delete('group');
return next;
});
resetPagination();
};

Comment thread
sentry-junior[bot] marked this conversation as resolved.
useEffect(() => {
const timeout = window.setTimeout(() => {
setSearch(searchInput.trim());
Comment thread
sentry[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -212,10 +223,7 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) {
<span>Group</span>
<select
value={group}
onChange={(event) => {
setGroup(event.target.value);
resetPagination();
}}
onChange={(event) => selectGroup(event.target.value)}
>
<option value="">All groups</option>
{year.data.groups.map((item) => (
Expand Down Expand Up @@ -252,7 +260,7 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) {
{year.data.awards.map((award) => (
<Link
key={award.id}
href={`/years/${yearId}/projects/${award.projectId}`}
href={`/years/${yearId}/projects/${award.projectId}${group ? `?group=${encodeURIComponent(group)}` : ''}`}
>
<span>{award.categoryName}</span>
<small>{getAwardCategoryDescription(award.categoryName)}</small>
Expand Down Expand Up @@ -289,6 +297,7 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) {
project={project}
view={view}
voteCategories={voteCategoriesByProject.get(project.id)}
detailsSearch={group ? `group=${encodeURIComponent(group)}` : undefined}
key={project.id}
/>
))}
Comment thread
sentry-junior[bot] marked this conversation as resolved.
Expand Down
51 changes: 51 additions & 0 deletions test/app/routes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,57 @@ describe('clickable project routes', () => {
).toBe('true');
});

it('keeps the selected group when viewing a project and returning', async () => {
fetchMock.mockImplementation(async (input) => {
const url = requestUrl(input);
if (url.includes('/api/years/2026')) {
return json({
year: {
id: '2026',
votingEnabled: false,
submissionsClosed: false,
projectCount: 1,
ideaCount: 0,
groupCount: 1,
participantCount: 1,
},
groups: [{id: 'group', yearId: '2026', name: 'Orbital', projectCount: 1}],
awards: [],
});
}
expect(new URL(url, 'https://hackweek.test').searchParams.get('group')).toBe(
'group',
);
return json({projects: [projectFixture], nextCursor: null});
});

const projects = renderRoute(
<ProjectsPage />,
'/years/2026/projects?group=group',
'/years/:yearId/projects',
);

const groupSelect = await screen.findByRole('combobox', {name: 'Group'});
expect(groupSelect).toBeInstanceOf(HTMLSelectElement);
if (!(groupSelect instanceof HTMLSelectElement)) throw new Error();
expect(groupSelect.value).toBe('group');
expect(screen.getByRole('link', {name: 'A small machine'}).getAttribute('href')).toBe(
'/years/2026/projects/project?group=group',
);

projects.unmount();
mockProjectDetails({detail: projectFixture});
renderRoute(
<ProjectDetailsPage />,
'/years/2026/projects/project?group=group',
'/years/:yearId/projects/:projectId',
);

expect(
(await screen.findByRole('link', {name: '← 2026 projects'})).getAttribute('href'),
).toBe('/years/2026/projects?group=group');
});

it('requests a 250-item page and focuses and announces loaded pages', async () => {
let resolveSecondPage!: (response: Response) => void;
const pendingSecondPage = new Promise<Response>((resolve) => {
Expand Down
Loading