Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { use } from 'react';
|
|
|
|
|
import { trpc } from '@/lib/trpc/client';
|
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
|
import { DeliberationRankingForm } from '@/components/jury/deliberation-ranking-form';
|
|
|
|
|
import { CheckCircle2 } from 'lucide-react';
|
|
|
|
|
import { toast } from 'sonner';
|
|
|
|
|
|
|
|
|
|
export default function JuryDeliberationPage({ params: paramsPromise }: { params: Promise<{ sessionId: string }> }) {
|
|
|
|
|
const params = use(paramsPromise);
|
|
|
|
|
const utils = trpc.useUtils();
|
|
|
|
|
|
2026-02-16 09:30:19 +01:00
|
|
|
const { data: session, isLoading } = trpc.deliberation.getSession.useQuery(
|
|
|
|
|
{ sessionId: params.sessionId },
|
|
|
|
|
{ refetchInterval: 10_000 },
|
|
|
|
|
);
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
|
|
|
|
|
const submitVoteMutation = trpc.deliberation.submitVote.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
utils.deliberation.getSession.invalidate();
|
|
|
|
|
toast.success('Vote submitted successfully');
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
toast.error(err.message);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handleSubmitVote = (votes: Array<{ projectId: string; rank?: number; isWinnerPick?: boolean }>) => {
|
|
|
|
|
votes.forEach((vote) => {
|
|
|
|
|
submitVoteMutation.mutate({
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
juryMemberId: session?.currentUser?.id || '',
|
|
|
|
|
projectId: vote.projectId,
|
|
|
|
|
rank: vote.rank,
|
|
|
|
|
isWinnerPick: vote.isWinnerPick
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="flex items-center justify-center py-12">
|
|
|
|
|
<p className="text-muted-foreground">Loading session...</p>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="flex items-center justify-center py-12">
|
|
|
|
|
<p className="text-muted-foreground">Session not found</p>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hasVoted = session.currentUser?.hasVoted;
|
|
|
|
|
|
|
|
|
|
if (session.status !== 'DELIB_VOTING') {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Deliberation Session</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
{session.round?.name} - {session.category}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
{session.status === 'DELIB_OPEN'
|
|
|
|
|
? 'Voting has not started yet. Please wait for the admin to open voting.'
|
|
|
|
|
: session.status === 'DELIB_TALLYING'
|
|
|
|
|
? 'Voting is closed. Results are being tallied.'
|
|
|
|
|
: 'This session is locked.'}
|
|
|
|
|
</p>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasVoted) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<div className="flex items-start justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<CardTitle>Deliberation Session</CardTitle>
|
|
|
|
|
<CardDescription className="mt-1">
|
|
|
|
|
{session.round?.name} - {session.category}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</div>
|
|
|
|
|
<Badge>{session.status}</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
|
|
|
<CheckCircle2 className="mb-4 h-12 w-12 text-green-600" />
|
|
|
|
|
<p className="font-medium">Vote Submitted</p>
|
|
|
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
|
|
|
Thank you for your participation in this deliberation
|
|
|
|
|
</p>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<div className="flex items-start justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<CardTitle>Deliberation Session</CardTitle>
|
|
|
|
|
<CardDescription className="mt-1">
|
|
|
|
|
{session.round?.name} - {session.category}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</div>
|
|
|
|
|
<Badge>{session.status}</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
{session.mode === 'SINGLE_WINNER_VOTE'
|
|
|
|
|
? 'Select your top choice for this category.'
|
|
|
|
|
: 'Rank all projects from best to least preferred.'}
|
|
|
|
|
</p>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<DeliberationRankingForm
|
|
|
|
|
projects={session.projects || []}
|
|
|
|
|
mode={session.mode}
|
|
|
|
|
onSubmit={handleSubmitVote}
|
|
|
|
|
disabled={submitVoteMutation.isPending}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|