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