From bfdbd0fc6aa16703d0df55b7e4aafb4480671038 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 17 Feb 2026 13:07:40 +0100 Subject: [PATCH] Jury UX: fix COI modal, add sliders, redesign stats, gate evaluations - Switch COI dialog from Dialog to AlertDialog (non-dismissible) - Replace number inputs with sliders + rating buttons for criteria/global scores - Redesign jury dashboard stat cards: compact strip on mobile, editorial grid on desktop - Remove ROUND_ACTIVE filter from myAssignments so all assignments show - Block evaluate page when round is inactive or voting window is closed - Gate evaluate button on project detail page based on voting window status Co-Authored-By: Claude Opus 4.6 --- .../projects/[projectId]/evaluate/page.tsx | 253 +++++++++++++----- .../[roundId]/projects/[projectId]/page.tsx | 29 +- src/app/(jury)/jury/competitions/page.tsx | 7 +- src/app/(jury)/jury/page.tsx | 138 ++++------ src/server/routers/assignment.ts | 1 - 5 files changed, 265 insertions(+), 163 deletions(-) diff --git a/src/app/(jury)/jury/competitions/[roundId]/projects/[projectId]/evaluate/page.tsx b/src/app/(jury)/jury/competitions/[roundId]/projects/[projectId]/evaluate/page.tsx index 1be77c4..0e969fa 100644 --- a/src/app/(jury)/jury/competitions/[roundId]/projects/[projectId]/evaluate/page.tsx +++ b/src/app/(jury)/jury/competitions/[roundId]/projects/[projectId]/evaluate/page.tsx @@ -7,21 +7,22 @@ import type { Route } from 'next' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' +import { Slider } from '@/components/ui/slider' import { Textarea } from '@/components/ui/textarea' import { Label } from '@/components/ui/label' import { Skeleton } from '@/components/ui/skeleton' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' import { Checkbox } from '@/components/ui/checkbox' -import { ArrowLeft, Save, Send, AlertCircle, ThumbsUp, ThumbsDown } from 'lucide-react' +import { cn } from '@/lib/utils' +import { ArrowLeft, Save, Send, AlertCircle, ThumbsUp, ThumbsDown, Clock } from 'lucide-react' import { toast } from 'sonner' import type { EvaluationConfig } from '@/types/competition-configs' @@ -235,25 +236,23 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) { // COI Dialog if (!coiAccepted && showCOIDialog && evalConfig?.coiRequired !== false) { return ( - { /* prevent dismissal */ }}> - e.preventDefault()} - onEscapeKeyDown={(e) => e.preventDefault()} - onInteractOutside={(e) => e.preventDefault()} - > - - Conflict of Interest Declaration - -

- Before evaluating this project, you must confirm that you have no conflict of - interest. -

-

- A conflict of interest exists if you have a personal, professional, or financial - relationship with the project team that could influence your judgment. -

-
-
+ + + + Conflict of Interest Declaration + +
+

+ Before evaluating this project, you must confirm that you have no conflict of + interest. +

+

+ A conflict of interest exists if you have a personal, professional, or financial + relationship with the project team that could influence your judgment. +

+
+
+
- + - -
-
+ + + ) } @@ -300,6 +299,48 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) { ) } + // Check if round is active and voting window is open + const now = new Date() + const isRoundActive = round.status === 'ROUND_ACTIVE' + const isWindowOpen = isRoundActive && + round.windowOpenAt && round.windowCloseAt && + new Date(round.windowOpenAt) <= now && new Date(round.windowCloseAt) >= now + + if (!isWindowOpen) { + return ( +
+
+ +
+ + +
+ +
+
+

Evaluation Not Available

+

+ {!isRoundActive + ? 'This round is not currently active. Evaluations can only be submitted during an active round.' + : 'The voting window for this round is not currently open. Please check back when the window opens.'} +

