feat: clickable status badges, observer status alignment, CSV export all
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m36s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m36s
- Admin projects: status summary badges are clickable to filter by round state with ring highlight, opacity fade, and clear button - Add roundStates filter param to project.list backend query (filters by latest round state per project, consistent with counts) - Observer status dropdown now uses ProjectRoundState values (Pending/In Progress/Completed/Passed/Rejected/Withdrawn) - Observer status derived from latest ProjectRoundState instead of stale Project.status - Observer CSV export fetches all matching projects, not just current page - Add PENDING and PASSED styles to StatusBadge component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import Link from 'next/link'
|
||||
import { useSearchParams, usePathname } from 'next/navigation'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { toast } from 'sonner'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -145,6 +146,9 @@ function parseFiltersFromParams(
|
||||
statuses: searchParams.get('status')
|
||||
? searchParams.get('status')!.split(',')
|
||||
: [],
|
||||
roundStates: searchParams.get('roundState')
|
||||
? searchParams.get('roundState')!.split(',')
|
||||
: [],
|
||||
roundId: searchParams.get('round') || '',
|
||||
competitionCategory: searchParams.get('category') || '',
|
||||
oceanIssue: searchParams.get('issue') || '',
|
||||
@@ -179,6 +183,8 @@ function filtersToParams(
|
||||
if (filters.search) params.set('q', filters.search)
|
||||
if (filters.statuses.length > 0)
|
||||
params.set('status', filters.statuses.join(','))
|
||||
if (filters.roundStates.length > 0)
|
||||
params.set('roundState', filters.roundStates.join(','))
|
||||
if (filters.roundId) params.set('round', filters.roundId)
|
||||
if (filters.competitionCategory)
|
||||
params.set('category', filters.competitionCategory)
|
||||
@@ -204,6 +210,7 @@ export default function ProjectsPage() {
|
||||
const [filters, setFilters] = useState<ProjectFilters>({
|
||||
search: parsed.search,
|
||||
statuses: parsed.statuses,
|
||||
roundStates: parsed.roundStates,
|
||||
roundId: parsed.roundId,
|
||||
competitionCategory: parsed.competitionCategory,
|
||||
oceanIssue: parsed.oceanIssue,
|
||||
@@ -309,6 +316,12 @@ export default function ProjectsPage() {
|
||||
wantsMentorship: filters.wantsMentorship,
|
||||
hasFiles: filters.hasFiles,
|
||||
hasAssignments: filters.hasAssignments,
|
||||
roundStates:
|
||||
filters.roundStates.length > 0
|
||||
? (filters.roundStates as Array<
|
||||
'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'PASSED' | 'REJECTED' | 'WITHDRAWN'
|
||||
>)
|
||||
: undefined,
|
||||
page,
|
||||
perPage,
|
||||
sortBy: sortBy as 'title' | 'category' | 'program' | 'assignments' | 'status' | 'createdAt' | undefined,
|
||||
@@ -752,7 +765,7 @@ export default function ProjectsPage() {
|
||||
/>
|
||||
|
||||
{/* Stats Summary + View Toggle */}
|
||||
{data && data.projects.length > 0 && (
|
||||
{data && (Object.keys(data.statusCounts ?? {}).length > 0 || data.projects.length > 0) && (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm">
|
||||
{Object.entries(data.statusCounts ?? {})
|
||||
@@ -760,15 +773,43 @@ export default function ProjectsPage() {
|
||||
const order = ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'PASSED', 'REJECTED', 'WITHDRAWN']
|
||||
return order.indexOf(a) - order.indexOf(b)
|
||||
})
|
||||
.map(([status, count]) => (
|
||||
<Badge
|
||||
key={status}
|
||||
variant={statusColors[status] || 'secondary'}
|
||||
className="text-xs font-normal"
|
||||
>
|
||||
{count} {status.charAt(0) + status.slice(1).toLowerCase().replace('_', ' ')}
|
||||
</Badge>
|
||||
))}
|
||||
.map(([status, count]) => {
|
||||
const isActive = filters.roundStates.includes(status)
|
||||
return (
|
||||
<button
|
||||
key={status}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const next = isActive
|
||||
? filters.roundStates.filter((s) => s !== status)
|
||||
: [...filters.roundStates, status]
|
||||
handleFiltersChange({ ...filters, roundStates: next })
|
||||
}}
|
||||
className="inline-flex items-center"
|
||||
>
|
||||
<Badge
|
||||
variant={statusColors[status] || 'secondary'}
|
||||
className={cn(
|
||||
'text-xs font-normal cursor-pointer transition-all',
|
||||
isActive && 'ring-2 ring-offset-1 ring-primary',
|
||||
!isActive && filters.roundStates.length > 0 && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{count} {status.charAt(0) + status.slice(1).toLowerCase().replace('_', ' ')}
|
||||
</Badge>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{filters.roundStates.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleFiltersChange({ ...filters, roundStates: [] })}
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors ml-1"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
{data.total > data.projects.length && (
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
(page {data.page} of {data.totalPages})
|
||||
|
||||
@@ -63,6 +63,7 @@ const ISSUE_LABELS: Record<string, string> = {
|
||||
export interface ProjectFilters {
|
||||
search: string
|
||||
statuses: string[]
|
||||
roundStates: string[]
|
||||
roundId: string
|
||||
competitionCategory: string
|
||||
oceanIssue: string
|
||||
@@ -94,6 +95,7 @@ export function ProjectFiltersBar({
|
||||
|
||||
const activeFilterCount = [
|
||||
filters.statuses.length > 0,
|
||||
filters.roundStates.length > 0,
|
||||
filters.roundId !== '',
|
||||
filters.competitionCategory !== '',
|
||||
filters.oceanIssue !== '',
|
||||
@@ -114,6 +116,7 @@ export function ProjectFiltersBar({
|
||||
onChange({
|
||||
search: filters.search,
|
||||
statuses: [],
|
||||
roundStates: [],
|
||||
roundId: '',
|
||||
competitionCategory: '',
|
||||
oceanIssue: '',
|
||||
|
||||
Reference in New Issue
Block a user