feat: resolve project logo URLs server-side, show logos in admin + observer
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m30s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m30s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string>('')
|
||||
@@ -619,6 +641,13 @@ export default function ProjectsPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setBulkNotifyOpen(true)}
|
||||
>
|
||||
<Bell className="mr-2 h-4 w-4" />
|
||||
Send Notifications
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setAiTagDialogOpen(true)}
|
||||
@@ -713,7 +742,7 @@ export default function ProjectsPage() {
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm">
|
||||
{Object.entries(data.statusCounts ?? {})
|
||||
.sort(([a], [b]) => {
|
||||
const order = ['SUBMITTED', 'ELIGIBLE', 'ASSIGNED', 'SEMIFINALIST', 'FINALIST', 'WINNER', 'REJECTED', 'WITHDRAWN']
|
||||
const order = ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'PASSED', 'REJECTED', 'WITHDRAWN']
|
||||
return order.indexOf(a) - order.indexOf(b)
|
||||
})
|
||||
.map(([status, count]) => (
|
||||
@@ -873,7 +902,7 @@ export default function ProjectsPage() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.projects.map((project) => {
|
||||
const isEliminated = project.status === 'REJECTED'
|
||||
const isEliminated = (project.projectRoundStates ?? []).some((p: ProjectRoundStateInfo) => p.state === 'REJECTED')
|
||||
return (
|
||||
<TableRow
|
||||
key={project.id}
|
||||
@@ -894,6 +923,7 @@ export default function ProjectsPage() {
|
||||
>
|
||||
<ProjectLogo
|
||||
project={project}
|
||||
logoUrl={project.logoUrl}
|
||||
size="sm"
|
||||
fallback="initials"
|
||||
/>
|
||||
@@ -972,7 +1002,10 @@ export default function ProjectsPage() {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={project.status ?? 'SUBMITTED'} />
|
||||
{(() => {
|
||||
const derived = deriveProjectStatus(project.projectRoundStates ?? [])
|
||||
return <Badge variant={derived.variant}>{derived.label}</Badge>
|
||||
})()}
|
||||
</TableCell>
|
||||
<TableCell className="relative z-10 text-right">
|
||||
<DropdownMenu>
|
||||
@@ -1042,13 +1075,16 @@ export default function ProjectsPage() {
|
||||
<Card className="transition-all duration-200 hover:bg-muted/50 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start gap-3 pl-8">
|
||||
<ProjectLogo project={project} size="md" fallback="initials" />
|
||||
<ProjectLogo project={project} logoUrl={project.logoUrl} size="md" fallback="initials" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<CardTitle className={`text-base line-clamp-2 ${uppercaseNames ? 'uppercase' : ''}`}>
|
||||
{project.title}
|
||||
</CardTitle>
|
||||
<StatusBadge status={project.status ?? 'SUBMITTED'} className="shrink-0" />
|
||||
{(() => {
|
||||
const derived = deriveProjectStatus(project.projectRoundStates ?? [])
|
||||
return <Badge variant={derived.variant} className="shrink-0">{derived.label}</Badge>
|
||||
})()}
|
||||
</div>
|
||||
<CardDescription>{project.teamName}</CardDescription>
|
||||
</div>
|
||||
@@ -1096,7 +1132,7 @@ export default function ProjectsPage() {
|
||||
/* Card View */
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{data.projects.map((project) => {
|
||||
const isEliminated = project.status === 'REJECTED'
|
||||
const isEliminated = (project.projectRoundStates ?? []).some((p: ProjectRoundStateInfo) => p.state === 'REJECTED')
|
||||
return (
|
||||
<div key={project.id} className="relative">
|
||||
<div className="absolute left-3 top-3 z-10">
|
||||
@@ -1110,7 +1146,7 @@ export default function ProjectsPage() {
|
||||
<Card className={`transition-all duration-200 hover:bg-muted/50 hover:-translate-y-0.5 hover:shadow-md h-full ${isEliminated ? 'opacity-60 bg-destructive/5' : ''}`}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start gap-3 pl-7">
|
||||
<ProjectLogo project={project} size="lg" fallback="initials" />
|
||||
<ProjectLogo project={project} logoUrl={project.logoUrl} size="lg" fallback="initials" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<CardTitle className={`text-base line-clamp-2 ${uppercaseNames ? 'uppercase' : ''}`}>
|
||||
@@ -1177,7 +1213,10 @@ export default function ProjectsPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 pt-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<StatusBadge status={project.status ?? 'SUBMITTED'} />
|
||||
{(() => {
|
||||
const derived = deriveProjectStatus(project.projectRoundStates ?? [])
|
||||
return <Badge variant={derived.variant}>{derived.label}</Badge>
|
||||
})()}
|
||||
{project.competitionCategory && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{project.competitionCategory === 'STARTUP' ? 'Startup' : 'Business Concept'}
|
||||
@@ -1846,6 +1885,8 @@ export default function ProjectsPage() {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<BulkNotificationDialog open={bulkNotifyOpen} onOpenChange={setBulkNotifyOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user