+ +
+
+
+
+ ) + } + return (
@@ -342,51 +383,119 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) { {scoringMode === 'criteria' && criteria && criteria.length > 0 && (

Criteria Scores

- {criteria.map((criterion) => ( -
- - {criterion.description && ( -

{criterion.description}

- )} - - setCriteriaScores({ - ...criteriaScores, - [criterion.id]: parseInt(e.target.value, 10) || 0, - }) - } - placeholder={`Score (${criterion.minScore ?? 0}-${criterion.maxScore ?? 10})`} - /> -
- ))} + {criteria.map((criterion) => { + const min = criterion.minScore ?? 1 + const max = criterion.maxScore ?? 10 + const currentValue = criteriaScores[criterion.id] + const displayValue = currentValue !== undefined ? currentValue : undefined + const sliderValue = typeof currentValue === 'number' ? currentValue : Math.ceil((min + max) / 2) + + return ( +
+
+
+ + {criterion.description && ( +

{criterion.description}

+ )} +
+ + {displayValue !== undefined ? displayValue : '—'}/{max} + +
+ +
+ {min} + + setCriteriaScores({ ...criteriaScores, [criterion.id]: v[0] }) + } + className="flex-1" + /> + {max} +
+ +
+ {Array.from({ length: max - min + 1 }, (_, i) => i + min).map((num) => ( + + ))} +
+
+ ) + })}
)} {/* Global scoring */} {scoringMode === 'global' && ( -
- - setGlobalScore(e.target.value)} - placeholder="Enter score (1-10)" - /> +
+
+ + + {globalScore || '—'}/10 + +
+
+ 1 + setGlobalScore(v[0].toString())} + className="flex-1" + /> + 10 +
+
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((num) => { + const current = globalScore ? parseInt(globalScore, 10) : 0 + return ( + + ) + })} +

Provide a score from 1 to 10 based on your overall assessment

