diff --git a/src/app/(observer)/observer/projects/[projectId]/page.tsx b/src/app/(observer)/observer/projects/[projectId]/page.tsx index 8aa4689..c44148c 100644 --- a/src/app/(observer)/observer/projects/[projectId]/page.tsx +++ b/src/app/(observer)/observer/projects/[projectId]/page.tsx @@ -6,10 +6,13 @@ export const dynamic = 'force-dynamic' export default async function ObserverProjectDetailPage({ params, + searchParams, }: { params: Promise<{ projectId: string }> + searchParams: Promise<{ round?: string }> }) { const { projectId } = await params + const sp = await searchParams - return + return } diff --git a/src/components/observer/observer-project-detail.tsx b/src/components/observer/observer-project-detail.tsx index bc611dc..d50e127 100644 --- a/src/components/observer/observer-project-detail.tsx +++ b/src/components/observer/observer-project-detail.tsx @@ -1,5 +1,6 @@ 'use client' +import { useEffect, useState } from 'react' import Link from 'next/link' import type { Route } from 'next' import { useRouter } from 'next/navigation' @@ -44,15 +45,41 @@ import { } from 'lucide-react' import { cn, formatDate, formatDateOnly } from '@/lib/utils' -export function ObserverProjectDetail({ projectId }: { projectId: string }) { +export function ObserverProjectDetail({ + projectId, + initialRoundId, +}: { + projectId: string + initialRoundId?: string +}) { const router = useRouter() + const [activeRoundId, setActiveRoundId] = useState(initialRoundId) + + // Resolve a default round when none is set: prefer the currently OPEN round + // the project participates in, fall back to the most recently CLOSED one. + const { data: roundCandidates } = trpc.analytics.getProjectRoundsForObserver.useQuery( + { projectId }, + ) + useEffect(() => { + if (activeRoundId || !roundCandidates) return + const active = roundCandidates.find((r) => r.status === 'ROUND_ACTIVE') + if (active) { + setActiveRoundId(active.id) + return + } + const closed = [...roundCandidates] + .filter((r) => r.status === 'ROUND_CLOSED') + .sort((a, b) => b.sortOrder - a.sortOrder)[0] + if (closed) setActiveRoundId(closed.id) + }, [roundCandidates, activeRoundId]) + const { data, isLoading } = trpc.analytics.getProjectDetail.useQuery( - { id: projectId }, + { id: projectId, roundId: activeRoundId }, { refetchInterval: 30_000 }, ) const { data: flags } = trpc.settings.getFeatureFlags.useQuery() - const roundId = data?.assignments?.[0]?.roundId as string | undefined + const roundId = activeRoundId ?? (data?.assignments?.[0]?.roundId as string | undefined) const { data: activeForm } = trpc.evaluation.getStageForm.useQuery( { roundId: roundId ?? '', category: data?.project?.competitionCategory }, { enabled: !!roundId }, @@ -223,6 +250,19 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) { {stats.yesPercentage.toFixed(0)}% recommended

)} + {roundCandidates && roundCandidates.length > 1 && ( +
+ +
+ )} diff --git a/src/server/routers/analytics.ts b/src/server/routers/analytics.ts index 7475433..7d52a17 100644 --- a/src/server/routers/analytics.ts +++ b/src/server/routers/analytics.ts @@ -2187,6 +2187,26 @@ export const analyticsRouter = router({ } }), + /** + * Returns rounds the project has participated in, restricted to those that + * are open or already closed. Used by the observer full project page to + * resolve a default round when none is specified in the URL. + */ + getProjectRoundsForObserver: observerProcedure + .input(z.object({ projectId: z.string() })) + .query(async ({ ctx, input }) => { + const states = await ctx.prisma.projectRoundState.findMany({ + where: { projectId: input.projectId }, + select: { + round: { select: { id: true, name: true, status: true, sortOrder: true } }, + }, + }) + return states + .map((s) => s.round) + .filter((r) => r.status === 'ROUND_ACTIVE' || r.status === 'ROUND_CLOSED') + .sort((a, b) => a.sortOrder - b.sortOrder) + }), + getRecentFiles: observerProcedure .input(z.object({ roundId: z.string(), limit: z.number().min(1).max(50).default(10) })) .query(async ({ ctx, input }) => {