All checks were successful
Build and Push Docker Image / build (push) Successful in 8m53s
- Fix project documents not displaying on jury project page (rewrote MultiWindowDocViewer to use file.listByProject) - Add working download/preview for project files via presigned URLs - Display project tags on jury project detail page - Add autosave for evaluation drafts (debounced 3s + save on unmount/beforeunload) - Support mixed criterion types: numeric scores, yes/no booleans, text responses, section headers - Replace inline criteria editor with rich EvaluationFormBuilder on admin round page - Remove COI dialog from evaluation page - Update AI summary service to handle boolean/text criteria (yes/no counts, text synthesis) - Update EvaluationSummaryCard to show boolean criteria bars and text responses - Add evaluation detail sheet on admin project page (click juror row to view full scores + feedback) - Add Recent Evaluations dashboard widget showing latest jury reviews Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
205 lines
6.4 KiB
TypeScript
205 lines
6.4 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { trpc } from '@/lib/trpc/client'
|
|
import { Card, CardContent } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
CircleDot,
|
|
AlertTriangle,
|
|
Upload,
|
|
UserPlus,
|
|
} from 'lucide-react'
|
|
import { GeographicSummaryCard } from '@/components/charts'
|
|
import { AnimatedCard } from '@/components/shared/animated-container'
|
|
import { motion } from 'motion/react'
|
|
|
|
import { CompetitionPipeline } from '@/components/dashboard/competition-pipeline'
|
|
import { RoundStats } from '@/components/dashboard/round-stats'
|
|
import { ActiveRoundPanel } from '@/components/dashboard/active-round-panel'
|
|
import { SmartActions } from '@/components/dashboard/smart-actions'
|
|
import { ProjectListCompact } from '@/components/dashboard/project-list-compact'
|
|
import { ActivityFeed } from '@/components/dashboard/activity-feed'
|
|
import { CategoryBreakdown } from '@/components/dashboard/category-breakdown'
|
|
import { DashboardSkeleton } from '@/components/dashboard/dashboard-skeleton'
|
|
import { RecentEvaluations } from '@/components/dashboard/recent-evaluations'
|
|
|
|
type DashboardContentProps = {
|
|
editionId: string
|
|
sessionName: string
|
|
}
|
|
|
|
export function DashboardContent({ editionId, sessionName }: DashboardContentProps) {
|
|
const { data, isLoading, error } = trpc.dashboard.getStats.useQuery(
|
|
{ editionId },
|
|
{ enabled: !!editionId, retry: 1, refetchInterval: 30_000 }
|
|
)
|
|
const { data: recentEvals } = trpc.dashboard.getRecentEvaluations.useQuery(
|
|
{ editionId, limit: 8 },
|
|
{ enabled: !!editionId, refetchInterval: 30_000 }
|
|
)
|
|
|
|
if (isLoading) {
|
|
return <DashboardSkeleton />
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
|
<AlertTriangle className="h-12 w-12 text-destructive/50" />
|
|
<p className="mt-2 font-medium">Failed to load dashboard</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{error.message || 'An unexpected error occurred. Please try refreshing the page.'}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
if (!data) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
|
<CircleDot className="h-12 w-12 text-muted-foreground/50" />
|
|
<p className="mt-2 font-medium">Edition not found</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
The selected edition could not be found
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
const {
|
|
edition,
|
|
pipelineRounds,
|
|
activeRoundId,
|
|
nextActions,
|
|
projectCount,
|
|
newProjectsThisWeek,
|
|
totalJurors,
|
|
activeJurors,
|
|
evaluationStats,
|
|
totalAssignments,
|
|
latestProjects,
|
|
categoryBreakdown,
|
|
oceanIssueBreakdown,
|
|
recentActivity,
|
|
} = data
|
|
|
|
const activeRound = activeRoundId
|
|
? pipelineRounds.find((r) => r.id === activeRoundId) ?? null
|
|
: null
|
|
|
|
return (
|
|
<>
|
|
{/* Page Header */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -6 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.3 }}
|
|
className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
|
>
|
|
<div>
|
|
<h1 className="text-xl font-bold tracking-tight md:text-2xl">
|
|
{edition.name} {edition.year}
|
|
</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
Welcome back, {sessionName}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Link href="/admin/rounds">
|
|
<Button size="sm" variant="outline">
|
|
<CircleDot className="mr-1.5 h-3.5 w-3.5" />
|
|
Rounds
|
|
</Button>
|
|
</Link>
|
|
<Link href="/admin/projects/new">
|
|
<Button size="sm" variant="outline">
|
|
<Upload className="mr-1.5 h-3.5 w-3.5" />
|
|
Import
|
|
</Button>
|
|
</Link>
|
|
<Link href="/admin/members">
|
|
<Button size="sm" variant="outline">
|
|
<UserPlus className="mr-1.5 h-3.5 w-3.5" />
|
|
Invite
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Competition Pipeline */}
|
|
<AnimatedCard index={0}>
|
|
<CompetitionPipeline rounds={pipelineRounds} />
|
|
</AnimatedCard>
|
|
|
|
{/* Round-Specific Stats */}
|
|
<AnimatedCard index={1}>
|
|
<RoundStats
|
|
activeRound={activeRound}
|
|
projectCount={projectCount}
|
|
newProjectsThisWeek={newProjectsThisWeek}
|
|
totalJurors={totalJurors}
|
|
activeJurors={activeJurors}
|
|
totalAssignments={totalAssignments}
|
|
evaluationStats={evaluationStats}
|
|
actionsCount={nextActions.length}
|
|
/>
|
|
</AnimatedCard>
|
|
|
|
{/* Two-Column Layout */}
|
|
<div className="grid gap-6 lg:grid-cols-12">
|
|
{/* Left Column */}
|
|
<div className="space-y-6 lg:col-span-8">
|
|
{activeRound && (
|
|
<AnimatedCard index={2}>
|
|
<ActiveRoundPanel round={activeRound} />
|
|
</AnimatedCard>
|
|
)}
|
|
|
|
<AnimatedCard index={3}>
|
|
<ProjectListCompact projects={latestProjects} />
|
|
</AnimatedCard>
|
|
|
|
{recentEvals && recentEvals.length > 0 && (
|
|
<AnimatedCard index={4}>
|
|
<RecentEvaluations evaluations={recentEvals} />
|
|
</AnimatedCard>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right Column */}
|
|
<div className="space-y-6 lg:col-span-4">
|
|
<AnimatedCard index={5}>
|
|
<SmartActions actions={nextActions} />
|
|
</AnimatedCard>
|
|
|
|
<AnimatedCard index={6}>
|
|
<ActivityFeed activity={recentActivity} />
|
|
</AnimatedCard>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bottom Full Width */}
|
|
<div className="grid gap-6 lg:grid-cols-12">
|
|
<div className="lg:col-span-8">
|
|
<AnimatedCard index={7}>
|
|
<GeographicSummaryCard programId={editionId} />
|
|
</AnimatedCard>
|
|
</div>
|
|
<div className="lg:col-span-4">
|
|
<AnimatedCard index={8}>
|
|
<CategoryBreakdown
|
|
categories={categoryBreakdown}
|
|
issues={oceanIssueBreakdown}
|
|
/>
|
|
</AnimatedCard>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|