diff --git a/src/app/(jury)/jury/competitions/[roundId]/projects/[projectId]/page.tsx b/src/app/(jury)/jury/competitions/[roundId]/projects/[projectId]/page.tsx index a421f92..9c3a616 100644 --- a/src/app/(jury)/jury/competitions/[roundId]/projects/[projectId]/page.tsx +++ b/src/app/(jury)/jury/competitions/[roundId]/projects/[projectId]/page.tsx @@ -22,6 +22,17 @@ export default function JuryProjectDetailPage() { { enabled: !!projectId } ) + const { data: round } = trpc.round.getById.useQuery( + { id: roundId }, + { enabled: !!roundId } + ) + + // Determine if voting is currently open + const now = new Date() + const isVotingOpen = round?.status === 'ROUND_ACTIVE' && + round?.windowOpenAt && round?.windowCloseAt && + new Date(round.windowOpenAt) <= now && new Date(round.windowCloseAt) >= now + if (isLoading) { return (
@@ -71,12 +82,18 @@ export default function JuryProjectDetailPage() {

{project.teamName}

)}
- + {isVotingOpen ? ( + + ) : ( + + Voting not open + + )}
diff --git a/src/app/(jury)/jury/competitions/page.tsx b/src/app/(jury)/jury/competitions/page.tsx index fe4654a..0b4d2c3 100644 --- a/src/app/(jury)/jury/competitions/page.tsx +++ b/src/app/(jury)/jury/competitions/page.tsx @@ -74,7 +74,7 @@ export default function JuryAssignmentsPage() {

No Assignments

- You don't have any active assignments yet. + You don't have any assignments yet. Assignments will appear once an administrator assigns projects to you.

@@ -95,6 +95,11 @@ export default function JuryAssignmentsPage() { {formatEnumLabel(round.roundType)} + {round.status !== 'ROUND_ACTIVE' && ( + + {formatEnumLabel(round.status)} + + )}
diff --git a/src/app/(jury)/jury/page.tsx b/src/app/(jury)/jury/page.tsx index 26e919f..39967bb 100644 --- a/src/app/(jury)/jury/page.tsx +++ b/src/app/(jury)/jury/page.tsx @@ -24,7 +24,6 @@ import { GitCompare, Zap, BarChart3, - Target, Waves, } from 'lucide-react' import { formatDateOnly } from '@/lib/utils' @@ -187,36 +186,28 @@ async function JuryDashboardContent() { const stats = [ { - label: 'Total Assignments', value: totalAssignments, - icon: ClipboardList, - accentColor: 'border-l-blue-500', - iconBg: 'bg-blue-50 dark:bg-blue-950/40', - iconColor: 'text-blue-600 dark:text-blue-400', + label: 'Assigned', + detail: 'Total projects', + accent: 'text-brand-blue', }, { - label: 'Completed', value: completedAssignments, - icon: CheckCircle2, - accentColor: 'border-l-emerald-500', - iconBg: 'bg-emerald-50 dark:bg-emerald-950/40', - iconColor: 'text-emerald-600 dark:text-emerald-400', + label: 'Completed', + detail: `${completionRate.toFixed(0)}% done`, + accent: 'text-emerald-600', }, { - label: 'In Progress', value: inProgressAssignments, - icon: Clock, - accentColor: 'border-l-amber-500', - iconBg: 'bg-amber-50 dark:bg-amber-950/40', - iconColor: 'text-amber-600 dark:text-amber-400', + label: 'In draft', + detail: inProgressAssignments > 0 ? 'Work in progress' : 'None started', + accent: inProgressAssignments > 0 ? 'text-amber-600' : 'text-emerald-600', }, { - label: 'Pending', value: pendingAssignments, - icon: Target, - accentColor: 'border-l-slate-400', - iconBg: 'bg-slate-50 dark:bg-slate-800/50', - iconColor: 'text-slate-500 dark:text-slate-400', + label: 'Pending', + detail: pendingAssignments > 0 ? 'Not yet started' : 'All started', + accent: pendingAssignments > 0 ? 'text-amber-600' : 'text-emerald-600', }, ] @@ -301,48 +292,34 @@ async function JuryDashboardContent() { )} - {/* Stats + Overall Completion in one row */} -
- {stats.map((stat, i) => ( - - - -
- -
-
-

{stat.value}

-

{stat.label}

-
-
-
-
- ))} - {/* Overall completion as 5th stat card */} - - - -
- + {/* Stats — editorial strip */} + + {/* Mobile: compact horizontal data strip */} +
+ {stats.map((s, i) => ( +
0 ? 'border-l border-border/50' : ''}`}> + {s.value} +

{s.label}

+
+ ))} +
+ + {/* Desktop: editorial stat row */} +
+
+ {stats.map((s, i) => ( +
+ {s.value} +

{s.label}

+

{s.detail}

-
-

- {completionRate.toFixed(0)}% -

-
-
-
-
- - - -
+ ))} +
+
+
{/* Main content -- two column layout */}
@@ -670,30 +647,25 @@ function DashboardSkeleton() { return ( <> {/* Stats skeleton */} -
+
{[...Array(4)].map((_, i) => ( - - - -
- - -
-
-
+
0 ? 'border-l border-border/50' : ''}`}> + + +
))}
- {/* Progress bar skeleton */} - -
- -
- - -
- -
- +
+
+ {[...Array(4)].map((_, i) => ( +
+ + + +
+ ))} +
+
{/* Two-column skeleton */}
diff --git a/src/server/routers/assignment.ts b/src/server/routers/assignment.ts index d8bfd71..daa423b 100644 --- a/src/server/routers/assignment.ts +++ b/src/server/routers/assignment.ts @@ -253,7 +253,6 @@ export const assignmentRouter = router({ .query(async ({ ctx, input }) => { const where: Record = { userId: ctx.user.id, - round: { status: 'ROUND_ACTIVE' }, } if (input.roundId) {