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 (
+
+ )
+}
+
+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 }
+
+
+
+
+
+ {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
@@ -400,15 +405,47 @@ export default function ApplicantDashboardPage() {
-
- {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}
+
+
+
+
+ )}
+
+ )
+ })}