From 3a6a9a2b45d03efa5b38ee1908b8e0eae178cf87 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 14 Apr 2026 12:49:28 -0400 Subject: [PATCH] feat: add inline file viewer and project logos to award voting - Replace custom download-only file list with full FileViewer component that supports inline preview (PDF, video, images, Office docs), open in new tab, and download - Add project logos next to project names in award voting cards - Backend now returns full file metadata (mimeType, size, pageCount, language detection) and project logo URLs - Award jurors can access files for eligible projects (access control already added in prior commit) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/(jury)/jury/awards/[id]/page.tsx | 90 ++++++------------------ src/server/routers/specialAward.ts | 16 ++++- 2 files changed, 38 insertions(+), 68 deletions(-) diff --git a/src/app/(jury)/jury/awards/[id]/page.tsx b/src/app/(jury)/jury/awards/[id]/page.tsx index c5f77a1..ffb2c22 100644 --- a/src/app/(jury)/jury/awards/[id]/page.tsx +++ b/src/app/(jury)/jury/awards/[id]/page.tsx @@ -21,10 +21,7 @@ import { Loader2, GripVertical, ChevronDown, - FileText, - Download, Users, - MapPin, Tag, } from 'lucide-react' import { cn } from '@/lib/utils' @@ -34,7 +31,8 @@ import { CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' -import { Separator } from '@/components/ui/separator' +import { FileViewer } from '@/components/shared/file-viewer' +import { ProjectLogo } from '@/components/shared/project-logo' export default function JuryAwardVotingPage({ params, @@ -68,19 +66,6 @@ export default function JuryAwardVotingPage({ }) } - 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) { @@ -212,7 +197,7 @@ export default function JuryAwardVotingPage({ isExpanded={expandedProjects.has(project.id)} onSelect={() => setSelectedProjectId(project.id)} onToggleExpand={() => toggleExpanded(project.id)} - onDownload={handleDownload} + /> ))} @@ -254,7 +239,7 @@ export default function JuryAwardVotingPage({ {index + 1} - +

{project.title}

@@ -277,7 +262,7 @@ export default function JuryAwardVotingPage({

- + ) @@ -298,7 +283,7 @@ export default function JuryAwardVotingPage({ 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} /> ))} @@ -345,12 +330,22 @@ type ProjectData = { competitionCategory: string | null country: string | null tags: string[] + logoKey?: string | null + logoUrl?: string | null files: Array<{ id: string fileName: string fileType: string + mimeType: string + size: number bucket: string objectKey: string + version: number + isLate: boolean + pageCount?: number | null + detectedLang?: string | null + langConfidence?: number | null + analyzedAt?: Date | string | null createdAt: Date }> teamMembers: Array<{ @@ -360,19 +355,11 @@ type ProjectData = { }> } -function ProjectDetails({ - project, - onDownload, -}: { - project: ProjectData - onDownload: (storageKey: string, fileName: string) => void -}) { +function ProjectDetails({ project }: { project: ProjectData }) { return ( -
+
{project.description && ( -
-

{project.description}

-
+

{project.description}

)} {project.tags.length > 0 && ( @@ -402,32 +389,8 @@ function ProjectDetails({ )} {project.files.length > 0 && ( -
-
- - - Documents ({project.files.length}) - -
-
- {project.files.map((file) => ( - - ))} -
+
e.stopPropagation()}> + [0]['files']} projectId={project.id} />
)} @@ -444,7 +407,6 @@ function ProjectCard({ isExpanded, onSelect, onToggleExpand, - onDownload, selectLabel, }: { project: ProjectData @@ -452,7 +414,6 @@ function ProjectCard({ isExpanded: boolean onSelect: () => void onToggleExpand: () => void - onDownload: (storageKey: string, fileName: string) => void selectLabel?: string }) { return ( @@ -463,6 +424,7 @@ function ProjectCard({ )} >
+
- {isExpanded && } + {isExpanded && } ) } diff --git a/src/server/routers/specialAward.ts b/src/server/routers/specialAward.ts index 35a6533..98c1b1d 100644 --- a/src/server/routers/specialAward.ts +++ b/src/server/routers/specialAward.ts @@ -8,6 +8,7 @@ import { processEligibilityJob } from '../services/award-eligibility-job' import { resolveAwardWinner } from '../services/award-winner-resolver' import { getAwardSelectionNotificationTemplate, sendJuryInvitationEmail } from '@/lib/email' import { generateInviteToken, getInviteExpiryMs } from '@/server/utils/invite' +import { attachProjectLogoUrls } from '../utils/project-logo-url' import { sendBatchNotifications } from '../services/notification-sender' import type { NotificationItem } from '../services/notification-sender' import type { PrismaClient } from '@prisma/client' @@ -740,14 +741,24 @@ export const specialAwardRouter = router({ competitionCategory: true, country: true, tags: true, + logoKey: true, + logoProvider: true, files: { where: { replacedById: null }, select: { id: true, fileName: true, fileType: true, + mimeType: true, + size: true, bucket: true, objectKey: true, + version: true, + isLate: true, + pageCount: true, + detectedLang: true, + langConfidence: true, + analyzedAt: true, createdAt: true, }, orderBy: { createdAt: 'desc' }, @@ -768,9 +779,12 @@ export const specialAwardRouter = router({ }), ]) + const projectsRaw = eligibleProjects.map((e) => e.project) + const projectsWithLogos = await attachProjectLogoUrls(projectsRaw) + return { award, - projects: eligibleProjects.map((e) => e.project), + projects: projectsWithLogos, myVotes, } }),