feat: show vote status on jury dashboard and add logos to award-master
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m47s

- Jury dashboard now shows "Submitted" badge (green) with "Edit
  Rankings" button when juror has already voted, instead of always
  showing "Vote Now" — prevents confusion about whether vote saved
- Award-master page now shows project logos next to project names
- Backend getMyAwardDetailEnhanced now returns logo URLs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-04-14 13:09:34 -04:00
parent 3a6a9a2b45
commit f1955b68f9
3 changed files with 46 additions and 14 deletions

View File

@@ -42,6 +42,7 @@ import {
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { CountryDisplay } from '@/components/shared/country-display' import { CountryDisplay } from '@/components/shared/country-display'
import { ProjectFilesSection } from '@/components/jury/project-files-section' import { ProjectFilesSection } from '@/components/jury/project-files-section'
import { ProjectLogo } from '@/components/shared/project-logo'
export default function AwardMasterVotingPage({ export default function AwardMasterVotingPage({
params, params,
@@ -254,7 +255,8 @@ export default function AwardMasterVotingPage({
onClick={() => handleProjectClick(project.id)} onClick={() => handleProjectClick(project.id)}
> >
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex items-start justify-between"> <div className="flex items-start gap-3">
<ProjectLogo project={project} logoUrl={project.logoUrl} size="sm" fallback="initials" className="mt-0.5" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<CardTitle className="text-base"> <CardTitle className="text-base">
{project.title} {project.title}

View File

@@ -132,6 +132,17 @@ async function JuryDashboardContent() {
const juryCompareEnabled = compareFlag?.value === 'true' const juryCompareEnabled = compareFlag?.value === 'true'
// Check which awards the user has already voted on
const awardIds = myAwardJurorRecords.map((r) => r.award.id)
const myAwardVotes = awardIds.length > 0
? await prisma.awardVote.groupBy({
by: ['awardId'],
where: { userId, awardId: { in: awardIds } },
_count: { id: true },
})
: []
const votedAwardIds = new Set(myAwardVotes.filter((v) => v._count.id > 0).map((v) => v.awardId))
// Awards where voting is open // Awards where voting is open
const activeAwards = myAwardJurorRecords.filter( const activeAwards = myAwardJurorRecords.filter(
(r) => r.award.status === 'VOTING_OPEN' (r) => r.award.status === 'VOTING_OPEN'
@@ -314,35 +325,50 @@ async function JuryDashboardContent() {
const award = record.award const award = record.award
const deadline = award.votingEndAt ? new Date(award.votingEndAt) : null const deadline = award.votingEndAt ? new Date(award.votingEndAt) : null
const isUrgent = deadline && (deadline.getTime() - now.getTime()) < 24 * 60 * 60 * 1000 const isUrgent = deadline && (deadline.getTime() - now.getTime()) < 24 * 60 * 60 * 1000
const hasVoted = votedAwardIds.has(award.id)
return ( return (
<div <div
key={award.id} key={award.id}
className={cn( className={cn(
'rounded-xl border p-4 space-y-3 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md', 'rounded-xl border p-4 space-y-3 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md',
isUrgent hasVoted
? 'border-red-200 bg-red-50/50 dark:border-red-900 dark:bg-red-950/20' ? 'border-green-200/60 bg-green-50/30 dark:border-green-800/40 dark:bg-green-950/10'
: 'border-amber-200/60 bg-amber-50/30 dark:border-amber-800/40 dark:bg-amber-950/10' : 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 className="flex items-start justify-between">
<div> <div>
<h3 className="font-semibold text-amber-700 dark:text-amber-400">{award.name}</h3> <h3 className={cn('font-semibold', hasVoted ? 'text-green-700 dark:text-green-400' : 'text-amber-700 dark:text-amber-400')}>{award.name}</h3>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
{award._count.eligibilities} project{award._count.eligibilities !== 1 ? 's' : ''} to review {award._count.eligibilities} project{award._count.eligibilities !== 1 ? 's' : ''} to review
{record.isChair && ' · You are the Chair'} {record.isChair && ' · You are the Chair'}
</p> </p>
</div> </div>
<Badge className="bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-700"> {hasVoted ? (
Vote Now <Badge className="bg-green-100 text-green-800 border-green-300 dark:bg-green-950 dark:text-green-300 dark:border-green-700">
</Badge> <CheckCircle2 className="mr-1 h-3 w-3" />
Submitted
</Badge>
) : (
<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> </div>
{deadline && ( {deadline && (
<CountdownTimer deadline={deadline} label="Voting closes:" /> <CountdownTimer deadline={deadline} label="Voting closes:" />
)} )}
<Button asChild size="sm" className="w-full bg-amber-600 hover:bg-amber-700 text-white shadow-sm"> <Button asChild size="sm" className={cn(
'w-full shadow-sm',
hasVoted
? 'bg-green-600 hover:bg-green-700 text-white'
: 'bg-amber-600 hover:bg-amber-700 text-white'
)}>
<Link href={`/jury/awards/${award.id}`}> <Link href={`/jury/awards/${award.id}`}>
Review & Vote {hasVoted ? 'Edit Rankings' : 'Review & Vote'}
<ArrowRight className="ml-2 h-4 w-4" /> <ArrowRight className="ml-2 h-4 w-4" />
</Link> </Link>
</Button> </Button>

View File

@@ -818,6 +818,7 @@ export const specialAwardRouter = router({
select: { select: {
id: true, title: true, teamName: true, description: true, id: true, title: true, teamName: true, description: true,
competitionCategory: true, country: true, tags: true, competitionCategory: true, country: true, tags: true,
logoKey: true, logoProvider: true,
}, },
}, },
}, },
@@ -886,12 +887,15 @@ export const specialAwardRouter = router({
})) }))
} }
const projectsWithScores = eligibleProjects.map((e) => ({
...e.project,
evaluationScore: projectScores[e.project.id] ?? null,
}))
const projectsWithLogos = await attachProjectLogoUrls(projectsWithScores)
return { return {
award, award,
projects: eligibleProjects.map((e) => ({ projects: projectsWithLogos,
...e.project,
evaluationScore: projectScores[e.project.id] ?? null,
})),
myVotes, myVotes,
isChair, isChair,
otherVotes, otherVotes,