fix: submission round completion %, document details, project teams UX
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m32s

- Fix 0% completion on SUBMISSION pipeline cards — now based on teams
  with uploads / total projects instead of evaluation completions
- Add page count, detected language, and non-English warning indicator
  to Recent Documents list items
- Add project avatar (logo) to document and project list rows
- Make document rows clickable into project detail page
- Remove team name from project list (none have names), show country only
- Rename "Teams Submitted" to "Teams with Uploads" for clarity
- Add "See all" link to Projects section → /observer/projects
- Rename section from "Project Teams" to "Projects"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 15:50:51 +01:00
parent ec30dc83d6
commit 0390d05727
3 changed files with 99 additions and 48 deletions

View File

@@ -8,7 +8,8 @@ import { Skeleton } from '@/components/ui/skeleton'
import { Badge } from '@/components/ui/badge'
import { AnimatedCard } from '@/components/shared/animated-container'
import { CountryDisplay } from '@/components/shared/country-display'
import { FileText, Upload, Users } from 'lucide-react'
import { ProjectLogoWithUrl } from '@/components/shared/project-logo-with-url'
import { AlertTriangle, FileText, Upload, Users } from 'lucide-react'
function relativeTime(date: Date | string): string {
const now = Date.now()
@@ -20,22 +21,6 @@ function relativeTime(date: Date | string): string {
return `${Math.floor(diff / 86400)}d ago`
}
const FILE_TYPE_ICONS: Record<string, string> = {
pdf: '📄',
image: '🖼️',
video: '🎥',
default: '📎',
}
function fileIcon(fileType: string | null | undefined): string {
if (!fileType) return FILE_TYPE_ICONS.default
const ft = fileType.toLowerCase()
if (ft.includes('pdf')) return FILE_TYPE_ICONS.pdf
if (ft.includes('image') || ft.includes('png') || ft.includes('jpg') || ft.includes('jpeg')) return FILE_TYPE_ICONS.image
if (ft.includes('video') || ft.includes('mp4')) return FILE_TYPE_ICONS.video
return FILE_TYPE_ICONS.default
}
export function SubmissionPanel({ roundId, programId }: { roundId: string; programId: string }) {
const { data: roundStats, isLoading: statsLoading } = trpc.analytics.getRoundTypeStats.useQuery(
{ roundId },
@@ -82,7 +67,7 @@ export function SubmissionPanel({ roundId, programId }: { roundId: string; progr
<Users className="h-4 w-4 text-blue-500" />
<p className="text-2xl font-semibold tabular-nums">{stats.teamsSubmitted}</p>
</div>
<p className="text-xs text-muted-foreground mt-0.5">Teams Submitted</p>
<p className="text-xs text-muted-foreground mt-0.5">Teams with Uploads</p>
</Card>
</div>
) : null}
@@ -100,25 +85,47 @@ export function SubmissionPanel({ roundId, programId }: { roundId: string; progr
<CardContent className="p-0">
<div className="divide-y">
{files.map((f: any) => (
<div key={f.id} className="flex items-center gap-3 px-4 py-2.5">
<span className="text-lg shrink-0">
{fileIcon(f.fileType)}
</span>
<Link
key={f.id}
href={`/observer/projects/${f.project?.id}` as Route}
className="flex items-center gap-3 px-4 py-2.5 hover:bg-muted/50 transition-colors"
>
{/* Project avatar */}
<ProjectLogoWithUrl
project={{ id: f.project?.id ?? '', title: f.project?.title ?? '', logoKey: f.project?.logoKey }}
size="sm"
fallback="initials"
/>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{f.fileName}</p>
<p className="text-xs text-muted-foreground truncate">
<Link
href={`/observer/projects/${f.project?.id}` as Route}
className="hover:underline"
>
{f.project?.title ?? 'Unknown project'}
</Link>
</p>
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<span className="truncate">{f.project?.title ?? 'Unknown project'}</span>
{/* Page count */}
{f.pageCount != null && (
<>
<span>·</span>
<span className="shrink-0">{f.pageCount} pg{f.pageCount !== 1 ? 's' : ''}</span>
</>
)}
{/* Language badge */}
{f.detectedLang && f.detectedLang !== 'und' && (
<>
<span>·</span>
<span className={`shrink-0 font-mono uppercase ${f.detectedLang !== 'eng' ? 'text-amber-600 font-semibold' : ''}`}>
{f.detectedLang}
</span>
{f.detectedLang !== 'eng' && (
<AlertTriangle className="h-3 w-3 text-amber-500 shrink-0" />
)}
</>
)}
</div>
</div>
<span className="text-[11px] tabular-nums text-muted-foreground shrink-0">
{f.createdAt ? relativeTime(f.createdAt) : ''}
</span>
</div>
</Link>
))}
</div>
</CardContent>
@@ -131,10 +138,18 @@ export function SubmissionPanel({ roundId, programId }: { roundId: string; progr
<AnimatedCard index={2}>
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-sm">
<Users className="h-4 w-4 text-emerald-500" />
Project Teams
</CardTitle>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2 text-sm">
<Users className="h-4 w-4 text-emerald-500" />
Projects
</CardTitle>
<Link
href={'/observer/projects' as Route}
className="text-xs text-primary hover:underline"
>
See all
</Link>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="divide-y">
@@ -142,17 +157,21 @@ export function SubmissionPanel({ roundId, programId }: { roundId: string; progr
<Link
key={p.id}
href={`/observer/projects/${p.id}` as Route}
className="flex items-center justify-between gap-2 px-4 py-2.5 hover:bg-muted/50 transition-colors"
className="flex items-center gap-3 px-4 py-2.5 hover:bg-muted/50 transition-colors"
>
<ProjectLogoWithUrl
project={{ id: p.id, title: p.title, logoKey: (p as any).logoKey }}
size="sm"
fallback="initials"
/>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{p.title}</p>
<p className="text-xs text-muted-foreground truncate">
{p.teamName ?? 'No team'} · {p.country ? <CountryDisplay country={p.country} /> : ''}
</p>
{p.country && (
<p className="text-xs text-muted-foreground">
<CountryDisplay country={p.country} />
</p>
)}
</div>
<Badge variant="outline" className="text-xs shrink-0">
{p.country ? <CountryDisplay country={p.country} /> : '—'}
</Badge>
</Link>
))}
</div>

View File

@@ -105,7 +105,7 @@ export function RoundTypeStatsCards({ roundId }: RoundTypeStatsCardsProps) {
case 'SUBMISSION':
return [
{ label: 'Total Files', value: (stats.totalFiles as number) ?? 0, icon: Upload, color: '#053d57' },
{ label: 'Teams Submitted', value: (stats.teamsSubmitted as number) ?? 0, icon: FileText, color: '#557f8c' },
{ label: 'Teams with Uploads', value: (stats.teamsSubmitted as number) ?? 0, icon: FileText, color: '#557f8c' },
]
case 'MENTORING':