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
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:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user