feat: revamp applicant jury feedback UI with score summaries and observer-style design
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Dashboard sidebar card now shows per-round avg score with progress bars. Evaluations page redesigned with stats strip, score comparison bars, criteria progress bars, animated cards, and styled feedback blocks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,8 +8,82 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Progress } from '@/components/ui/progress'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
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 (
|
||||||
|
<div className="flex items-center gap-2 flex-1">
|
||||||
|
<div className="flex-1 overflow-hidden rounded-full bg-muted" style={{ height: 10 }}>
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full transition-all duration-500"
|
||||||
|
style={{ width: `${pct}%`, backgroundColor: color }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-semibold tabular-nums w-8 text-right">{score}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <Trophy className={cn('h-4 w-4 text-amber-500', className)} />
|
||||||
|
if (roundType === 'DELIBERATION') return <Vote className={cn('h-4 w-4 text-violet-500', className)} />
|
||||||
|
return <Star className={cn('h-4 w-4 text-yellow-500', className)} />
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
export default function ApplicantEvaluationsPage() {
|
||||||
const { data: rounds, isLoading } = trpc.applicant.getMyEvaluations.useQuery()
|
const { data: rounds, isLoading } = trpc.applicant.getMyEvaluations.useQuery()
|
||||||
@@ -21,6 +95,14 @@ export default function ApplicantEvaluationsPage() {
|
|||||||
<h1 className="text-2xl font-bold">Jury Feedback</h1>
|
<h1 className="text-2xl font-bold">Jury Feedback</h1>
|
||||||
<p className="text-muted-foreground">Anonymous evaluations from jury members</p>
|
<p className="text-muted-foreground">Anonymous evaluations from jury members</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-px overflow-hidden rounded-lg border bg-border">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className="bg-card p-4">
|
||||||
|
<Skeleton className="h-5 w-20 mb-2" />
|
||||||
|
<Skeleton className="h-8 w-16" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[1, 2].map((i) => (
|
{[1, 2].map((i) => (
|
||||||
<Card key={i}>
|
<Card key={i}>
|
||||||
@@ -37,6 +119,28 @@ export default function ApplicantEvaluationsPage() {
|
|||||||
|
|
||||||
const hasEvaluations = rounds && rounds.length > 0
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
@@ -49,7 +153,9 @@ export default function ApplicantEvaluationsPage() {
|
|||||||
{!hasEvaluations ? (
|
{!hasEvaluations ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||||
<Star className="h-12 w-12 text-muted-foreground/50 mb-4" />
|
<div className="rounded-2xl bg-muted/60 p-4 mb-4">
|
||||||
|
<Star className="h-8 w-8 text-muted-foreground/50" />
|
||||||
|
</div>
|
||||||
<h3 className="text-lg font-medium mb-2">No Evaluations Available</h3>
|
<h3 className="text-lg font-medium mb-2">No Evaluations Available</h3>
|
||||||
<p className="text-muted-foreground text-center max-w-md">
|
<p className="text-muted-foreground text-center max-w-md">
|
||||||
Evaluations will appear here once jury review is complete and results are published.
|
Evaluations will appear here once jury review is complete and results are published.
|
||||||
@@ -58,101 +164,188 @@ export default function ApplicantEvaluationsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{rounds.map((round) => {
|
{/* Stats Summary Strip */}
|
||||||
const roundIcon = round.roundType === 'LIVE_FINAL'
|
<AnimatedCard index={0}>
|
||||||
? <Trophy className="h-5 w-5 text-amber-500" />
|
<Card className="p-0 overflow-hidden">
|
||||||
: round.roundType === 'DELIBERATION'
|
<div className="grid grid-cols-3 divide-x divide-border">
|
||||||
? <Vote className="h-5 w-5 text-violet-500" />
|
<div className="p-4 text-center">
|
||||||
: <Star className="h-5 w-5 text-yellow-500" />
|
<div className="flex items-center justify-center gap-1.5 mb-1">
|
||||||
|
<BarChart3 className="h-3.5 w-3.5 text-blue-500" />
|
||||||
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Reviews</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold tabular-nums">{totalEvaluations}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 text-center">
|
||||||
|
<div className="flex items-center justify-center gap-1.5 mb-1">
|
||||||
|
<TrendingUp className="h-3.5 w-3.5 text-emerald-500" />
|
||||||
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Avg Score</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold tabular-nums">
|
||||||
|
{globalAvg !== null ? globalAvg.toFixed(1) : '—'}
|
||||||
|
{globalAvg !== null && <span className="text-sm font-normal text-muted-foreground"> / 100</span>}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 text-center">
|
||||||
|
<div className="flex items-center justify-center gap-1.5 mb-1">
|
||||||
|
<Award className="h-3.5 w-3.5 text-amber-500" />
|
||||||
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Highest</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold tabular-nums">
|
||||||
|
{globalHighest !== null ? globalHighest : '—'}
|
||||||
|
{globalHighest !== null && <span className="text-sm font-normal text-muted-foreground"> / 100</span>}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</AnimatedCard>
|
||||||
|
|
||||||
|
{/* Per-Round Cards */}
|
||||||
|
{rounds.map((round, roundIdx) => {
|
||||||
|
const { maxScore, avg, highest, lowest } = computeRoundStats(round)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={round.roundId}>
|
<AnimatedCard key={round.roundId} index={roundIdx + 1}>
|
||||||
<CardHeader>
|
<Card>
|
||||||
<div className="flex items-center justify-between">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<div className="flex items-center justify-between">
|
||||||
{roundIcon}
|
<CardTitle className="flex items-center gap-2.5">
|
||||||
{round.roundName}
|
<div className={cn('rounded-lg p-1.5', roundIconBg(round.roundType))}>
|
||||||
</CardTitle>
|
<RoundIcon roundType={round.roundType} />
|
||||||
<Badge variant="secondary">
|
|
||||||
{round.evaluationCount} {round.roundType === 'DELIBERATION' ? 'vote' : '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">
|
|
||||||
{round.roundType === 'DELIBERATION' ? `Juror #${idx + 1}` : `Evaluator #${idx + 1}`}
|
|
||||||
</span>
|
|
||||||
{ev.submittedAt && (
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{new Date(ev.submittedAt).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ev.globalScore !== null && round.roundType !== 'DELIBERATION' && (
|
|
||||||
<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">
|
|
||||||
/ {round.roundType === 'LIVE_FINAL' ? '10' : '100'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div>
|
||||||
|
<span>{round.roundName}</span>
|
||||||
{ev.criterionScores && ev.criteria && (
|
{avg !== null && round.roundType !== 'DELIBERATION' && (
|
||||||
<div className="space-y-2">
|
<p className="text-sm font-normal text-muted-foreground mt-0.5">
|
||||||
<p className="text-sm font-medium text-muted-foreground">Criterion Scores</p>
|
Average: <span className="font-semibold text-foreground">{avg.toFixed(1)}</span> / {maxScore}
|
||||||
<div className="grid gap-2">
|
{highest !== null && lowest !== null && highest !== lowest && (
|
||||||
{(() => {
|
<span className="ml-2">
|
||||||
const criteria = ev.criteria as Array<{ id?: string; label?: string; name?: string; maxScore?: number }>
|
Range: {lowest}–{highest}
|
||||||
const scores = ev.criterionScores as Record<string, number>
|
</span>
|
||||||
return criteria
|
)}
|
||||||
.filter((c) => c.id || c.label || c.name)
|
</p>
|
||||||
.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>
|
</div>
|
||||||
)}
|
</CardTitle>
|
||||||
|
<Badge variant="secondary">
|
||||||
{ev.feedbackText && (
|
{round.evaluationCount} {round.roundType === 'DELIBERATION' ? 'vote' : 'evaluation'}{round.evaluationCount !== 1 ? 's' : ''}
|
||||||
<div className="space-y-1.5">
|
</Badge>
|
||||||
<div className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
|
|
||||||
<MessageSquare className="h-3.5 w-3.5" />
|
|
||||||
{round.roundType === 'DELIBERATION' ? 'Result' : 'Written Feedback'}
|
|
||||||
</div>
|
|
||||||
<blockquote className="border-l-2 border-muted pl-4 text-sm italic text-muted-foreground">
|
|
||||||
{ev.feedbackText}
|
|
||||||
</blockquote>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</CardHeader>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
{/* Score Overview Bar — visual comparison across evaluators */}
|
||||||
|
{round.roundType !== 'DELIBERATION' && round.evaluations.some((ev) => ev.globalScore !== null) && (
|
||||||
|
<div className="px-6 pb-3">
|
||||||
|
<div className="rounded-lg bg-muted/40 p-3 space-y-2">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Score Comparison</p>
|
||||||
|
{round.evaluations.map((ev, idx) => {
|
||||||
|
if (ev.globalScore === null) return null
|
||||||
|
return (
|
||||||
|
<div key={ev.id} className="flex items-center gap-3">
|
||||||
|
<span className="text-xs text-muted-foreground w-6 text-right shrink-0 tabular-nums">
|
||||||
|
#{idx + 1}
|
||||||
|
</span>
|
||||||
|
<ScoreBar
|
||||||
|
score={ev.globalScore}
|
||||||
|
maxScore={maxScore}
|
||||||
|
color={getScoreColor(ev.globalScore, maxScore)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="divide-y">
|
||||||
|
{round.evaluations.map((ev, idx) => (
|
||||||
|
<div
|
||||||
|
key={ev.id}
|
||||||
|
className="px-6 py-4 space-y-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-medium text-sm">
|
||||||
|
{round.roundType === 'DELIBERATION' ? `Juror #${idx + 1}` : `Evaluator #${idx + 1}`}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{ev.globalScore !== null && round.roundType !== 'DELIBERATION' && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Star className="h-3.5 w-3.5 text-yellow-500" />
|
||||||
|
<span className="text-sm font-bold tabular-nums">{ev.globalScore}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">/ {maxScore}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{ev.submittedAt && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{new Date(ev.submittedAt).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ev.criterionScores && ev.criteria && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Criteria Breakdown</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]
|
||||||
|
const cMax = c.maxScore || 10
|
||||||
|
const pct = score !== undefined ? (score / cMax) * 100 : 0
|
||||||
|
return (
|
||||||
|
<div key={ci} className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">{c.label || c.name || `Criterion ${ci + 1}`}</span>
|
||||||
|
<span className="font-semibold tabular-nums">
|
||||||
|
{score !== undefined ? score : '—'}
|
||||||
|
<span className="text-muted-foreground font-normal text-xs"> / {cMax}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{score !== undefined && (
|
||||||
|
<Progress value={pct} className="h-1.5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ev.feedbackText && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
<MessageSquare className="h-3.5 w-3.5" />
|
||||||
|
{round.roundType === 'DELIBERATION' ? 'Result' : 'Written Feedback'}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg bg-muted/40 px-4 py-3 border-l-3 border-brand-teal">
|
||||||
|
<p className="text-sm italic text-muted-foreground leading-relaxed">
|
||||||
|
{ev.feedbackText}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</AnimatedCard>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground text-center">
|
{/* Confidentiality Footer */}
|
||||||
Evaluator identities are kept confidential.
|
<div className="flex items-center justify-center gap-2 py-2">
|
||||||
</p>
|
<ShieldCheck className="h-4 w-4 text-muted-foreground/60" />
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Evaluator identities are kept confidential.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { CompetitionTimelineSidebar } from '@/components/applicant/competition-t
|
|||||||
import { MentoringRequestCard } from '@/components/applicant/mentoring-request-card'
|
import { MentoringRequestCard } from '@/components/applicant/mentoring-request-card'
|
||||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||||
import { ProjectLogoUpload } from '@/components/shared/project-logo-upload'
|
import { ProjectLogoUpload } from '@/components/shared/project-logo-upload'
|
||||||
|
import { Progress } from '@/components/ui/progress'
|
||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
Calendar,
|
Calendar,
|
||||||
@@ -35,6 +36,8 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
X,
|
X,
|
||||||
UserCircle,
|
UserCircle,
|
||||||
|
Trophy,
|
||||||
|
Vote,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
@@ -390,7 +393,9 @@ export default function ApplicantDashboardPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Star className="h-5 w-5" />
|
<div className="rounded-lg bg-yellow-500/10 p-1.5">
|
||||||
|
<Star className="h-4 w-4 text-yellow-500" />
|
||||||
|
</div>
|
||||||
Jury Feedback
|
Jury Feedback
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Button variant="ghost" size="sm" asChild>
|
<Button variant="ghost" size="sm" asChild>
|
||||||
@@ -400,15 +405,47 @@ export default function ApplicantDashboardPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-3">
|
||||||
{evaluations?.map((round) => (
|
{evaluations?.map((round) => {
|
||||||
<div key={round.roundId} className="flex items-center justify-between text-sm rounded-lg border p-2.5">
|
const scores = round.evaluations
|
||||||
<span className="font-medium">{round.roundName}</span>
|
.map((ev) => ev.globalScore)
|
||||||
<Badge variant="secondary" className="text-xs">
|
.filter((s): s is number => s !== null)
|
||||||
{round.evaluationCount} review{round.evaluationCount !== 1 ? 's' : ''}
|
const avgScore = scores.length > 0
|
||||||
</Badge>
|
? scores.reduce((a, b) => a + b, 0) / scores.length
|
||||||
</div>
|
: null
|
||||||
))}
|
const maxScore = round.roundType === 'LIVE_FINAL' ? 10 : 100
|
||||||
|
const pct = avgScore !== null ? (avgScore / maxScore) * 100 : 0
|
||||||
|
const roundIcon = round.roundType === 'LIVE_FINAL'
|
||||||
|
? <Trophy className="h-3.5 w-3.5 text-amber-500" />
|
||||||
|
: round.roundType === 'DELIBERATION'
|
||||||
|
? <Vote className="h-3.5 w-3.5 text-violet-500" />
|
||||||
|
: <Star className="h-3.5 w-3.5 text-yellow-500" />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={round.roundId} className="rounded-lg border p-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1.5 text-sm font-medium">
|
||||||
|
{roundIcon}
|
||||||
|
{round.roundName}
|
||||||
|
</span>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{round.evaluationCount} review{round.evaluationCount !== 1 ? 's' : ''}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{avgScore !== null && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||||
|
<span>Avg Score</span>
|
||||||
|
<span className="font-semibold text-foreground tabular-nums">
|
||||||
|
{avgScore.toFixed(1)}<span className="text-muted-foreground font-normal"> / {maxScore}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={pct} className="h-1.5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
|
|||||||
Reference in New Issue
Block a user