All checks were successful
Build and Push Docker Image / build (push) Successful in 9m6s
- Fix programId/competitionId bug in competition timeline - Add applicantVisibility config to EvaluationConfigSchema (JSONB) - Add admin UI card for controlling applicant feedback visibility - Add 6 new tRPC procedures: getNavFlags, getMyCompetitionTimeline, getMyEvaluations, getUpcomingDeadlines, getDocumentCompleteness, and extend getMyDashboard with hasPassedIntake - Rewrite competition timeline to show only EVALUATION + Grand Finale, synthesize FILTERING rejections, handle manually-created projects - Dynamic ApplicantNav with conditional Evaluations/Mentoring/Resources - Dashboard: conditional timeline, jury feedback card, deadlines, document completeness, conditional mentor tile - New /applicant/evaluations page with anonymous jury feedback - New /applicant/resources pages (clone of jury learning hub) - Rename /applicant/competitions → /applicant/competition - Remove broken /applicant/competitions/[windowId] page - Add permission info banner to team invite dialog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
148 lines
5.7 KiB
TypeScript
148 lines
5.7 KiB
TypeScript
'use client'
|
|
|
|
import { trpc } from '@/lib/trpc/client'
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
import { Star, MessageSquare } from 'lucide-react'
|
|
|
|
export default function ApplicantEvaluationsPage() {
|
|
const { data: rounds, isLoading } = trpc.applicant.getMyEvaluations.useQuery()
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Jury Feedback</h1>
|
|
<p className="text-muted-foreground">Anonymous evaluations from jury members</p>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{[1, 2].map((i) => (
|
|
<Card key={i}>
|
|
<CardContent className="py-6">
|
|
<Skeleton className="h-6 w-48 mb-4" />
|
|
<Skeleton className="h-24 w-full" />
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const hasEvaluations = rounds && rounds.length > 0
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Jury Feedback</h1>
|
|
<p className="text-muted-foreground">
|
|
Anonymous evaluations from jury members
|
|
</p>
|
|
</div>
|
|
|
|
{!hasEvaluations ? (
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
<Star className="h-12 w-12 text-muted-foreground/50 mb-4" />
|
|
<h3 className="text-lg font-medium mb-2">No Evaluations Available</h3>
|
|
<p className="text-muted-foreground text-center max-w-md">
|
|
Evaluations will appear here once jury review is complete and results are published.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-6">
|
|
{rounds.map((round) => (
|
|
<Card key={round.roundId}>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle>{round.roundName}</CardTitle>
|
|
<Badge variant="secondary">
|
|
{round.evaluationCount} evaluation{round.evaluationCount !== 1 ? 's' : ''}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{round.evaluations.map((ev, idx) => (
|
|
<div
|
|
key={ev.id}
|
|
className="rounded-lg border p-4 space-y-3"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-medium text-sm">
|
|
Evaluator #{idx + 1}
|
|
</span>
|
|
{ev.submittedAt && (
|
|
<span className="text-xs text-muted-foreground">
|
|
{new Date(ev.submittedAt).toLocaleDateString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{ev.globalScore !== null && (
|
|
<div className="flex items-center gap-2">
|
|
<Star className="h-4 w-4 text-yellow-500" />
|
|
<span className="text-lg font-semibold">{ev.globalScore}</span>
|
|
<span className="text-sm text-muted-foreground">/ 100</span>
|
|
</div>
|
|
)}
|
|
|
|
{ev.criterionScores && ev.criteria && (
|
|
<div className="space-y-2">
|
|
<p className="text-sm font-medium text-muted-foreground">Criterion Scores</p>
|
|
<div className="grid gap-2">
|
|
{(() => {
|
|
const criteria = ev.criteria as Array<{ id?: string; label?: string; name?: string; maxScore?: number }>
|
|
const scores = ev.criterionScores as Record<string, number>
|
|
return criteria
|
|
.filter((c) => c.id || c.label || c.name)
|
|
.map((c, ci) => {
|
|
const key = c.id || String(ci)
|
|
const score = scores[key]
|
|
return (
|
|
<div key={ci} className="flex items-center justify-between text-sm">
|
|
<span>{c.label || c.name || `Criterion ${ci + 1}`}</span>
|
|
<span className="font-medium">
|
|
{score !== undefined ? score : '—'}
|
|
{c.maxScore ? ` / ${c.maxScore}` : ''}
|
|
</span>
|
|
</div>
|
|
)
|
|
})
|
|
})()}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{ev.feedbackText && (
|
|
<div className="space-y-1.5">
|
|
<div className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
|
|
<MessageSquare className="h-3.5 w-3.5" />
|
|
Written Feedback
|
|
</div>
|
|
<blockquote className="border-l-2 border-muted pl-4 text-sm italic text-muted-foreground">
|
|
{ev.feedbackText}
|
|
</blockquote>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
|
|
<p className="text-xs text-muted-foreground text-center">
|
|
Evaluator identities are kept confidential.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|