feat: show awards on jury dashboard and add project details to award voting
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m27s

- Jury dashboard now shows active award voting banners with project
  count, deadline countdown, and direct link to vote
- Award voting page shows full project details: description, team
  members, tags, and downloadable files in expandable cards
- Award jurors can now download files for eligible projects (added
  awardJuror access check to file.getDownloadUrl)
- Backend query enhanced to include files and team members

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-04-14 12:28:53 -04:00
parent acd75427b3
commit 0987d49817
4 changed files with 394 additions and 86 deletions

View File

@@ -27,6 +27,7 @@ import {
BarChart3,
Waves,
Send,
Trophy,
} from 'lucide-react'
import { formatDateOnly } from '@/lib/utils'
import { CountdownTimer } from '@/components/shared/countdown-timer'
@@ -49,8 +50,8 @@ async function JuryDashboardContent() {
return null
}
// Get assignments, grace periods, and feature flags in parallel
const [assignments, gracePeriods, compareFlag] = await Promise.all([
// Get assignments, grace periods, feature flags, and award juror records in parallel
const [assignments, gracePeriods, compareFlag, myAwardJurorRecords] = await Promise.all([
prisma.assignment.findMany({
where: { userId },
include: {
@@ -109,10 +110,36 @@ async function JuryDashboardContent() {
},
}),
prisma.systemSettings.findUnique({ where: { key: 'jury_compare_enabled' } }),
prisma.awardJuror.findMany({
where: { userId },
include: {
award: {
select: {
id: true,
name: true,
description: true,
status: true,
votingStartAt: true,
votingEndAt: true,
scoringMode: true,
maxRankedPicks: true,
_count: { select: { eligibilities: { where: { eligible: true } } } },
},
},
},
}),
])
const juryCompareEnabled = compareFlag?.value === 'true'
// Awards where voting is open
const activeAwards = myAwardJurorRecords.filter(
(r) => r.award.status === 'VOTING_OPEN'
)
const upcomingAwards = myAwardJurorRecords.filter(
(r) => r.award.status === 'NOMINATIONS_OPEN'
)
// Calculate stats
const totalAssignments = assignments.length
const completedAssignments = assignments.filter(
@@ -216,8 +243,8 @@ async function JuryDashboardContent() {
},
]
// Zero-assignment state: compact welcome card
if (totalAssignments === 0) {
// Zero-assignment state: compact welcome card (but still show awards if any)
if (totalAssignments === 0 && activeAwards.length === 0 && upcomingAwards.length === 0) {
return (
<AnimatedCard index={0}>
<Card className="overflow-hidden">
@@ -268,6 +295,67 @@ async function JuryDashboardContent() {
return (
<>
{/* Active Awards Banner */}
{activeAwards.length > 0 && (
<AnimatedCard index={0}>
<Card className="overflow-hidden border-0 shadow-lg">
<div className="bg-gradient-to-r from-amber-400 to-amber-500 p-[1px] rounded-lg">
<div className="rounded-[7px] bg-background">
<CardHeader className="pb-2 pt-4 px-5">
<div className="flex items-center gap-2.5">
<div className="rounded-lg bg-amber-100 p-1.5 dark:bg-amber-900/40">
<Trophy className="h-4 w-4 text-amber-600 dark:text-amber-400" />
</div>
<CardTitle className="text-lg">Special Awards Voting Open</CardTitle>
</div>
</CardHeader>
<CardContent className="px-5 pb-4 space-y-3">
{activeAwards.map((record) => {
const award = record.award
const deadline = award.votingEndAt ? new Date(award.votingEndAt) : null
const isUrgent = deadline && (deadline.getTime() - now.getTime()) < 24 * 60 * 60 * 1000
return (
<div
key={award.id}
className={cn(
'rounded-xl border p-4 space-y-3 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md',
isUrgent
? 'border-red-200 bg-red-50/50 dark:border-red-900 dark:bg-red-950/20'
: 'border-amber-200/60 bg-amber-50/30 dark:border-amber-800/40 dark:bg-amber-950/10'
)}
>
<div className="flex items-start justify-between">
<div>
<h3 className="font-semibold text-amber-700 dark:text-amber-400">{award.name}</h3>
<p className="text-xs text-muted-foreground mt-0.5">
{award._count.eligibilities} project{award._count.eligibilities !== 1 ? 's' : ''} to review
{record.isChair && ' · You are the Chair'}
</p>
</div>
<Badge className="bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-700">
Vote Now
</Badge>
</div>
{deadline && (
<CountdownTimer deadline={deadline} label="Voting closes:" />
)}
<Button asChild size="sm" className="w-full bg-amber-600 hover:bg-amber-700 text-white shadow-sm">
<Link href={`/jury/awards/${award.id}`}>
Review & Vote
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
)
})}
</CardContent>
</div>
</div>
</Card>
</AnimatedCard>
)}
{/* Hero CTA - Jump to next evaluation */}
{nextUnevaluated && activeRemaining > 0 && (
<AnimatedCard index={0}>