From 270c02d960440edbb5c86d015a630f82801f388e Mon Sep 17 00:00:00 2001 From: Shannon Anahata Date: Fri, 21 Aug 2026 11:07:18 -0700 Subject: [PATCH 1/2] feat(projects): show ready video availability --- src/app/components/ProjectCard.tsx | 9 ++++ src/app/queries/projects.ts | 4 +- src/app/routes/ProjectsPage.tsx | 17 +++++++- src/app/styles.css | 17 ++++++++ src/shared/projects.ts | 1 + src/worker/repositories/projects.ts | 15 ++++++- src/worker/routes/projects.ts | 5 +++ test/app/ProjectForm.test.tsx | 1 + test/app/routes.test.tsx | 59 +++++++++++++++++++++++++ test/projects/projects.test.ts | 67 +++++++++++++++++++++++++++++ 10 files changed, 192 insertions(+), 3 deletions(-) diff --git a/src/app/components/ProjectCard.tsx b/src/app/components/ProjectCard.tsx index 3c1edb2..d8ea656 100644 --- a/src/app/components/ProjectCard.tsx +++ b/src/app/components/ProjectCard.tsx @@ -28,6 +28,7 @@ export function ProjectCard({ groupName={project.group?.name ?? 'ungrouped'} members={project.members} needsHelp={project.needsHelp} + hasVideo={project.hasVideo} voteCategories={voteCategories} /> ); @@ -58,6 +59,7 @@ export function ProjectListItem({ detail, members, needsHelp = false, + hasVideo = false, emptyMemberLabel = 'up for grabs', voteCategories = [], }: { @@ -70,6 +72,7 @@ export function ProjectListItem({ detail?: string; members: ProjectListMember[]; needsHelp?: boolean; + hasVideo?: boolean; emptyMemberLabel?: string; voteCategories?: string[]; }) { @@ -92,6 +95,7 @@ export function ProjectListItem({
{groupName} {detail && {detail}} + {needsHelp && looking for help}
@@ -120,11 +124,16 @@ function ProjectTags({project, className}: {project: ProjectSummary; className: {project.kind === 'idea' ? 'open idea' : (project.group?.name ?? 'ungrouped')} + {project.needsHelp && looking for help} ); } +function ProjectVideoTag({hasVideo}: {hasVideo: boolean}) { + return hasVideo ? has video : null; +} + function MemberStack({ members, emptyLabel = 'up for grabs', diff --git a/src/app/queries/projects.ts b/src/app/queries/projects.ts index 10f4261..edd54b4 100644 --- a/src/app/queries/projects.ts +++ b/src/app/queries/projects.ts @@ -39,6 +39,7 @@ export function useProjects( kind?: 'project' | 'idea', group?: string, search?: string, + hasVideo?: boolean, cursor?: string, ) { const query = new URLSearchParams({ @@ -48,9 +49,10 @@ export function useProjects( if (kind) query.set('kind', kind); if (group) query.set('group', group); if (search) query.set('q', search); + if (hasVideo) query.set('hasVideo', 'true'); if (cursor) query.set('cursor', cursor); return useQuery({ - queryKey: ['projects', yearId, kind, group, search, cursor ?? null], + queryKey: ['projects', yearId, kind, group, search, hasVideo, cursor ?? null], queryFn: () => apiRequest(`/projects?${query}`), placeholderData: keepPreviousData, }); diff --git a/src/app/routes/ProjectsPage.tsx b/src/app/routes/ProjectsPage.tsx index 62d9067..4f23acd 100644 --- a/src/app/routes/ProjectsPage.tsx +++ b/src/app/routes/ProjectsPage.tsx @@ -36,6 +36,7 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { const {yearId} = useParams<{yearId: string}>(); const [kind, setKind] = useState<'project' | 'idea'>('project'); const [group, setGroup] = useState(''); + const [hasVideoOnly, setHasVideoOnly] = useState(false); const [searchInput, setSearchInput] = useState(''); const [search, setSearch] = useState(''); const [cursor, setCursor] = useState(); @@ -50,6 +51,7 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { kind, kind === 'project' ? group || undefined : undefined, search || undefined, + kind === 'project' && hasVideoOnly ? true : undefined, cursor, ); const error = year.error ?? projects.error; @@ -226,6 +228,19 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) { )} + {kind === 'project' && ( + + )}
{(['grid', 'list'] as const).map((option) => (