Add observer project detail page with files, evaluations & reviews
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m59s

New page at /observer/projects/[projectId] showing project info,
documents grouped by round requirements, and jury evaluations with
click-through to full review details. Dashboard table rows now link
to project detail. Also cleans up redundant programName prefixes
and fixes chart edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-20 18:39:53 +01:00
parent f1062f4805
commit 03c59c188e
9 changed files with 1236 additions and 57 deletions

View File

@@ -2,6 +2,7 @@
import { useState, useEffect } from 'react'
import Link from 'next/link'
import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client'
import {
Card,
@@ -150,26 +151,6 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
</p>
</div>
{/* Observer Notice */}
<div className="rounded-lg border border-blue-200 bg-blue-50/50 px-4 py-3">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-blue-100 p-2.5">
<Eye className="h-4 w-4 text-blue-600" />
</div>
<div>
<div className="flex items-center gap-2">
<p className="font-semibold text-blue-900">Observer Mode</p>
<Badge variant="outline" className="border-blue-300 text-blue-700 text-xs">
Read-Only
</Badge>
</div>
<p className="text-sm text-blue-700">
You have read-only access to view platform statistics and reports.
</p>
</div>
</div>
</div>
{/* Round Filter */}
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<label className="text-sm font-medium">Filter by Round:</label>
@@ -181,7 +162,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
<SelectItem value="all">All Rounds</SelectItem>
{rounds.map((round) => (
<SelectItem key={round.id} value={round.id}>
{round.programName} - {round.name}{round.roundType ? ` (${round.roundType.replace(/_/g, ' ')})` : ''}
{round.name}{round.roundType ? ` (${round.roundType.replace(/_/g, ' ')})` : ''}
</SelectItem>
))}
</SelectContent>
@@ -364,7 +345,6 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
Title<SortIcon column="title" />
</button>
</TableHead>
<TableHead>Team</TableHead>
<TableHead>Round</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">
@@ -381,11 +361,12 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
</TableHeader>
<TableBody>
{projectsData.projects.map((project) => (
<TableRow key={project.id}>
<TableCell className="font-medium max-w-[250px] truncate">
{project.title}
<TableRow key={project.id} className="cursor-pointer hover:bg-muted/50" onClick={() => window.location.href = `/observer/projects/${project.id}`}>
<TableCell className="font-medium max-w-[300px] truncate">
<Link href={`/observer/projects/${project.id}` as Route} className="hover:underline" onClick={(e) => e.stopPropagation()}>
{project.title}
</Link>
</TableCell>
<TableCell className="max-w-[150px] truncate">{project.teamName || '-'}</TableCell>
<TableCell>
<Badge variant="outline" className="text-xs whitespace-nowrap">
{project.roundName}
@@ -411,26 +392,25 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
{/* Mobile Cards */}
<div className="space-y-3 md:hidden">
{projectsData.projects.map((project) => (
<Card key={project.id}>
<CardContent className="pt-4 space-y-2">
<div className="flex items-start justify-between gap-2">
<p className="font-medium text-sm leading-tight">{project.title}</p>
<StatusBadge status={project.status} />
</div>
{project.teamName && (
<p className="text-xs text-muted-foreground">{project.teamName}</p>
)}
<div className="flex items-center justify-between text-xs text-muted-foreground">
<Badge variant="outline" className="text-xs">
{project.roundName}
</Badge>
<div className="flex gap-3">
<span>Score: {project.averageScore !== null ? project.averageScore.toFixed(2) : '-'}</span>
<span>{project.evaluationCount} eval{project.evaluationCount !== 1 ? 's' : ''}</span>
<Link key={project.id} href={`/observer/projects/${project.id}` as Route}>
<Card className="transition-colors hover:bg-muted/50">
<CardContent className="pt-4 space-y-2">
<div className="flex items-start justify-between gap-2">
<p className="font-medium text-sm leading-tight">{project.title}</p>
<StatusBadge status={project.status} />
</div>
</div>
</CardContent>
</Card>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<Badge variant="outline" className="text-xs">
{project.roundName}
</Badge>
<div className="flex gap-3">
<span>Score: {project.averageScore !== null ? project.averageScore.toFixed(2) : '-'}</span>
<span>{project.evaluationCount} eval{project.evaluationCount !== 1 ? 's' : ''}</span>
</div>
</div>
</CardContent>
</Card>
</Link>
))}
</div>