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 }) => {