diff --git a/src/app/(applicant)/applicant/evaluations/page.tsx b/src/app/(applicant)/applicant/evaluations/page.tsx index d749379..fb4975d 100644 --- a/src/app/(applicant)/applicant/evaluations/page.tsx +++ b/src/app/(applicant)/applicant/evaluations/page.tsx @@ -8,8 +8,82 @@ import { CardTitle, } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' +import { Progress } from '@/components/ui/progress' import { Skeleton } from '@/components/ui/skeleton' -import { Star, MessageSquare, Trophy, Vote } from 'lucide-react' +import { AnimatedCard } from '@/components/shared/animated-container' +import { + Star, + MessageSquare, + Trophy, + Vote, + TrendingUp, + BarChart3, + Award, + ShieldCheck, +} from 'lucide-react' +import { cn } from '@/lib/utils' + +type EvaluationRound = { + roundId: string + roundName: string + roundType: string + evaluationCount: number + evaluations: Array<{ + id: string + submittedAt: Date | null + globalScore: number | null + criterionScores: unknown + feedbackText: string | null + criteria: unknown + }> +} + +function computeRoundStats(round: EvaluationRound) { + const maxScore = round.roundType === 'LIVE_FINAL' ? 10 : 100 + const scores = round.evaluations + .map((ev) => ev.globalScore) + .filter((s): s is number => s !== null) + const avg = scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : null + const highest = scores.length > 0 ? Math.max(...scores) : null + const lowest = scores.length > 0 ? Math.min(...scores) : null + return { maxScore, avg, highest, lowest, scores } +} + +function ScoreBar({ score, maxScore, color }: { score: number; maxScore: number; color: string }) { + const pct = (score / maxScore) * 100 + return ( +
+
+
+
+ {score} +
+ ) +} + +function getScoreColor(score: number, maxScore: number): string { + const pct = score / maxScore + if (pct >= 0.8) return '#053d57' + if (pct >= 0.6) return '#1e7a8a' + if (pct >= 0.4) return '#557f8c' + if (pct >= 0.2) return '#c4453a' + return '#de0f1e' +} + +function RoundIcon({ roundType, className }: { roundType: string; className?: string }) { + if (roundType === 'LIVE_FINAL') return + if (roundType === 'DELIBERATION') return + return +} + +function roundIconBg(roundType: string) { + if (roundType === 'LIVE_FINAL') return 'bg-amber-500/10' + if (roundType === 'DELIBERATION') return 'bg-violet-500/10' + return 'bg-yellow-500/10' +} export default function ApplicantEvaluationsPage() { const { data: rounds, isLoading } = trpc.applicant.getMyEvaluations.useQuery() @@ -21,6 +95,14 @@ export default function ApplicantEvaluationsPage() {

Jury Feedback

Anonymous evaluations from jury members

+
+ {[1, 2, 3].map((i) => ( +
+ + +
+ ))} +
{[1, 2].map((i) => ( @@ -37,6 +119,28 @@ export default function ApplicantEvaluationsPage() { const hasEvaluations = rounds && rounds.length > 0 + // Compute global stats + const allScores: number[] = [] + let totalEvaluations = 0 + if (rounds) { + for (const round of rounds) { + totalEvaluations += round.evaluationCount + for (const ev of round.evaluations) { + if (ev.globalScore !== null && round.roundType !== 'DELIBERATION') { + // Normalize to 0-100 for live final scores + const normalized = round.roundType === 'LIVE_FINAL' + ? ev.globalScore * 10 + : ev.globalScore + allScores.push(normalized) + } + } + } + } + const globalAvg = allScores.length > 0 + ? allScores.reduce((a, b) => a + b, 0) / allScores.length + : null + const globalHighest = allScores.length > 0 ? Math.max(...allScores) : null + return (
@@ -49,7 +153,9 @@ export default function ApplicantEvaluationsPage() { {!hasEvaluations ? ( - +
+ +

No Evaluations Available

Evaluations will appear here once jury review is complete and results are published. @@ -58,101 +164,188 @@ export default function ApplicantEvaluationsPage() { ) : (

- {rounds.map((round) => { - const roundIcon = round.roundType === 'LIVE_FINAL' - ? - : round.roundType === 'DELIBERATION' - ? - : + {/* Stats Summary Strip */} + + +
+
+
+ + Reviews +
+

{totalEvaluations}

+
+
+
+ + Avg Score +
+

+ {globalAvg !== null ? globalAvg.toFixed(1) : '—'} + {globalAvg !== null && / 100} +

+
+
+
+ + Highest +
+

+ {globalHighest !== null ? globalHighest : '—'} + {globalHighest !== null && / 100} +

+
+
+
+
+ + {/* Per-Round Cards */} + {rounds.map((round, roundIdx) => { + const { maxScore, avg, highest, lowest } = computeRoundStats(round) return ( - - -
- - {roundIcon} - {round.roundName} - - - {round.evaluationCount} {round.roundType === 'DELIBERATION' ? 'vote' : 'evaluation'}{round.evaluationCount !== 1 ? 's' : ''} - -
-
- - {round.evaluations.map((ev, idx) => ( -
-
- - {round.roundType === 'DELIBERATION' ? `Juror #${idx + 1}` : `Evaluator #${idx + 1}`} - - {ev.submittedAt && ( - - {new Date(ev.submittedAt).toLocaleDateString()} - - )} -
- - {ev.globalScore !== null && round.roundType !== 'DELIBERATION' && ( -
- - {ev.globalScore} - - / {round.roundType === 'LIVE_FINAL' ? '10' : '100'} - + + + +
+ +
+
- )} - - {ev.criterionScores && ev.criteria && ( -
-

Criterion Scores

-
- {(() => { - const criteria = ev.criteria as Array<{ id?: string; label?: string; name?: string; maxScore?: number }> - const scores = ev.criterionScores as Record - return criteria - .filter((c) => c.id || c.label || c.name) - .map((c, ci) => { - const key = c.id || String(ci) - const score = scores[key] - return ( -
- {c.label || c.name || `Criterion ${ci + 1}`} - - {score !== undefined ? score : '—'} - {c.maxScore ? ` / ${c.maxScore}` : ''} - -
- ) - }) - })()} -
+
+ {round.roundName} + {avg !== null && round.roundType !== 'DELIBERATION' && ( +

+ Average: {avg.toFixed(1)} / {maxScore} + {highest !== null && lowest !== null && highest !== lowest && ( + + Range: {lowest}–{highest} + + )} +

+ )}
- )} - - {ev.feedbackText && ( -
-
- - {round.roundType === 'DELIBERATION' ? 'Result' : 'Written Feedback'} -
-
- {ev.feedbackText} -
-
- )} + + + {round.evaluationCount} {round.roundType === 'DELIBERATION' ? 'vote' : 'evaluation'}{round.evaluationCount !== 1 ? 's' : ''} +
- ))} - - + + + {/* Score Overview Bar — visual comparison across evaluators */} + {round.roundType !== 'DELIBERATION' && round.evaluations.some((ev) => ev.globalScore !== null) && ( +
+
+

Score Comparison

+ {round.evaluations.map((ev, idx) => { + if (ev.globalScore === null) return null + return ( +
+ + #{idx + 1} + + +
+ ) + })} +
+
+ )} + + +
+ {round.evaluations.map((ev, idx) => ( +
+
+ + {round.roundType === 'DELIBERATION' ? `Juror #${idx + 1}` : `Evaluator #${idx + 1}`} + +
+ {ev.globalScore !== null && round.roundType !== 'DELIBERATION' && ( + + + {ev.globalScore} + / {maxScore} + + )} + {ev.submittedAt && ( + + {new Date(ev.submittedAt).toLocaleDateString()} + + )} +
+
+ + {ev.criterionScores && ev.criteria && ( +
+

Criteria Breakdown

+
+ {(() => { + const criteria = ev.criteria as Array<{ id?: string; label?: string; name?: string; maxScore?: number }> + const scores = ev.criterionScores as Record + return criteria + .filter((c) => c.id || c.label || c.name) + .map((c, ci) => { + const key = c.id || String(ci) + const score = scores[key] + const cMax = c.maxScore || 10 + const pct = score !== undefined ? (score / cMax) * 100 : 0 + return ( +
+
+ {c.label || c.name || `Criterion ${ci + 1}`} + + {score !== undefined ? score : '—'} + / {cMax} + +
+ {score !== undefined && ( + + )} +
+ ) + }) + })()} +
+
+ )} + + {ev.feedbackText && ( +
+
+ + {round.roundType === 'DELIBERATION' ? 'Result' : 'Written Feedback'} +
+
+

+ {ev.feedbackText} +

+
+
+ )} +
+ ))} +
+
+ + ) })} -

- Evaluator identities are kept confidential. -

+ {/* Confidentiality Footer */} +
+ +

+ Evaluator identities are kept confidential. +

+
)}
diff --git a/src/app/(applicant)/applicant/page.tsx b/src/app/(applicant)/applicant/page.tsx index 5b67cde..76f04ad 100644 --- a/src/app/(applicant)/applicant/page.tsx +++ b/src/app/(applicant)/applicant/page.tsx @@ -19,6 +19,7 @@ import { CompetitionTimelineSidebar } from '@/components/applicant/competition-t import { MentoringRequestCard } from '@/components/applicant/mentoring-request-card' import { AnimatedCard } from '@/components/shared/animated-container' import { ProjectLogoUpload } from '@/components/shared/project-logo-upload' +import { Progress } from '@/components/ui/progress' import { FileText, Calendar, @@ -35,6 +36,8 @@ import { Check, X, UserCircle, + Trophy, + Vote, } from 'lucide-react' import { toast } from 'sonner' @@ -390,7 +393,9 @@ export default function ApplicantDashboardPage() {
- +
+ +
Jury Feedback
- - {evaluations?.map((round) => ( -
- {round.roundName} - - {round.evaluationCount} review{round.evaluationCount !== 1 ? 's' : ''} - -
- ))} + + {evaluations?.map((round) => { + const scores = round.evaluations + .map((ev) => ev.globalScore) + .filter((s): s is number => s !== null) + const avgScore = scores.length > 0 + ? scores.reduce((a, b) => a + b, 0) / scores.length + : null + const maxScore = round.roundType === 'LIVE_FINAL' ? 10 : 100 + const pct = avgScore !== null ? (avgScore / maxScore) * 100 : 0 + const roundIcon = round.roundType === 'LIVE_FINAL' + ? + : round.roundType === 'DELIBERATION' + ? + : + + return ( +
+
+ + {roundIcon} + {round.roundName} + + + {round.evaluationCount} review{round.evaluationCount !== 1 ? 's' : ''} + +
+ {avgScore !== null && ( +
+
+ Avg Score + + {avgScore.toFixed(1)} / {maxScore} + +
+ +
+ )} +
+ ) + })}