Admin dashboard & round management UX overhaul
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m43s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m43s
- Extract round detail monolith (2900→600 lines) into 13 standalone components - Add shared round/status config (round-config.ts) replacing 4 local copies - Delete 12 legacy competition-scoped pages, merge project pool into projects page - Add round-type-specific dashboard stat panels (submission, mentoring, live final, deliberation, summary) - Add contextual header quick actions based on active round type - Improve pipeline visualization: progress bars, checkmarks, chevron connectors, overflow fix - Add config tab completion dots (green/amber/red) and inline validation warnings - Enhance juries page with round assignments, member avatars, and cap mode badges - Add context-aware project list (recent submissions vs active evaluations) - Move competition settings into Manage Editions page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,13 +3,6 @@
|
||||
import Link from 'next/link'
|
||||
import { motion } from 'motion/react'
|
||||
import {
|
||||
Inbox,
|
||||
Filter,
|
||||
ClipboardCheck,
|
||||
Upload,
|
||||
Users,
|
||||
Radio,
|
||||
Scale,
|
||||
Clock,
|
||||
ArrowRight,
|
||||
} from 'lucide-react'
|
||||
@@ -25,6 +18,7 @@ import {
|
||||
} from '@/components/ui/tooltip'
|
||||
import { StatusBadge } from '@/components/shared/status-badge'
|
||||
import { cn, formatEnumLabel, daysUntil } from '@/lib/utils'
|
||||
import { roundTypeConfig, projectStateConfig } from '@/lib/round-config'
|
||||
|
||||
export type PipelineRound = {
|
||||
id: string
|
||||
@@ -60,24 +54,13 @@ type ActiveRoundPanelProps = {
|
||||
round: PipelineRound
|
||||
}
|
||||
|
||||
const roundTypeIcons: Record<string, React.ElementType> = {
|
||||
INTAKE: Inbox,
|
||||
FILTERING: Filter,
|
||||
EVALUATION: ClipboardCheck,
|
||||
SUBMISSION: Upload,
|
||||
MENTORING: Users,
|
||||
LIVE_FINAL: Radio,
|
||||
DELIBERATION: Scale,
|
||||
}
|
||||
const roundTypeIcons: Record<string, React.ElementType> = Object.fromEntries(
|
||||
Object.entries(roundTypeConfig).map(([k, v]) => [k, v.icon])
|
||||
)
|
||||
|
||||
const stateColors: Record<string, { bg: string; label: string }> = {
|
||||
PENDING: { bg: 'bg-slate-300', label: 'Pending' },
|
||||
IN_PROGRESS: { bg: 'bg-blue-400', label: 'In Progress' },
|
||||
PASSED: { bg: 'bg-emerald-500', label: 'Passed' },
|
||||
REJECTED: { bg: 'bg-red-400', label: 'Rejected' },
|
||||
COMPLETED: { bg: 'bg-[#557f8c]', label: 'Completed' },
|
||||
WITHDRAWN: { bg: 'bg-slate-400', label: 'Withdrawn' },
|
||||
}
|
||||
const stateColors: Record<string, { bg: string; label: string }> = Object.fromEntries(
|
||||
Object.entries(projectStateConfig).map(([k, v]) => [k, { bg: v.bg, label: v.label }])
|
||||
)
|
||||
|
||||
function DeadlineCountdown({ date }: { date: Date }) {
|
||||
const days = daysUntil(date)
|
||||
@@ -264,7 +247,7 @@ function RoundTypeContent({ round }: { round: PipelineRound }) {
|
||||
}
|
||||
|
||||
export function ActiveRoundPanel({ round }: ActiveRoundPanelProps) {
|
||||
const Icon = roundTypeIcons[round.roundType] || ClipboardCheck
|
||||
const Icon = roundTypeIcons[round.roundType] || roundTypeConfig.INTAKE.icon
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from 'next/link'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { motion } from 'motion/react'
|
||||
import { Workflow, ArrowRight } from 'lucide-react'
|
||||
import { Workflow, ArrowRight, ChevronRight } from 'lucide-react'
|
||||
import {
|
||||
PipelineRoundNode,
|
||||
type PipelineRound,
|
||||
@@ -12,27 +12,46 @@ import {
|
||||
|
||||
function Connector({
|
||||
prevStatus,
|
||||
nextStatus,
|
||||
index,
|
||||
}: {
|
||||
prevStatus: string
|
||||
nextStatus: string
|
||||
index: number
|
||||
}) {
|
||||
const isCompleted =
|
||||
prevStatus === 'ROUND_CLOSED' || prevStatus === 'ROUND_ARCHIVED'
|
||||
const isNextActive = nextStatus === 'ROUND_ACTIVE'
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: 1 }}
|
||||
transition={{ duration: 0.25, delay: 0.15 + index * 0.06 }}
|
||||
className="flex items-center self-center origin-left"
|
||||
initial={{ scaleX: 0, opacity: 0 }}
|
||||
animate={{ scaleX: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.15 + index * 0.06 }}
|
||||
className="flex items-center self-center origin-left px-0.5"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'h-0.5 w-6',
|
||||
isCompleted ? 'bg-emerald-300' : 'bg-slate-200'
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-0">
|
||||
<div
|
||||
className={cn(
|
||||
'h-0.5 w-5 transition-colors',
|
||||
isCompleted
|
||||
? 'bg-emerald-400'
|
||||
: isNextActive
|
||||
? 'bg-blue-300'
|
||||
: 'bg-slate-200'
|
||||
)}
|
||||
/>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 -ml-1',
|
||||
isCompleted
|
||||
? 'text-emerald-400'
|
||||
: isNextActive
|
||||
? 'text-blue-300'
|
||||
: 'text-slate-200'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -88,15 +107,17 @@ export function CompetitionPipeline({
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto pb-2">
|
||||
<div className="flex items-start gap-0 min-w-max">
|
||||
<CardContent className="px-4 pb-4">
|
||||
{/* Scrollable container with padding to prevent cutoff */}
|
||||
<div className="overflow-x-auto -mx-1 px-1 pt-2 pb-3">
|
||||
<div className="flex items-center gap-0 min-w-max">
|
||||
{rounds.map((round, index) => (
|
||||
<div key={round.id} className="flex items-start">
|
||||
<div key={round.id} className="flex items-center">
|
||||
<PipelineRoundNode round={round} index={index} />
|
||||
{index < rounds.length - 1 && (
|
||||
<Connector
|
||||
prevStatus={round.status}
|
||||
nextStatus={rounds[index + 1].status}
|
||||
index={index}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -5,14 +5,9 @@ import type { Route } from 'next'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { motion } from 'motion/react'
|
||||
import {
|
||||
Upload,
|
||||
Filter,
|
||||
ClipboardCheck,
|
||||
FileUp,
|
||||
GraduationCap,
|
||||
Radio,
|
||||
Scale,
|
||||
} from 'lucide-react'
|
||||
roundTypeConfig as sharedRoundTypeConfig,
|
||||
roundStatusConfig as sharedRoundStatusConfig,
|
||||
} from '@/lib/round-config'
|
||||
|
||||
type PipelineRound = {
|
||||
id: string
|
||||
@@ -55,66 +50,11 @@ type PipelineRound = {
|
||||
deliberationCount: number
|
||||
}
|
||||
|
||||
const roundTypeConfig: Record<
|
||||
string,
|
||||
{ icon: typeof Upload; iconColor: string; iconBg: string }
|
||||
> = {
|
||||
INTAKE: { icon: Upload, iconColor: 'text-sky-600', iconBg: 'bg-sky-100' },
|
||||
FILTERING: {
|
||||
icon: Filter,
|
||||
iconColor: 'text-amber-600',
|
||||
iconBg: 'bg-amber-100',
|
||||
},
|
||||
EVALUATION: {
|
||||
icon: ClipboardCheck,
|
||||
iconColor: 'text-violet-600',
|
||||
iconBg: 'bg-violet-100',
|
||||
},
|
||||
SUBMISSION: {
|
||||
icon: FileUp,
|
||||
iconColor: 'text-blue-600',
|
||||
iconBg: 'bg-blue-100',
|
||||
},
|
||||
MENTORING: {
|
||||
icon: GraduationCap,
|
||||
iconColor: 'text-teal-600',
|
||||
iconBg: 'bg-teal-100',
|
||||
},
|
||||
LIVE_FINAL: {
|
||||
icon: Radio,
|
||||
iconColor: 'text-red-600',
|
||||
iconBg: 'bg-red-100',
|
||||
},
|
||||
DELIBERATION: {
|
||||
icon: Scale,
|
||||
iconColor: 'text-indigo-600',
|
||||
iconBg: 'bg-indigo-100',
|
||||
},
|
||||
}
|
||||
const roundTypeConfig = sharedRoundTypeConfig
|
||||
|
||||
const statusStyles: Record<
|
||||
string,
|
||||
{ container: string; label: string }
|
||||
> = {
|
||||
ROUND_DRAFT: {
|
||||
container:
|
||||
'bg-slate-50 border-slate-200 text-slate-400 border-dashed',
|
||||
label: 'Draft',
|
||||
},
|
||||
ROUND_ACTIVE: {
|
||||
container:
|
||||
'bg-blue-50 border-blue-300 text-blue-700 ring-2 ring-blue-400/30 shadow-lg shadow-blue-500/10',
|
||||
label: 'Active',
|
||||
},
|
||||
ROUND_CLOSED: {
|
||||
container: 'bg-emerald-50 border-emerald-200 text-emerald-600',
|
||||
label: 'Closed',
|
||||
},
|
||||
ROUND_ARCHIVED: {
|
||||
container: 'bg-slate-50/50 border-slate-100 text-slate-300',
|
||||
label: 'Archived',
|
||||
},
|
||||
}
|
||||
const statusStyles: Record<string, { container: string; label: string }> = Object.fromEntries(
|
||||
Object.entries(sharedRoundStatusConfig).map(([k, v]) => [k, { container: v.pipelineContainer, label: v.label }])
|
||||
)
|
||||
|
||||
function getMetric(round: PipelineRound): string {
|
||||
const { roundType, projectStates, filteringTotal, filteringPassed, evalTotal, evalSubmitted, assignmentCount, liveSessionStatus, deliberationCount } = round
|
||||
@@ -147,6 +87,30 @@ function getMetric(round: PipelineRound): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getProgressPct(round: PipelineRound): number | null {
|
||||
if (round.status !== 'ROUND_ACTIVE') return null
|
||||
|
||||
switch (round.roundType) {
|
||||
case 'FILTERING': {
|
||||
const processed = round.filteringPassed + round.filteringRejected + round.filteringFlagged
|
||||
const total = round.projectStates.total || round.filteringTotal
|
||||
return total > 0 ? Math.round((processed / total) * 100) : 0
|
||||
}
|
||||
case 'EVALUATION':
|
||||
return round.evalTotal > 0 ? Math.round((round.evalSubmitted / round.evalTotal) * 100) : 0
|
||||
case 'SUBMISSION': {
|
||||
const total = round.projectStates.total
|
||||
return total > 0 ? Math.round((round.projectStates.COMPLETED / total) * 100) : 0
|
||||
}
|
||||
case 'MENTORING': {
|
||||
const total = round.projectStates.total
|
||||
return total > 0 ? Math.round((round.projectStates.COMPLETED / total) * 100) : 0
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function PipelineRoundNode({
|
||||
round,
|
||||
index,
|
||||
@@ -158,7 +122,9 @@ export function PipelineRoundNode({
|
||||
const Icon = typeConfig.icon
|
||||
const status = statusStyles[round.status] ?? statusStyles.ROUND_DRAFT
|
||||
const isActive = round.status === 'ROUND_ACTIVE'
|
||||
const isCompleted = round.status === 'ROUND_CLOSED' || round.status === 'ROUND_ARCHIVED'
|
||||
const metric = getMetric(round)
|
||||
const progressPct = getProgressPct(round)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -172,8 +138,8 @@ export function PipelineRoundNode({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex flex-col items-center rounded-xl border-2 p-3 transition-all hover:-translate-y-0.5 hover:shadow-md',
|
||||
isActive ? 'w-44' : 'w-36',
|
||||
'relative flex flex-col items-center rounded-xl border-2 transition-all hover:-translate-y-0.5 hover:shadow-md',
|
||||
isActive ? 'w-48 px-4 py-4' : 'w-40 px-3 py-3.5',
|
||||
status.container
|
||||
)}
|
||||
>
|
||||
@@ -185,30 +151,64 @@ export function PipelineRoundNode({
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Completed check */}
|
||||
{isCompleted && (
|
||||
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-emerald-500 text-white">
|
||||
<svg className="h-2.5 w-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-8 w-8 items-center justify-center rounded-lg',
|
||||
'flex items-center justify-center rounded-lg',
|
||||
isActive ? 'h-10 w-10' : 'h-9 w-9',
|
||||
typeConfig.iconBg
|
||||
)}
|
||||
>
|
||||
<Icon className={cn('h-4 w-4', typeConfig.iconColor)} />
|
||||
<Icon className={cn(isActive ? 'h-5 w-5' : 'h-4 w-4', typeConfig.iconColor)} />
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<p className="mt-2 text-center text-xs font-semibold leading-tight line-clamp-2 group-hover:text-foreground transition-colors">
|
||||
<p className="mt-2.5 text-center text-xs font-semibold leading-tight line-clamp-2 group-hover:text-foreground transition-colors">
|
||||
{round.name}
|
||||
</p>
|
||||
|
||||
{/* Type label */}
|
||||
<span className="mt-1 text-[10px] font-medium text-muted-foreground/70">
|
||||
{typeConfig.label}
|
||||
</span>
|
||||
|
||||
{/* Status label */}
|
||||
<span className="mt-1.5 text-[10px] font-medium uppercase tracking-wider opacity-70">
|
||||
<span className="mt-1 text-[10px] font-semibold uppercase tracking-wider opacity-70">
|
||||
{status.label}
|
||||
</span>
|
||||
|
||||
{/* Progress bar for active rounds */}
|
||||
{progressPct !== null && (
|
||||
<div className="mt-2 w-full">
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-black/5">
|
||||
<motion.div
|
||||
className="h-full rounded-full bg-blue-500"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progressPct}%` }}
|
||||
transition={{ duration: 0.8, ease: 'easeOut' }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-0.5 text-center text-[10px] font-medium tabular-nums text-blue-600">
|
||||
{progressPct}%
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metric */}
|
||||
<p className="mt-1 text-[11px] font-medium tabular-nums opacity-80">
|
||||
{metric}
|
||||
</p>
|
||||
{progressPct === null && (
|
||||
<p className="mt-1.5 text-[11px] font-medium tabular-nums opacity-80">
|
||||
{metric}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
@@ -13,24 +13,41 @@ import {
|
||||
import { StatusBadge } from '@/components/shared/status-badge'
|
||||
import { ProjectLogo } from '@/components/shared/project-logo'
|
||||
import { getCountryName } from '@/lib/countries'
|
||||
import { formatDateOnly, truncate } from '@/lib/utils'
|
||||
import { formatDateOnly, truncate, formatRelativeTime } from '@/lib/utils'
|
||||
|
||||
type ProjectListCompactProps = {
|
||||
projects: Array<{
|
||||
id: string
|
||||
title: string
|
||||
teamName: string | null
|
||||
country: string | null
|
||||
competitionCategory: string | null
|
||||
oceanIssue: string | null
|
||||
logoKey: string | null
|
||||
createdAt: Date
|
||||
submittedAt: Date | null
|
||||
status: string
|
||||
}>
|
||||
type BaseProject = {
|
||||
id: string
|
||||
title: string
|
||||
teamName: string | null
|
||||
country: string | null
|
||||
competitionCategory: string | null
|
||||
oceanIssue: string | null
|
||||
logoKey: string | null
|
||||
createdAt: Date
|
||||
submittedAt: Date | null
|
||||
status: string
|
||||
}
|
||||
|
||||
export function ProjectListCompact({ projects }: ProjectListCompactProps) {
|
||||
type ActiveProject = BaseProject & {
|
||||
latestEvaluator: string | null
|
||||
latestScore: number | null
|
||||
evaluatedAt: Date | null
|
||||
}
|
||||
|
||||
type ProjectListCompactProps = {
|
||||
projects: BaseProject[]
|
||||
activeProjects?: ActiveProject[]
|
||||
mode?: 'recent' | 'active'
|
||||
}
|
||||
|
||||
export function ProjectListCompact({
|
||||
projects,
|
||||
activeProjects,
|
||||
mode = 'recent',
|
||||
}: ProjectListCompactProps) {
|
||||
const isActiveMode = mode === 'active' && activeProjects && activeProjects.length > 0
|
||||
const displayProjects = isActiveMode ? activeProjects : projects
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
@@ -40,8 +57,12 @@ export function ProjectListCompact({ projects }: ProjectListCompactProps) {
|
||||
<ClipboardList className="h-4 w-4 text-brand-blue" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">Recent Projects</CardTitle>
|
||||
<CardDescription className="text-xs">Latest submissions</CardDescription>
|
||||
<CardTitle className="text-base">
|
||||
{isActiveMode ? 'Recently Active' : 'Recent Projects'}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
{isActiveMode ? 'Latest evaluation activity' : 'Latest submissions'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
@@ -53,7 +74,7 @@ export function ProjectListCompact({ projects }: ProjectListCompactProps) {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{projects.length === 0 ? (
|
||||
{displayProjects.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-10 text-center">
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-muted">
|
||||
<ClipboardList className="h-7 w-7 text-muted-foreground/40" />
|
||||
@@ -64,48 +85,69 @@ export function ProjectListCompact({ projects }: ProjectListCompactProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{projects.map((project, idx) => (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.25, delay: 0.15 + idx * 0.04 }}
|
||||
>
|
||||
<Link
|
||||
href={`/admin/projects/${project.id}`}
|
||||
className="block"
|
||||
{displayProjects.map((project, idx) => {
|
||||
const activeProject = isActiveMode ? (project as ActiveProject) : null
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.25, delay: 0.15 + idx * 0.04 }}
|
||||
>
|
||||
<div className="flex items-center gap-3 py-3 px-1 transition-colors hover:bg-muted/40 rounded-lg group">
|
||||
<ProjectLogo
|
||||
project={project}
|
||||
size="sm"
|
||||
fallback="initials"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-sm font-medium truncate group-hover:text-brand-blue transition-colors">
|
||||
{truncate(project.title, 50)}
|
||||
<Link
|
||||
href={`/admin/projects/${project.id}`}
|
||||
className="block"
|
||||
>
|
||||
<div className="flex items-center gap-3 py-3 px-1 transition-colors hover:bg-muted/40 rounded-lg group">
|
||||
<ProjectLogo
|
||||
project={project}
|
||||
size="sm"
|
||||
fallback="initials"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-sm font-medium truncate group-hover:text-brand-blue transition-colors">
|
||||
{truncate(project.title, 50)}
|
||||
</p>
|
||||
{activeProject?.latestScore != null ? (
|
||||
<span className="shrink-0 text-xs font-semibold tabular-nums text-brand-blue">
|
||||
{activeProject.latestScore.toFixed(1)}/10
|
||||
</span>
|
||||
) : (
|
||||
<StatusBadge
|
||||
status={project.status ?? 'SUBMITTED'}
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{isActiveMode && activeProject ? (
|
||||
<>
|
||||
{activeProject.latestEvaluator && (
|
||||
<span>{activeProject.latestEvaluator}</span>
|
||||
)}
|
||||
{activeProject.evaluatedAt && (
|
||||
<span> · {formatRelativeTime(activeProject.evaluatedAt)}</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
[
|
||||
project.teamName,
|
||||
project.country ? getCountryName(project.country) : null,
|
||||
formatDateOnly(project.submittedAt || project.createdAt),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' \u00b7 ')
|
||||
)}
|
||||
</p>
|
||||
<StatusBadge
|
||||
status={project.status ?? 'SUBMITTED'}
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{[
|
||||
project.teamName,
|
||||
project.country ? getCountryName(project.country) : null,
|
||||
formatDateOnly(project.submittedAt || project.createdAt),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' \u00b7 ')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</Link>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
99
src/components/dashboard/round-stats-deliberation.tsx
Normal file
99
src/components/dashboard/round-stats-deliberation.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'motion/react'
|
||||
|
||||
type PipelineRound = {
|
||||
id: string
|
||||
name: string
|
||||
roundType: string
|
||||
status: string
|
||||
projectStates: {
|
||||
PENDING: number
|
||||
IN_PROGRESS: number
|
||||
PASSED: number
|
||||
REJECTED: number
|
||||
COMPLETED: number
|
||||
WITHDRAWN: number
|
||||
total: number
|
||||
}
|
||||
deliberationCount: number
|
||||
}
|
||||
|
||||
type RoundStatsDeliberationProps = {
|
||||
round: PipelineRound
|
||||
}
|
||||
|
||||
export function RoundStatsDeliberation({ round }: RoundStatsDeliberationProps) {
|
||||
const { projectStates, deliberationCount } = round
|
||||
const decided = projectStates.PASSED + projectStates.REJECTED
|
||||
const decidedPct = projectStates.total > 0
|
||||
? ((decided / projectStates.total) * 100).toFixed(0)
|
||||
: '0'
|
||||
|
||||
const stats = [
|
||||
{
|
||||
value: deliberationCount,
|
||||
label: 'Sessions',
|
||||
detail: deliberationCount > 0 ? 'Deliberation sessions' : 'No sessions yet',
|
||||
accent: deliberationCount > 0 ? 'text-brand-blue' : 'text-amber-600',
|
||||
},
|
||||
{
|
||||
value: projectStates.total,
|
||||
label: 'Under review',
|
||||
detail: 'Projects in deliberation',
|
||||
accent: 'text-brand-blue',
|
||||
},
|
||||
{
|
||||
value: decided,
|
||||
label: 'Decided',
|
||||
detail: `${decidedPct}% resolved`,
|
||||
accent: 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
value: projectStates.PENDING,
|
||||
label: 'Pending',
|
||||
detail: projectStates.PENDING > 0 ? 'Awaiting vote' : 'All voted',
|
||||
accent: projectStates.PENDING > 0 ? 'text-amber-600' : 'text-emerald-600',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-widest text-muted-foreground/70">
|
||||
{round.name} — Deliberation
|
||||
</p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="flex items-baseline justify-between border-b border-t py-3 md:hidden"
|
||||
>
|
||||
{stats.map((s, i) => (
|
||||
<div key={i} className={`flex-1 text-center ${i > 0 ? 'border-l border-border/50' : ''}`}>
|
||||
<span className="text-xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mt-0.5">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<div className="grid grid-cols-4 gap-px rounded-lg bg-border/40 overflow-hidden">
|
||||
{stats.map((s, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.06 }}
|
||||
className="bg-background px-5 py-4 group hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className="text-3xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-xs font-medium text-muted-foreground mt-1">{s.label}</p>
|
||||
<p className={`text-xs mt-0.5 ${s.accent}`}>{s.detail}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
105
src/components/dashboard/round-stats-live-final.tsx
Normal file
105
src/components/dashboard/round-stats-live-final.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'motion/react'
|
||||
import { formatEnumLabel } from '@/lib/utils'
|
||||
|
||||
type PipelineRound = {
|
||||
id: string
|
||||
name: string
|
||||
roundType: string
|
||||
status: string
|
||||
projectStates: {
|
||||
PENDING: number
|
||||
IN_PROGRESS: number
|
||||
PASSED: number
|
||||
REJECTED: number
|
||||
COMPLETED: number
|
||||
WITHDRAWN: number
|
||||
total: number
|
||||
}
|
||||
liveSessionStatus: string | null
|
||||
assignmentCount: number
|
||||
}
|
||||
|
||||
type RoundStatsLiveFinalProps = {
|
||||
round: PipelineRound
|
||||
}
|
||||
|
||||
export function RoundStatsLiveFinal({ round }: RoundStatsLiveFinalProps) {
|
||||
const { projectStates, liveSessionStatus, assignmentCount } = round
|
||||
const sessionLabel = liveSessionStatus
|
||||
? formatEnumLabel(liveSessionStatus)
|
||||
: 'Not started'
|
||||
|
||||
const stats = [
|
||||
{
|
||||
value: projectStates.total,
|
||||
label: 'Presenting',
|
||||
detail: 'Projects in finals',
|
||||
accent: 'text-brand-blue',
|
||||
},
|
||||
{
|
||||
value: sessionLabel,
|
||||
label: 'Session',
|
||||
detail: liveSessionStatus ? 'Live session active' : 'No session yet',
|
||||
accent: liveSessionStatus ? 'text-emerald-600' : 'text-amber-600',
|
||||
isText: true,
|
||||
},
|
||||
{
|
||||
value: projectStates.COMPLETED,
|
||||
label: 'Scored',
|
||||
detail: `${projectStates.total - projectStates.COMPLETED} remaining`,
|
||||
accent: 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
value: assignmentCount,
|
||||
label: 'Jury votes',
|
||||
detail: 'Jury assignments',
|
||||
accent: 'text-brand-teal',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-widest text-muted-foreground/70">
|
||||
{round.name} — Live Finals
|
||||
</p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="flex items-baseline justify-between border-b border-t py-3 md:hidden"
|
||||
>
|
||||
{stats.map((s, i) => (
|
||||
<div key={i} className={`flex-1 text-center ${i > 0 ? 'border-l border-border/50' : ''}`}>
|
||||
<span className={`font-bold tabular-nums tracking-tight ${(s as any).isText ? 'text-sm' : 'text-xl'}`}>
|
||||
{s.value}
|
||||
</span>
|
||||
<p className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mt-0.5">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<div className="grid grid-cols-4 gap-px rounded-lg bg-border/40 overflow-hidden">
|
||||
{stats.map((s, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.06 }}
|
||||
className="bg-background px-5 py-4 group hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className={`font-bold tabular-nums tracking-tight ${(s as any).isText ? 'text-lg' : 'text-3xl'}`}>
|
||||
{s.value}
|
||||
</span>
|
||||
<p className="text-xs font-medium text-muted-foreground mt-1">{s.label}</p>
|
||||
<p className={`text-xs mt-0.5 ${s.accent}`}>{s.detail}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
99
src/components/dashboard/round-stats-mentoring.tsx
Normal file
99
src/components/dashboard/round-stats-mentoring.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'motion/react'
|
||||
|
||||
type PipelineRound = {
|
||||
id: string
|
||||
name: string
|
||||
roundType: string
|
||||
status: string
|
||||
projectStates: {
|
||||
PENDING: number
|
||||
IN_PROGRESS: number
|
||||
PASSED: number
|
||||
REJECTED: number
|
||||
COMPLETED: number
|
||||
WITHDRAWN: number
|
||||
total: number
|
||||
}
|
||||
assignmentCount: number
|
||||
}
|
||||
|
||||
type RoundStatsMentoringProps = {
|
||||
round: PipelineRound
|
||||
}
|
||||
|
||||
export function RoundStatsMentoring({ round }: RoundStatsMentoringProps) {
|
||||
const { projectStates, assignmentCount } = round
|
||||
const withMentor = projectStates.IN_PROGRESS + projectStates.COMPLETED + projectStates.PASSED
|
||||
const completedPct = projectStates.total > 0
|
||||
? ((projectStates.COMPLETED / projectStates.total) * 100).toFixed(0)
|
||||
: '0'
|
||||
|
||||
const stats = [
|
||||
{
|
||||
value: assignmentCount,
|
||||
label: 'Assignments',
|
||||
detail: 'Mentor-project pairs',
|
||||
accent: 'text-brand-blue',
|
||||
},
|
||||
{
|
||||
value: withMentor,
|
||||
label: 'With mentor',
|
||||
detail: withMentor > 0 ? 'Actively mentored' : 'None assigned',
|
||||
accent: withMentor > 0 ? 'text-emerald-600' : 'text-amber-600',
|
||||
},
|
||||
{
|
||||
value: projectStates.COMPLETED,
|
||||
label: 'Completed',
|
||||
detail: `${completedPct}% done`,
|
||||
accent: 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
value: projectStates.total,
|
||||
label: 'Total',
|
||||
detail: 'Projects in round',
|
||||
accent: 'text-brand-teal',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-widest text-muted-foreground/70">
|
||||
{round.name} — Mentoring
|
||||
</p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="flex items-baseline justify-between border-b border-t py-3 md:hidden"
|
||||
>
|
||||
{stats.map((s, i) => (
|
||||
<div key={i} className={`flex-1 text-center ${i > 0 ? 'border-l border-border/50' : ''}`}>
|
||||
<span className="text-xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mt-0.5">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<div className="grid grid-cols-4 gap-px rounded-lg bg-border/40 overflow-hidden">
|
||||
{stats.map((s, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.06 }}
|
||||
className="bg-background px-5 py-4 group hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className="text-3xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-xs font-medium text-muted-foreground mt-1">{s.label}</p>
|
||||
<p className={`text-xs mt-0.5 ${s.accent}`}>{s.detail}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
109
src/components/dashboard/round-stats-submission.tsx
Normal file
109
src/components/dashboard/round-stats-submission.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'motion/react'
|
||||
import { daysUntil } from '@/lib/utils'
|
||||
|
||||
type PipelineRound = {
|
||||
id: string
|
||||
name: string
|
||||
roundType: string
|
||||
status: string
|
||||
projectStates: {
|
||||
PENDING: number
|
||||
IN_PROGRESS: number
|
||||
PASSED: number
|
||||
REJECTED: number
|
||||
COMPLETED: number
|
||||
WITHDRAWN: number
|
||||
total: number
|
||||
}
|
||||
windowCloseAt: Date | null
|
||||
}
|
||||
|
||||
type RoundStatsSubmissionProps = {
|
||||
round: PipelineRound
|
||||
}
|
||||
|
||||
export function RoundStatsSubmission({ round }: RoundStatsSubmissionProps) {
|
||||
const { projectStates } = round
|
||||
const completedPct = projectStates.total > 0
|
||||
? ((projectStates.COMPLETED / projectStates.total) * 100).toFixed(0)
|
||||
: '0'
|
||||
|
||||
const deadlineDays = round.windowCloseAt ? daysUntil(new Date(round.windowCloseAt)) : null
|
||||
const deadlineLabel =
|
||||
deadlineDays === null
|
||||
? 'No deadline'
|
||||
: deadlineDays <= 0
|
||||
? 'Closed'
|
||||
: deadlineDays === 1
|
||||
? '1 day left'
|
||||
: `${deadlineDays} days left`
|
||||
|
||||
const stats = [
|
||||
{
|
||||
value: projectStates.total,
|
||||
label: 'In round',
|
||||
detail: 'Total projects',
|
||||
accent: 'text-brand-blue',
|
||||
},
|
||||
{
|
||||
value: projectStates.COMPLETED,
|
||||
label: 'Completed',
|
||||
detail: `${completedPct}% done`,
|
||||
accent: 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
value: projectStates.IN_PROGRESS,
|
||||
label: 'In progress',
|
||||
detail: projectStates.IN_PROGRESS > 0 ? 'Working on submissions' : 'None in progress',
|
||||
accent: projectStates.IN_PROGRESS > 0 ? 'text-amber-600' : 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
value: deadlineDays ?? '—',
|
||||
label: 'Deadline',
|
||||
detail: deadlineLabel,
|
||||
accent: deadlineDays !== null && deadlineDays <= 3 ? 'text-red-600' : 'text-brand-teal',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-widest text-muted-foreground/70">
|
||||
{round.name} — Submission
|
||||
</p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="flex items-baseline justify-between border-b border-t py-3 md:hidden"
|
||||
>
|
||||
{stats.map((s, i) => (
|
||||
<div key={i} className={`flex-1 text-center ${i > 0 ? 'border-l border-border/50' : ''}`}>
|
||||
<span className="text-xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mt-0.5">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<div className="grid grid-cols-4 gap-px rounded-lg bg-border/40 overflow-hidden">
|
||||
{stats.map((s, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.06 }}
|
||||
className="bg-background px-5 py-4 group hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className="text-3xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-xs font-medium text-muted-foreground mt-1">{s.label}</p>
|
||||
<p className={`text-xs mt-0.5 ${s.accent}`}>{s.detail}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { motion } from 'motion/react'
|
||||
|
||||
type RoundStatsGenericProps = {
|
||||
type RoundStatsSummaryProps = {
|
||||
projectCount: number
|
||||
newProjectsThisWeek: number
|
||||
totalJurors: number
|
||||
@@ -10,52 +10,58 @@ type RoundStatsGenericProps = {
|
||||
totalAssignments: number
|
||||
evaluationStats: Array<{ status: string; _count: number }>
|
||||
actionsCount: number
|
||||
nextDraftRound?: { name: string; roundType: string } | null
|
||||
}
|
||||
|
||||
export function RoundStatsGeneric({
|
||||
export function RoundStatsSummary({
|
||||
projectCount,
|
||||
newProjectsThisWeek,
|
||||
totalJurors,
|
||||
activeJurors,
|
||||
totalAssignments,
|
||||
evaluationStats,
|
||||
actionsCount,
|
||||
}: RoundStatsGenericProps) {
|
||||
nextDraftRound,
|
||||
}: RoundStatsSummaryProps) {
|
||||
const submittedCount =
|
||||
evaluationStats.find((e) => e.status === 'SUBMITTED')?._count ?? 0
|
||||
const completionPct =
|
||||
totalAssignments > 0 ? ((submittedCount / totalAssignments) * 100).toFixed(0) : '0'
|
||||
totalAssignments > 0 ? ((submittedCount / totalAssignments) * 100).toFixed(0) : '—'
|
||||
|
||||
const stats = [
|
||||
{
|
||||
value: projectCount,
|
||||
label: 'Projects',
|
||||
detail: newProjectsThisWeek > 0 ? `+${newProjectsThisWeek} this week` : null,
|
||||
label: 'Total projects',
|
||||
detail: 'In this edition',
|
||||
accent: 'text-brand-blue',
|
||||
},
|
||||
{
|
||||
value: totalJurors,
|
||||
label: 'Jurors',
|
||||
detail: `${activeJurors} active`,
|
||||
value: `${activeJurors}/${totalJurors}`,
|
||||
label: 'Jury coverage',
|
||||
detail: totalJurors > 0 ? `${activeJurors} active jurors` : 'No jurors assigned',
|
||||
accent: 'text-brand-teal',
|
||||
},
|
||||
{
|
||||
value: `${submittedCount}/${totalAssignments}`,
|
||||
label: 'Evaluations',
|
||||
detail: `${completionPct}% complete`,
|
||||
value: totalAssignments > 0 ? `${completionPct}%` : '—',
|
||||
label: 'Completion',
|
||||
detail: totalAssignments > 0 ? `${submittedCount}/${totalAssignments} evaluations` : 'No evaluations yet',
|
||||
accent: 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
value: actionsCount,
|
||||
label: actionsCount === 1 ? 'Action' : 'Actions',
|
||||
detail: actionsCount > 0 ? 'Pending' : 'All clear',
|
||||
detail: nextDraftRound
|
||||
? `Next: ${nextDraftRound.name}`
|
||||
: actionsCount > 0 ? 'Pending' : 'All clear',
|
||||
accent: actionsCount > 0 ? 'text-amber-600' : 'text-emerald-600',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile: horizontal data strip */}
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-widest text-muted-foreground/70">
|
||||
No active round — Competition Summary
|
||||
</p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
@@ -70,7 +76,6 @@ export function RoundStatsGeneric({
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Desktop: editorial stat row */}
|
||||
<div className="hidden md:block">
|
||||
<div className="grid grid-cols-4 gap-px rounded-lg bg-border/40 overflow-hidden">
|
||||
{stats.map((s, i) => (
|
||||
@@ -81,13 +86,9 @@ export function RoundStatsGeneric({
|
||||
transition={{ duration: 0.3, delay: i * 0.06 }}
|
||||
className="bg-background px-5 py-4 group hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
</div>
|
||||
<span className="text-3xl font-bold tabular-nums tracking-tight">{s.value}</span>
|
||||
<p className="text-xs font-medium text-muted-foreground mt-1">{s.label}</p>
|
||||
{s.detail && (
|
||||
<p className={`text-xs mt-0.5 ${s.accent}`}>{s.detail}</p>
|
||||
)}
|
||||
<p className={`text-xs mt-0.5 ${s.accent}`}>{s.detail}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
@@ -3,7 +3,11 @@
|
||||
import { RoundStatsIntake } from '@/components/dashboard/round-stats-intake'
|
||||
import { RoundStatsFiltering } from '@/components/dashboard/round-stats-filtering'
|
||||
import { RoundStatsEvaluation } from '@/components/dashboard/round-stats-evaluation'
|
||||
import { RoundStatsGeneric } from '@/components/dashboard/round-stats-generic'
|
||||
import { RoundStatsSubmission } from '@/components/dashboard/round-stats-submission'
|
||||
import { RoundStatsMentoring } from '@/components/dashboard/round-stats-mentoring'
|
||||
import { RoundStatsLiveFinal } from '@/components/dashboard/round-stats-live-final'
|
||||
import { RoundStatsDeliberation } from '@/components/dashboard/round-stats-deliberation'
|
||||
import { RoundStatsSummary } from '@/components/dashboard/round-stats-summary'
|
||||
|
||||
type PipelineRound = {
|
||||
id: string
|
||||
@@ -37,6 +41,7 @@ type PipelineRound = {
|
||||
|
||||
type RoundStatsProps = {
|
||||
activeRound: PipelineRound | null
|
||||
allActiveRounds?: PipelineRound[]
|
||||
projectCount: number
|
||||
newProjectsThisWeek: number
|
||||
totalJurors: number
|
||||
@@ -44,6 +49,7 @@ type RoundStatsProps = {
|
||||
totalAssignments: number
|
||||
evaluationStats: Array<{ status: string; _count: number }>
|
||||
actionsCount: number
|
||||
nextDraftRound?: { name: string; roundType: string } | null
|
||||
}
|
||||
|
||||
export function RoundStats({
|
||||
@@ -55,10 +61,11 @@ export function RoundStats({
|
||||
totalAssignments,
|
||||
evaluationStats,
|
||||
actionsCount,
|
||||
nextDraftRound,
|
||||
}: RoundStatsProps) {
|
||||
if (!activeRound) {
|
||||
return (
|
||||
<RoundStatsGeneric
|
||||
<RoundStatsSummary
|
||||
projectCount={projectCount}
|
||||
newProjectsThisWeek={newProjectsThisWeek}
|
||||
totalJurors={totalJurors}
|
||||
@@ -66,6 +73,7 @@ export function RoundStats({
|
||||
totalAssignments={totalAssignments}
|
||||
evaluationStats={evaluationStats}
|
||||
actionsCount={actionsCount}
|
||||
nextDraftRound={nextDraftRound}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -89,9 +97,25 @@ export function RoundStats({
|
||||
activeJurors={activeJurors}
|
||||
/>
|
||||
)
|
||||
case 'SUBMISSION':
|
||||
return (
|
||||
<RoundStatsSubmission round={activeRound} />
|
||||
)
|
||||
case 'MENTORING':
|
||||
return (
|
||||
<RoundStatsMentoring round={activeRound} />
|
||||
)
|
||||
case 'LIVE_FINAL':
|
||||
return (
|
||||
<RoundStatsLiveFinal round={activeRound} />
|
||||
)
|
||||
case 'DELIBERATION':
|
||||
return (
|
||||
<RoundStatsDeliberation round={activeRound} />
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<RoundStatsGeneric
|
||||
<RoundStatsSummary
|
||||
projectCount={projectCount}
|
||||
newProjectsThisWeek={newProjectsThisWeek}
|
||||
totalJurors={totalJurors}
|
||||
@@ -99,6 +123,7 @@ export function RoundStats({
|
||||
totalAssignments={totalAssignments}
|
||||
evaluationStats={evaluationStats}
|
||||
actionsCount={actionsCount}
|
||||
nextDraftRound={nextDraftRound}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user