Admin dashboard & round management UX overhaul
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m43s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m43s
- Extract round detail monolith (2900→600 lines) into 13 standalone components - Add shared round/status config (round-config.ts) replacing 4 local copies - Delete 12 legacy competition-scoped pages, merge project pool into projects page - Add round-type-specific dashboard stat panels (submission, mentoring, live final, deliberation, summary) - Add contextual header quick actions based on active round type - Improve pipeline visualization: progress bars, checkmarks, chevron connectors, overflow fix - Add config tab completion dots (green/amber/red) and inline validation warnings - Enhance juries page with round assignments, member avatars, and cap mode badges - Add context-aware project list (recent submissions vs active evaluations) - Move competition settings into Manage Editions page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,24 +13,41 @@ import {
|
||||
import { StatusBadge } from '@/components/shared/status-badge'
|
||||
import { ProjectLogo } from '@/components/shared/project-logo'
|
||||
import { getCountryName } from '@/lib/countries'
|
||||
import { formatDateOnly, truncate } from '@/lib/utils'
|
||||
import { formatDateOnly, truncate, formatRelativeTime } from '@/lib/utils'
|
||||
|
||||
type ProjectListCompactProps = {
|
||||
projects: Array<{
|
||||
id: string
|
||||
title: string
|
||||
teamName: string | null
|
||||
country: string | null
|
||||
competitionCategory: string | null
|
||||
oceanIssue: string | null
|
||||
logoKey: string | null
|
||||
createdAt: Date
|
||||
submittedAt: Date | null
|
||||
status: string
|
||||
}>
|
||||
type BaseProject = {
|
||||
id: string
|
||||
title: string
|
||||
teamName: string | null
|
||||
country: string | null
|
||||
competitionCategory: string | null
|
||||
oceanIssue: string | null
|
||||
logoKey: string | null
|
||||
createdAt: Date
|
||||
submittedAt: Date | null
|
||||
status: string
|
||||
}
|
||||
|
||||
export function ProjectListCompact({ projects }: ProjectListCompactProps) {
|
||||
type ActiveProject = BaseProject & {
|
||||
latestEvaluator: string | null
|
||||
latestScore: number | null
|
||||
evaluatedAt: Date | null
|
||||
}
|
||||
|
||||
type ProjectListCompactProps = {
|
||||
projects: BaseProject[]
|
||||
activeProjects?: ActiveProject[]
|
||||
mode?: 'recent' | 'active'
|
||||
}
|
||||
|
||||
export function ProjectListCompact({
|
||||
projects,
|
||||
activeProjects,
|
||||
mode = 'recent',
|
||||
}: ProjectListCompactProps) {
|
||||
const isActiveMode = mode === 'active' && activeProjects && activeProjects.length > 0
|
||||
const displayProjects = isActiveMode ? activeProjects : projects
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
@@ -40,8 +57,12 @@ export function ProjectListCompact({ projects }: ProjectListCompactProps) {
|
||||
<ClipboardList className="h-4 w-4 text-brand-blue" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">Recent Projects</CardTitle>
|
||||
<CardDescription className="text-xs">Latest submissions</CardDescription>
|
||||
<CardTitle className="text-base">
|
||||
{isActiveMode ? 'Recently Active' : 'Recent Projects'}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
{isActiveMode ? 'Latest evaluation activity' : 'Latest submissions'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
@@ -53,7 +74,7 @@ export function ProjectListCompact({ projects }: ProjectListCompactProps) {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{projects.length === 0 ? (
|
||||
{displayProjects.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-10 text-center">
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-muted">
|
||||
<ClipboardList className="h-7 w-7 text-muted-foreground/40" />
|
||||
@@ -64,48 +85,69 @@ export function ProjectListCompact({ projects }: ProjectListCompactProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{projects.map((project, idx) => (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.25, delay: 0.15 + idx * 0.04 }}
|
||||
>
|
||||
<Link
|
||||
href={`/admin/projects/${project.id}`}
|
||||
className="block"
|
||||
{displayProjects.map((project, idx) => {
|
||||
const activeProject = isActiveMode ? (project as ActiveProject) : null
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.25, delay: 0.15 + idx * 0.04 }}
|
||||
>
|
||||
<div className="flex items-center gap-3 py-3 px-1 transition-colors hover:bg-muted/40 rounded-lg group">
|
||||
<ProjectLogo
|
||||
project={project}
|
||||
size="sm"
|
||||
fallback="initials"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-sm font-medium truncate group-hover:text-brand-blue transition-colors">
|
||||
{truncate(project.title, 50)}
|
||||
<Link
|
||||
href={`/admin/projects/${project.id}`}
|
||||
className="block"
|
||||
>
|
||||
<div className="flex items-center gap-3 py-3 px-1 transition-colors hover:bg-muted/40 rounded-lg group">
|
||||
<ProjectLogo
|
||||
project={project}
|
||||
size="sm"
|
||||
fallback="initials"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-sm font-medium truncate group-hover:text-brand-blue transition-colors">
|
||||
{truncate(project.title, 50)}
|
||||
</p>
|
||||
{activeProject?.latestScore != null ? (
|
||||
<span className="shrink-0 text-xs font-semibold tabular-nums text-brand-blue">
|
||||
{activeProject.latestScore.toFixed(1)}/10
|
||||
</span>
|
||||
) : (
|
||||
<StatusBadge
|
||||
status={project.status ?? 'SUBMITTED'}
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{isActiveMode && activeProject ? (
|
||||
<>
|
||||
{activeProject.latestEvaluator && (
|
||||
<span>{activeProject.latestEvaluator}</span>
|
||||
)}
|
||||
{activeProject.evaluatedAt && (
|
||||
<span> · {formatRelativeTime(activeProject.evaluatedAt)}</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
[
|
||||
project.teamName,
|
||||
project.country ? getCountryName(project.country) : null,
|
||||
formatDateOnly(project.submittedAt || project.createdAt),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' \u00b7 ')
|
||||
)}
|
||||
</p>
|
||||
<StatusBadge
|
||||
status={project.status ?? 'SUBMITTED'}
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{[
|
||||
project.teamName,
|
||||
project.country ? getCountryName(project.country) : null,
|
||||
formatDateOnly(project.submittedAt || project.createdAt),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' \u00b7 ')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</Link>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user