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:
@@ -20,9 +20,21 @@ import {
|
||||
CheckCircle2,
|
||||
Loader2,
|
||||
GripVertical,
|
||||
ChevronDown,
|
||||
FileText,
|
||||
Download,
|
||||
Users,
|
||||
MapPin,
|
||||
Tag,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CountryDisplay } from '@/components/shared/country-display'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
export default function JuryAwardVotingPage({
|
||||
params,
|
||||
@@ -45,6 +57,30 @@ export default function JuryAwardVotingPage({
|
||||
null
|
||||
)
|
||||
const [rankedIds, setRankedIds] = useState<string[]>([])
|
||||
const [expandedProjects, setExpandedProjects] = useState<Set<string>>(new Set())
|
||||
|
||||
const toggleExpanded = (projectId: string) => {
|
||||
setExpandedProjects((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(projectId)) next.delete(projectId)
|
||||
else next.add(projectId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleDownload = async (objectKey: string, fileName: string) => {
|
||||
try {
|
||||
const result = await utils.file.getDownloadUrl.fetch({
|
||||
bucket: 'mopc',
|
||||
objectKey,
|
||||
fileName,
|
||||
forDownload: true,
|
||||
})
|
||||
if (result.url) window.open(result.url, '_blank')
|
||||
} catch {
|
||||
toast.error('Failed to download file')
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize from existing votes
|
||||
if (data && !selectedProjectId && !rankedIds.length && data.myVotes.length > 0) {
|
||||
@@ -165,39 +201,19 @@ export default function JuryAwardVotingPage({
|
||||
/* PICK_WINNER Mode */
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Select one project as the winner
|
||||
Select one project as the winner. Click the project header to expand details.
|
||||
</p>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-3">
|
||||
{projects.map((project) => (
|
||||
<Card
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
className={cn(
|
||||
'cursor-pointer transition-all',
|
||||
selectedProjectId === project.id
|
||||
? 'ring-2 ring-primary bg-primary/5'
|
||||
: 'hover:bg-muted/50'
|
||||
)}
|
||||
onClick={() => setSelectedProjectId(project.id)}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">{project.title}</CardTitle>
|
||||
<CardDescription>{project.teamName}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{project.competitionCategory && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{project.competitionCategory.replace('_', ' ')}
|
||||
</Badge>
|
||||
)}
|
||||
{project.country && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<CountryDisplay country={project.country} />
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
project={project}
|
||||
isSelected={selectedProjectId === project.id}
|
||||
isExpanded={expandedProjects.has(project.id)}
|
||||
onSelect={() => setSelectedProjectId(project.id)}
|
||||
onToggleExpand={() => toggleExpanded(project.id)}
|
||||
onDownload={handleDownload}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
@@ -219,42 +235,51 @@ export default function JuryAwardVotingPage({
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Select and rank your top {award.maxRankedPicks || 5} projects. Click
|
||||
to add/remove, drag to reorder.
|
||||
to add/remove from your rankings. Expand each project to see full details.
|
||||
</p>
|
||||
|
||||
{/* Selected rankings */}
|
||||
{rankedIds.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Your Rankings</CardTitle>
|
||||
<CardTitle className="text-base">Your Rankings ({rankedIds.length}/{award.maxRankedPicks || 5})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{rankedIds.map((id, index) => {
|
||||
const project = projects.find((p) => p.id === id)
|
||||
if (!project) return null
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className="flex items-center gap-3 rounded-lg border p-3"
|
||||
>
|
||||
<span className="font-bold text-lg w-8 text-center">
|
||||
{index + 1}
|
||||
</span>
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{project.title}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{project.teamName}
|
||||
</p>
|
||||
<Collapsible key={id} open={expandedProjects.has(id)}>
|
||||
<div className="flex items-center gap-3 rounded-lg border p-3">
|
||||
<span className="font-bold text-lg w-8 text-center text-brand-blue">
|
||||
{index + 1}
|
||||
</span>
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{project.title}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{project.teamName}
|
||||
{project.country && <> · <CountryDisplay country={project.country} /></>}
|
||||
</p>
|
||||
</div>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="sm" onClick={() => toggleExpanded(id)}>
|
||||
<ChevronDown className={cn('h-4 w-4 transition-transform', expandedProjects.has(id) && 'rotate-180')} />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => toggleRanked(id)}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => toggleRanked(id)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<CollapsibleContent>
|
||||
<ProjectDetails project={project} onDownload={handleDownload} />
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
@@ -262,36 +287,20 @@ export default function JuryAwardVotingPage({
|
||||
)}
|
||||
|
||||
{/* Available projects */}
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">Available Projects</h3>
|
||||
<div className="space-y-3">
|
||||
{projects
|
||||
.filter((p) => !rankedIds.includes(p.id))
|
||||
.map((project) => (
|
||||
<Card
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
onClick={() => toggleRanked(project.id)}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">
|
||||
{project.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{project.teamName}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{project.competitionCategory && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{project.competitionCategory.replace('_', ' ')}
|
||||
</Badge>
|
||||
)}
|
||||
{project.country && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<CountryDisplay country={project.country} />
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
project={project}
|
||||
isExpanded={expandedProjects.has(project.id)}
|
||||
onSelect={() => toggleRanked(project.id)}
|
||||
onToggleExpand={() => toggleExpanded(project.id)}
|
||||
onDownload={handleDownload}
|
||||
selectLabel={rankedIds.length < (award.maxRankedPicks || 5) ? `Add to #${rankedIds.length + 1}` : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -325,3 +334,182 @@ export default function JuryAwardVotingPage({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ── Shared project detail components ── */
|
||||
|
||||
type ProjectData = {
|
||||
id: string
|
||||
title: string
|
||||
teamName: string | null
|
||||
description: string | null
|
||||
competitionCategory: string | null
|
||||
country: string | null
|
||||
tags: string[]
|
||||
files: Array<{
|
||||
id: string
|
||||
fileName: string
|
||||
fileType: string
|
||||
bucket: string
|
||||
objectKey: string
|
||||
createdAt: Date
|
||||
}>
|
||||
teamMembers: Array<{
|
||||
id: string
|
||||
role: string
|
||||
user: { name: string | null; email: string }
|
||||
}>
|
||||
}
|
||||
|
||||
function ProjectDetails({
|
||||
project,
|
||||
onDownload,
|
||||
}: {
|
||||
project: ProjectData
|
||||
onDownload: (storageKey: string, fileName: string) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="px-4 pb-4 pt-2 space-y-3 border-t mt-2">
|
||||
{project.description && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground whitespace-pre-line">{project.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{project.tags.length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Tag className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
{project.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{project.teamMembers.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Users className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">Team Members</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.teamMembers.map((m) => (
|
||||
<Badge key={m.id} variant="outline" className="text-xs">
|
||||
{m.user.name || m.user.email}
|
||||
{m.role !== 'MEMBER' && ` (${m.role.toLowerCase()})`}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{project.files.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<FileText className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
Documents ({project.files.length})
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{project.files.map((file) => (
|
||||
<button
|
||||
key={file.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDownload(file.objectKey, file.fileName)
|
||||
}}
|
||||
className="flex items-center gap-2 w-full text-left rounded-md px-2 py-1.5 text-sm hover:bg-muted/50 transition-colors group"
|
||||
>
|
||||
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="truncate flex-1">{file.fileName}</span>
|
||||
<Badge variant="outline" className="text-[10px] shrink-0">
|
||||
{file.fileType.replace('_', ' ')}
|
||||
</Badge>
|
||||
<Download className="h-3.5 w-3.5 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!project.description && project.tags.length === 0 && project.files.length === 0 && project.teamMembers.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground italic">No additional details available</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ProjectCard({
|
||||
project,
|
||||
isSelected,
|
||||
isExpanded,
|
||||
onSelect,
|
||||
onToggleExpand,
|
||||
onDownload,
|
||||
selectLabel,
|
||||
}: {
|
||||
project: ProjectData
|
||||
isSelected?: boolean
|
||||
isExpanded: boolean
|
||||
onSelect: () => void
|
||||
onToggleExpand: () => void
|
||||
onDownload: (storageKey: string, fileName: string) => void
|
||||
selectLabel?: string
|
||||
}) {
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
'transition-all',
|
||||
isSelected ? 'ring-2 ring-primary bg-primary/5' : ''
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3 p-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<button
|
||||
onClick={onToggleExpand}
|
||||
className="flex items-center gap-2 text-left w-full group"
|
||||
>
|
||||
<ChevronDown className={cn(
|
||||
'h-4 w-4 text-muted-foreground transition-transform shrink-0',
|
||||
isExpanded && 'rotate-180'
|
||||
)} />
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-semibold text-sm group-hover:text-brand-blue dark:group-hover:text-brand-teal transition-colors">
|
||||
{project.title}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{project.teamName}
|
||||
{project.country && <> · <CountryDisplay country={project.country} /></>}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex flex-wrap gap-1 mt-2 ml-6">
|
||||
{project.competitionCategory && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{project.competitionCategory.replace('_', ' ')}
|
||||
</Badge>
|
||||
)}
|
||||
{project.files.length > 0 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<FileText className="mr-1 h-3 w-3" />
|
||||
{project.files.length} file{project.files.length !== 1 ? 's' : ''}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isSelected ? 'default' : 'outline'}
|
||||
onClick={(e) => { e.stopPropagation(); onSelect() }}
|
||||
className="shrink-0"
|
||||
>
|
||||
{isSelected ? (
|
||||
<><CheckCircle2 className="mr-1 h-3.5 w-3.5" /> Selected</>
|
||||
) : (
|
||||
selectLabel || 'Select'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{isExpanded && <ProjectDetails project={project} onDownload={onDownload} />}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -39,7 +39,7 @@ export const fileRouter = router({
|
||||
})
|
||||
}
|
||||
|
||||
const [juryAssignment, mentorAssignment, teamMembership] = await Promise.all([
|
||||
const [juryAssignment, mentorAssignment, teamMembership, awardJurorAccess] = await Promise.all([
|
||||
ctx.prisma.assignment.findFirst({
|
||||
where: { userId: ctx.user.id, projectId: file.projectId },
|
||||
select: { id: true, roundId: true },
|
||||
@@ -58,16 +58,29 @@ export const fileRouter = router({
|
||||
},
|
||||
select: { id: true },
|
||||
}),
|
||||
// Award jurors can access files for projects eligible in their awards
|
||||
ctx.prisma.awardJuror.findFirst({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
award: {
|
||||
status: { in: ['VOTING_OPEN', 'NOMINATIONS_OPEN'] },
|
||||
eligibilities: {
|
||||
some: { projectId: file.projectId, eligible: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
}),
|
||||
])
|
||||
|
||||
if (!juryAssignment && !mentorAssignment && !teamMembership) {
|
||||
if (!juryAssignment && !mentorAssignment && !teamMembership && !awardJurorAccess) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'You do not have access to this file',
|
||||
})
|
||||
}
|
||||
|
||||
if (juryAssignment && !mentorAssignment && !teamMembership) {
|
||||
if (juryAssignment && !mentorAssignment && !teamMembership && !awardJurorAccess) {
|
||||
const assignedRound = await ctx.prisma.round.findUnique({
|
||||
where: { id: juryAssignment.roundId },
|
||||
select: { competitionId: true, sortOrder: true },
|
||||
|
||||
@@ -740,6 +740,25 @@ export const specialAwardRouter = router({
|
||||
competitionCategory: true,
|
||||
country: true,
|
||||
tags: true,
|
||||
files: {
|
||||
where: { replacedById: null },
|
||||
select: {
|
||||
id: true,
|
||||
fileName: true,
|
||||
fileType: true,
|
||||
bucket: true,
|
||||
objectKey: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
},
|
||||
teamMembers: {
|
||||
select: {
|
||||
id: true,
|
||||
role: true,
|
||||
user: { select: { name: true, email: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user