Skip to content

Commit 08b9ca2

Browse files
0xdevcollinsclaude
andauthored
feat(judging): organizer dashboard — coverage, preview, per-track results (#570)
Pairs with boundless-nestjs feat(judging) organizer dashboard upgrades. Adds three new components to the organizer judging page; no changes to existing components, judges, or scoring flows. - `CoverageMatrix` — heatmap on the Overview tab. Rows are submissions, columns are judges. Surfaces idle judges (mostly-empty columns) and orphan submissions (rows with 0-1 scores) at a glance — both block a defensible publish. - `AllocationPreviewCard` — sits above the Publish button on the Results tab. Read-only allocator dry-run showing overall placements + per-track winners exactly as publish-results would commit. Calls out EXCLUSIVE stacking effects (track leader losing to overall), plus surfaces publish gates (deadline, completeness, partner allocation) so the organizer sees blockers without attempting to publish. - `TrackResultsSection` — per-track collapsible standings on the Results tab. Each section is scoped to a track's opt-ins, sorted by averageScore, with the bound prize tier shown as a chip. The leader is highlighted as the current pick — soft preview only; the Allocation Preview above shows the authoritative EXCLUSIVE-stacked outcome. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a36a87c commit 08b9ca2

5 files changed

Lines changed: 999 additions & 0 deletions

File tree

app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,14 @@ import { reportError, reportMessage } from '@/lib/error-reporting';
5353
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
5454
import { JudgingCriteriaList } from '@/components/organization/hackathons/judging/JudgingCriteriaList';
5555
import JudgingResultsTable from '@/components/organization/hackathons/judging/JudgingResultsTable';
56+
import TrackResultsSection from '@/components/organization/hackathons/judging/TrackResultsSection';
57+
import AllocationPreviewCard from '@/components/organization/hackathons/judging/AllocationPreviewCard';
58+
import CoverageMatrix from '@/components/organization/hackathons/judging/CoverageMatrix';
5659
import { OrganizerJudgesPanel } from '@/components/organization/hackathons/judging/OrganizerJudgesPanel';
60+
import {
61+
useHackathon,
62+
useHackathonTracks,
63+
} from '@/hooks/hackathon/use-hackathon-queries';
5764
import { Input } from '@/components/ui/input';
5865
import {
5966
AlertDialog,
@@ -79,6 +86,38 @@ export default function JudgingPage() {
7986
const hackathonId = params.hackathonId as string;
8087

8188
const { activeOrgId, activeOrg } = useOrganization();
89+
90+
// Hackathon + tracks for the per-track Results section. Both are
91+
// best-effort — a 404 or empty array degrades gracefully (the
92+
// per-track UI just won't render).
93+
const { data: hackathonDetail } = useHackathon(hackathonId);
94+
const { data: tracksData } = useHackathonTracks(hackathonId);
95+
const tracks = tracksData ?? [];
96+
97+
// Map trackId → human prize string ("$2,000 USDC") sourced from the
98+
// hackathon's bound prize tiers. Used as a chip on each track section
99+
// header so organizers know which prize a track is paying out.
100+
const prizeByTrackId = useMemo<Record<string, string>>(() => {
101+
const tiers =
102+
(
103+
hackathonDetail as
104+
| { prizeTiers?: Array<Record<string, unknown>> }
105+
| undefined
106+
)?.prizeTiers ?? [];
107+
const out: Record<string, string> = {};
108+
for (const t of tiers) {
109+
if (t?.kind !== 'TRACK' || !t?.trackId) continue;
110+
const amount = String(t.prizeAmount ?? '').trim();
111+
const currency = String(t.currency ?? 'USDC').trim();
112+
if (!amount) continue;
113+
out[t.trackId as string] =
114+
currency.length === 1
115+
? `${currency}${amount}`
116+
: `${amount} ${currency}`;
117+
}
118+
return out;
119+
}, [hackathonDetail]);
120+
82121
const [submissions, setSubmissions] = useState<JudgingSubmission[]>([]);
83122
const [criteria, setCriteria] = useState<JudgingCriterion[]>([]);
84123
const [isLoading, setIsLoading] = useState(true);
@@ -710,6 +749,14 @@ export default function JudgingPage() {
710749
</div>
711750

712751
<TabsContent value='overview' className='mt-6 space-y-6'>
752+
{/* Coverage heatmap. Surfaces idle judges + orphan
753+
submissions before they become a publish blocker. */}
754+
<CoverageMatrix
755+
organizationId={organizationId}
756+
hackathonId={hackathonId}
757+
refreshKey={submissionsPage}
758+
/>
759+
713760
{isLoading && submissions.length === 0 ? (
714761
<div className='flex items-center justify-center py-12'>
715762
<Loader2 className='h-8 w-8 animate-spin text-gray-400' />
@@ -1019,6 +1066,14 @@ export default function JudgingPage() {
10191066
</div>
10201067
)}
10211068

1069+
{!resultsPublished && (
1070+
<AllocationPreviewCard
1071+
organizationId={organizationId}
1072+
hackathonId={hackathonId}
1073+
refreshKey={resultsPage}
1074+
/>
1075+
)}
1076+
10221077
{winners.length > 0 && (
10231078
<div className='space-y-4'>
10241079
<h2 className='flex items-center gap-2 text-lg font-bold text-yellow-500'>
@@ -1037,6 +1092,20 @@ export default function JudgingPage() {
10371092
</div>
10381093
)}
10391094

1095+
{tracks.length > 0 && judgingResults.length > 0 && (
1096+
<TrackResultsSection
1097+
tracks={tracks}
1098+
results={judgingResults}
1099+
totalJudges={currentJudges.length}
1100+
criteria={criteria}
1101+
canManage={canManageJudges}
1102+
winnerOverrides={judgingSummary?.winnerOverrides}
1103+
prizeByTrackId={prizeByTrackId}
1104+
organizationId={organizationId}
1105+
hackathonId={hackathonId}
1106+
/>
1107+
)}
1108+
10401109
<div className='space-y-4'>
10411110
<h2 className='text-lg font-bold text-gray-200'>
10421111
Current Standings

0 commit comments

Comments
 (0)