feat: /judge review + scoring + leaderboard — TDD

Final screen — replaces the last WorkshopStub; App.tsx now has zero stubs:
- SubmissionList: live review queue with scored markers + selection
- AddReview: read-only render of a submitted 5-layer ADD
- ScoreForm: 0-10 rubric per layer, auto-summed total, notes
- Leaderboard: ranked teams by average judge total
- Judge page (behind name+code AccessGate): queue + review/score + leaderboard,
  refreshes on scoring and on any collective event

21 new tests; suite 140/140 green, typecheck + lint clean, build OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-16 19:31:36 -07:00
co-authored by Claude Opus 4.8
parent 6c07788fab
commit bd3250c9c7
11 changed files with 520 additions and 17 deletions
+38
View File
@@ -0,0 +1,38 @@
import type { SubmissionSummary } from '@/types'
import { Badge } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
export interface SubmissionListProps {
items: SubmissionSummary[]
selectedId?: string
onSelect: (teamId: string) => void
}
/** Judge review queue. Pure presentational. */
export function SubmissionList({ items, selectedId, onSelect }: SubmissionListProps) {
if (!items.length) {
return <p className="text-sm text-muted-foreground">No submissions yet.</p>
}
return (
<div className="space-y-1" data-testid="submission-list">
{items.map((s) => (
<button
key={s.teamId}
type="button"
data-team={s.teamId}
aria-current={s.teamId === selectedId}
onClick={() => onSelect(s.teamId)}
className={cn(
'w-full text-left rounded-md border px-3 py-2 transition',
s.teamId === selectedId ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/40',
)}
>
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium truncate">{s.teamName || s.teamId}</span>
{s.scored && <Badge className="font-mono text-[8px] uppercase tracking-wider">scored</Badge>}
</div>
</button>
))}
</div>
)
}