Add observer project detail page with files, evaluations & reviews
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m59s
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:
@@ -36,7 +36,7 @@ export const STATUS_COLORS: Record<string, string> = {
|
||||
export const STATUS_LABELS: Record<string, string> = {
|
||||
SUBMITTED: 'Submitted',
|
||||
ELIGIBLE: 'Eligible',
|
||||
ASSIGNED: 'Assigned',
|
||||
ASSIGNED: 'Special Award',
|
||||
SEMIFINALIST: 'Semi-finalist',
|
||||
FINALIST: 'Finalist',
|
||||
REJECTED: 'Rejected',
|
||||
|
||||
@@ -61,7 +61,7 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
: topCountries
|
||||
|
||||
const nivoPieData = countryPieData.map((c) => ({
|
||||
id: c.country,
|
||||
id: c.country === 'Others' ? 'Others' : c.country.toUpperCase(),
|
||||
label: getCountryName(c.country),
|
||||
value: c.count,
|
||||
}))
|
||||
|
||||
@@ -30,13 +30,13 @@ export function ProjectRankingsChart({
|
||||
data,
|
||||
limit = 20,
|
||||
}: ProjectRankingsProps) {
|
||||
if (!data?.length) return null
|
||||
|
||||
const scoredData = data.filter(
|
||||
const scoredData = (data ?? []).filter(
|
||||
(d): d is ProjectRankingData & { averageScore: number } =>
|
||||
d.averageScore !== null,
|
||||
)
|
||||
|
||||
if (!scoredData.length) return null
|
||||
|
||||
const averageScore =
|
||||
scoredData.length > 0
|
||||
? scoredData.reduce((sum, d) => sum + d.averageScore, 0) /
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
1051
src/components/observer/observer-project-detail.tsx
Normal file
1051
src/components/observer/observer-project-detail.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user