From 22df8c722af21666eb19473713bd43fa0edeec0a Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 11 Mar 2026 13:00:43 -0400 Subject: [PATCH] Hide 'See Results' button when jam results are locked. When voting is submitted, show 'Back to Jams' instead of 'See Results' if the jam hasn't reached the confidence threshold for unlocking results. The decision is computed on the client using voteCount / (entryCount * CONFIDENCE_N) >= 1. Changes: - Backend: Add getComparisonCountForJam() query and include entryCount/voteCount in POST session response - Hook: Expose resultsUnlocked boolean from useJudging - Judge page: Conditionally render button destination based on results lock status Co-Authored-By: Claude Haiku 4.5 --- backend/src/api/routes/jams.ts | 18 ++++++++++++++---- .../src/services/database/judgingQueries.ts | 15 +++++++++++++++ client/src/hooks/useJudging.tsx | 14 +++++++++++--- client/src/pages/Judge.tsx | 7 ++++++- 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/backend/src/api/routes/jams.ts b/backend/src/api/routes/jams.ts index 7fbfedd..800346c 100644 --- a/backend/src/api/routes/jams.ts +++ b/backend/src/api/routes/jams.ts @@ -4,6 +4,7 @@ import { getEntries, getEntryById } from '../../services/entries.js'; import { selectSessionPairs } from '../../services/pairing.js'; import { insertComparison, + getComparisonCountForJam, getComparisonCountForJudge, getComparisonsForJam, } from '../../services/database.js'; @@ -96,10 +97,19 @@ router.post( await insertComparison(slug, judgeId, vote.entryAId, vote.entryBId, vote.score); } - const count = await getComparisonCountForJudge(slug, judgeId); - const sessions = Math.floor(count / JUDGING_SESSION_SIZE); - - res.json({ recorded: true, count: votes.length, sessions }); + const [judgeCount, totalVotes] = await Promise.all([ + getComparisonCountForJudge(slug, judgeId), + getComparisonCountForJam(slug), + ]); + const sessions = Math.floor(judgeCount / JUDGING_SESSION_SIZE); + + res.json({ + recorded: true, + count: votes.length, + sessions, + entryCount: entries.length, + voteCount: totalVotes, + }); } catch (err) { console.error('Error recording session:', err); res.status(500).json({ error: 'Failed to record session' }); diff --git a/backend/src/services/database/judgingQueries.ts b/backend/src/services/database/judgingQueries.ts index 624f04c..dc04f53 100644 --- a/backend/src/services/database/judgingQueries.ts +++ b/backend/src/services/database/judgingQueries.ts @@ -105,6 +105,21 @@ export async function getComparisonCountForJudge( return parseCount(results[0]?.count); } +/** + * Get total comparison count for a jam. + */ +export async function getComparisonCountForJam(jamSlug: string): Promise { + const sql = getSql(); + + const results = await sql<{ count: string }[]>` + SELECT COUNT(*) as count + FROM jam_comparisons + WHERE jam_slug = ${jamSlug} + `; + + return parseCount(results[0]?.count); +} + /** * Get unique judge count for a jam. */ diff --git a/client/src/hooks/useJudging.tsx b/client/src/hooks/useJudging.tsx index 16991a9..bf28b21 100644 --- a/client/src/hooks/useJudging.tsx +++ b/client/src/hooks/useJudging.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback, useReducer } from 'react'; import { JUDGING_SESSION_SIZE } from '../constants/judging'; +import { CONFIDENCE_N } from '../utils/jam'; export interface EntryMetrics { classification: 'Whole Game' | 'Feature'; @@ -93,6 +94,8 @@ interface SessionState { fetchedAt: number; status: Status; sessions: number; + entryCount: number; + voteCount: number; error: string | null; } @@ -103,6 +106,8 @@ const initialState: SessionState = { fetchedAt: 0, status: 'loading', sessions: 0, + entryCount: 0, + voteCount: 0, error: null, }; @@ -112,7 +117,7 @@ type SessionAction = | { type: 'GO_BACK' } | { type: 'RESET' } | { type: 'SUBMIT_START' } - | { type: 'SUBMIT_SUCCESS'; sessions: number } + | { type: 'SUBMIT_SUCCESS'; sessions: number; entryCount: number; voteCount: number } | { type: 'SUBMIT_ERROR'; error: string } | { type: 'SET_ERROR'; error: string }; @@ -161,7 +166,7 @@ function sessionReducer(state: SessionState, action: SessionAction): SessionStat return { ...state, status: 'submitting' }; case 'SUBMIT_SUCCESS': - return { ...state, status: 'submitted', sessions: action.sessions }; + return { ...state, status: 'submitted', sessions: action.sessions, entryCount: action.entryCount, voteCount: action.voteCount }; case 'SUBMIT_ERROR': return { ...state, status: 'review', error: action.error }; @@ -287,7 +292,7 @@ export function useJudging(jamSlug: string) { const data = await res.json(); clearSession(jamSlug); - dispatch({ type: 'SUBMIT_SUCCESS', sessions: data.sessions ?? 0 }); + dispatch({ type: 'SUBMIT_SUCCESS', sessions: data.sessions ?? 0, entryCount: data.entryCount ?? 0, voteCount: data.voteCount ?? 0 }); } catch (e) { dispatch({ type: 'SUBMIT_ERROR', error: e instanceof Error ? e.message : 'Failed to submit session' }); } @@ -309,6 +314,8 @@ export function useJudging(jamSlug: string) { total: state.pairs.length || JUDGING_SESSION_SIZE, }; + const resultsUnlocked = state.entryCount > 0 && state.voteCount >= state.entryCount * CONFIDENCE_N; + return { user, pair, @@ -316,6 +323,7 @@ export function useJudging(jamSlug: string) { loading: state.status === 'loading', status: state.status, sessions: state.sessions, + resultsUnlocked, error: state.error, votes: state.votes, pairs: state.pairs, diff --git a/client/src/pages/Judge.tsx b/client/src/pages/Judge.tsx index 5665ed0..2bc10db 100644 --- a/client/src/pages/Judge.tsx +++ b/client/src/pages/Judge.tsx @@ -15,6 +15,7 @@ export default function Judge() { loading, status, sessions, + resultsUnlocked, error, canGoBack, submitScore, @@ -92,7 +93,11 @@ export default function Judge() { - See Results + {resultsUnlocked ? ( + See Results + ) : ( + Back to Jams + )} );