Round system redesign: criteria voting, audience voting, pipeline view, and admin UX improvements
- Schema: Extend LiveVotingSession with votingMode, criteriaJson, audience fields; add AudienceVoter model; make LiveVote.userId nullable for audience voters - Backend: Criteria-based voting with weighted scores, audience registration/voting with token-based dedup, configurable jury/audience weight in results - Jury UI: Criteria scoring with per-criterion sliders alongside simple 1-10 mode - Public audience voting page at /vote/[sessionId] with mobile-first design - Admin live voting: Tabbed layout (Session/Config/Results), criteria config, audience settings, weight-adjustable results with tie detection - Round type settings: Visual card selector replacing dropdown, feature tags - Round detail page: Live event status section, type-specific stats and actions - Round pipeline view: Horizontal visualization with bottleneck detection, List/Pipeline toggle on rounds page - SSE: Separate jury/audience vote events, audience vote tracking - Field visibility: Hide irrelevant fields per round type in create/edit forms Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -63,10 +63,13 @@ import {
|
||||
Loader2,
|
||||
GripVertical,
|
||||
ArrowRight,
|
||||
List,
|
||||
GitBranchPlus,
|
||||
} from 'lucide-react'
|
||||
import { format, isPast, isFuture } from 'date-fns'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import { RoundPipeline } from '@/components/admin/round-pipeline'
|
||||
|
||||
type RoundData = {
|
||||
id: string
|
||||
@@ -81,7 +84,7 @@ type RoundData = {
|
||||
}
|
||||
}
|
||||
|
||||
function RoundsContent() {
|
||||
function RoundsContent({ viewMode }: { viewMode: 'list' | 'pipeline' }) {
|
||||
const { data: programs, isLoading } = trpc.program.list.useQuery({
|
||||
includeRounds: true,
|
||||
})
|
||||
@@ -107,6 +110,45 @@ function RoundsContent() {
|
||||
)
|
||||
}
|
||||
|
||||
if (viewMode === 'pipeline') {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{programs.map((program, index) => (
|
||||
<AnimatedCard key={program.id} index={index}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-lg">{program.year} Edition</CardTitle>
|
||||
<CardDescription>
|
||||
{program.name} - {program.status}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link href={`/admin/rounds/new?program=${program.id}`}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Round
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{(program.rounds && program.rounds.length > 0) ? (
|
||||
<RoundPipeline rounds={program.rounds} programName={program.name} />
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Calendar className="mx-auto h-8 w-8 mb-2 opacity-50" />
|
||||
<p>No rounds created yet</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{programs.map((program, index) => (
|
||||
@@ -669,6 +711,8 @@ function RoundsListSkeleton() {
|
||||
}
|
||||
|
||||
export default function RoundsPage() {
|
||||
const [viewMode, setViewMode] = useState<'list' | 'pipeline'>('list')
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -679,11 +723,31 @@ export default function RoundsPage() {
|
||||
Manage selection rounds and voting periods
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 rounded-lg border p-1">
|
||||
<Button
|
||||
variant={viewMode === 'list' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className="h-8 px-3"
|
||||
onClick={() => setViewMode('list')}
|
||||
>
|
||||
<List className="mr-1.5 h-4 w-4" />
|
||||
List
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'pipeline' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className="h-8 px-3"
|
||||
onClick={() => setViewMode('pipeline')}
|
||||
>
|
||||
<GitBranchPlus className="mr-1.5 h-4 w-4" />
|
||||
Pipeline
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<Suspense fallback={<RoundsListSkeleton />}>
|
||||
<RoundsContent />
|
||||
<RoundsContent viewMode={viewMode} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user