From 267d26581d3641f60f6cb8b3c4cf7d2dc28f0f4c Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 4 Mar 2026 13:29:54 +0100 Subject: [PATCH] feat: resolve project logo URLs server-side, show logos in admin + observer Add attachProjectLogoUrls utility mirroring avatar URL pattern. Pipe project.list and analytics.getAllProjects through logo URL resolver so ProjectLogo components receive presigned URLs. Add logos to observer projects table and mobile cards. Co-Authored-By: Claude Opus 4.6 --- src/app/(admin)/admin/projects/page.tsx | 59 +- .../observer/observer-projects-content.tsx | 61 +- src/server/routers/analytics.ts | 10 +- src/server/routers/project.ts | 795 +++++++++++++++++- src/server/utils/project-logo-url.ts | 35 + 5 files changed, 919 insertions(+), 41 deletions(-) create mode 100644 src/server/utils/project-logo-url.ts diff --git a/src/app/(admin)/admin/projects/page.tsx b/src/app/(admin)/admin/projects/page.tsx index 142ae34..f7713a6 100644 --- a/src/app/(admin)/admin/projects/page.tsx +++ b/src/app/(admin)/admin/projects/page.tsx @@ -72,6 +72,7 @@ import { ArrowRightCircle, LayoutGrid, LayoutList, + Bell, } from 'lucide-react' import { Select, @@ -90,7 +91,8 @@ import { } from '@/components/ui/tooltip' import { truncate } from '@/lib/utils' import { ProjectLogo } from '@/components/shared/project-logo' -import { StatusBadge } from '@/components/shared/status-badge' +import { BulkNotificationDialog } from '@/components/admin/projects/bulk-notification-dialog' + import { Pagination } from '@/components/shared/pagination' import { getCountryName, getCountryFlag, normalizeCountryToCode } from '@/lib/countries' import { CountryFlagImg } from '@/components/ui/country-select' @@ -113,6 +115,25 @@ const statusColors: Record< WINNER: 'success', REJECTED: 'destructive', WITHDRAWN: 'secondary', + // Round-state-based statuses + PENDING: 'secondary', + IN_PROGRESS: 'default', + COMPLETED: 'default', + PASSED: 'success', +} + +type ProjectRoundStateInfo = { + state: string + round: { name: string; sortOrder: number } +} + +function deriveProjectStatus(prs: ProjectRoundStateInfo[]): { label: string; variant: 'default' | 'success' | 'secondary' | 'destructive' | 'warning' } { + if (!prs.length) return { label: 'Submitted', variant: 'secondary' } + if (prs.some((p) => p.state === 'REJECTED')) return { label: 'Rejected', variant: 'destructive' } + // prs is already sorted by sortOrder desc — first item is the latest round + const latest = prs[0] + if (latest.state === 'PASSED') return { label: latest.round.name, variant: 'success' } + return { label: latest.round.name, variant: 'default' } } function parseFiltersFromParams( @@ -290,6 +311,7 @@ export default function ProjectsPage() { const [projectToAssign, setProjectToAssign] = useState<{ id: string; title: string } | null>(null) const [assignRoundId, setAssignRoundId] = useState('') + const [bulkNotifyOpen, setBulkNotifyOpen] = useState(false) const [aiTagDialogOpen, setAiTagDialogOpen] = useState(false) const [taggingScope, setTaggingScope] = useState<'round' | 'program'>('round') const [selectedRoundForTagging, setSelectedRoundForTagging] = useState('') @@ -619,6 +641,13 @@ export default function ProjectsPage() {

+