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:
264
src/lib/round-config.ts
Normal file
264
src/lib/round-config.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Shared round type, status, and project state configuration.
|
||||
*
|
||||
* Single source of truth for colors, labels, icons, and descriptions
|
||||
* used across the admin dashboard, round detail, pipeline, and timeline.
|
||||
*/
|
||||
|
||||
import type { RoundType, RoundStatus, ProjectRoundStateValue } from '@prisma/client'
|
||||
import {
|
||||
Upload,
|
||||
Filter,
|
||||
ClipboardCheck,
|
||||
FileUp,
|
||||
GraduationCap,
|
||||
Radio,
|
||||
Scale,
|
||||
Inbox,
|
||||
Users,
|
||||
Circle,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
|
||||
// ── Round Type Configuration ─────────────────────────────────────────────────
|
||||
|
||||
export type RoundTypeConfig = {
|
||||
label: string
|
||||
description: string
|
||||
/** Badge classes: bg + text color */
|
||||
badgeClass: string
|
||||
/** Hex dot color for pipeline nodes */
|
||||
dotColor: string
|
||||
/** Lighter variant: bg, text, border classes for cards/filters */
|
||||
cardBg: string
|
||||
cardText: string
|
||||
cardBorder: string
|
||||
/** Icon for pipeline nodes and round detail */
|
||||
icon: LucideIcon
|
||||
iconColor: string
|
||||
iconBg: string
|
||||
}
|
||||
|
||||
export const roundTypeConfig: Record<RoundType, RoundTypeConfig> = {
|
||||
INTAKE: {
|
||||
label: 'Intake',
|
||||
description: 'Collecting applications',
|
||||
badgeClass: 'bg-gray-100 text-gray-700',
|
||||
dotColor: '#9ca3af',
|
||||
cardBg: 'bg-gray-50',
|
||||
cardText: 'text-gray-600',
|
||||
cardBorder: 'border-gray-300',
|
||||
icon: Inbox,
|
||||
iconColor: 'text-sky-600',
|
||||
iconBg: 'bg-sky-100',
|
||||
},
|
||||
FILTERING: {
|
||||
label: 'Filtering',
|
||||
description: 'AI + manual screening',
|
||||
badgeClass: 'bg-amber-100 text-amber-700',
|
||||
dotColor: '#f59e0b',
|
||||
cardBg: 'bg-amber-50',
|
||||
cardText: 'text-amber-700',
|
||||
cardBorder: 'border-amber-300',
|
||||
icon: Filter,
|
||||
iconColor: 'text-amber-600',
|
||||
iconBg: 'bg-amber-100',
|
||||
},
|
||||
EVALUATION: {
|
||||
label: 'Evaluation',
|
||||
description: 'Jury evaluation & scoring',
|
||||
badgeClass: 'bg-blue-100 text-blue-700',
|
||||
dotColor: '#3b82f6',
|
||||
cardBg: 'bg-blue-50',
|
||||
cardText: 'text-blue-700',
|
||||
cardBorder: 'border-blue-300',
|
||||
icon: ClipboardCheck,
|
||||
iconColor: 'text-violet-600',
|
||||
iconBg: 'bg-violet-100',
|
||||
},
|
||||
SUBMISSION: {
|
||||
label: 'Submission',
|
||||
description: 'Document submission',
|
||||
badgeClass: 'bg-purple-100 text-purple-700',
|
||||
dotColor: '#8b5cf6',
|
||||
cardBg: 'bg-purple-50',
|
||||
cardText: 'text-purple-700',
|
||||
cardBorder: 'border-purple-300',
|
||||
icon: FileUp,
|
||||
iconColor: 'text-blue-600',
|
||||
iconBg: 'bg-blue-100',
|
||||
},
|
||||
MENTORING: {
|
||||
label: 'Mentoring',
|
||||
description: 'Mentor-guided development',
|
||||
badgeClass: 'bg-teal-100 text-teal-700',
|
||||
dotColor: '#557f8c',
|
||||
cardBg: 'bg-teal-50',
|
||||
cardText: 'text-teal-700',
|
||||
cardBorder: 'border-teal-300',
|
||||
icon: GraduationCap,
|
||||
iconColor: 'text-teal-600',
|
||||
iconBg: 'bg-teal-100',
|
||||
},
|
||||
LIVE_FINAL: {
|
||||
label: 'Live Final',
|
||||
description: 'Live presentations & voting',
|
||||
badgeClass: 'bg-red-100 text-red-700',
|
||||
dotColor: '#de0f1e',
|
||||
cardBg: 'bg-red-50',
|
||||
cardText: 'text-red-700',
|
||||
cardBorder: 'border-red-300',
|
||||
icon: Radio,
|
||||
iconColor: 'text-red-600',
|
||||
iconBg: 'bg-red-100',
|
||||
},
|
||||
DELIBERATION: {
|
||||
label: 'Deliberation',
|
||||
description: 'Final jury deliberation',
|
||||
badgeClass: 'bg-indigo-100 text-indigo-700',
|
||||
dotColor: '#6366f1',
|
||||
cardBg: 'bg-indigo-50',
|
||||
cardText: 'text-indigo-700',
|
||||
cardBorder: 'border-indigo-300',
|
||||
icon: Scale,
|
||||
iconColor: 'text-indigo-600',
|
||||
iconBg: 'bg-indigo-100',
|
||||
},
|
||||
}
|
||||
|
||||
// ── Round Status Configuration ───────────────────────────────────────────────
|
||||
|
||||
export type RoundStatusConfig = {
|
||||
label: string
|
||||
description: string
|
||||
/** Badge classes for status badges */
|
||||
bgClass: string
|
||||
/** Dot color class (with optional animation) */
|
||||
dotClass: string
|
||||
/** Hex color for pipeline dot */
|
||||
dotColor: string
|
||||
/** Whether the dot should pulse (active round) */
|
||||
pulse: boolean
|
||||
/** Icon for timeline displays */
|
||||
timelineIcon: LucideIcon
|
||||
timelineIconColor: string
|
||||
/** Container classes for pipeline nodes */
|
||||
pipelineContainer: string
|
||||
}
|
||||
|
||||
export const roundStatusConfig: Record<RoundStatus, RoundStatusConfig> = {
|
||||
ROUND_DRAFT: {
|
||||
label: 'Draft',
|
||||
description: 'Not yet active. Configure before launching.',
|
||||
bgClass: 'bg-gray-100 text-gray-700',
|
||||
dotClass: 'bg-gray-500',
|
||||
dotColor: '#9ca3af',
|
||||
pulse: false,
|
||||
timelineIcon: Circle,
|
||||
timelineIconColor: 'text-gray-400',
|
||||
pipelineContainer: 'bg-slate-50 border-slate-200 text-slate-400 border-dashed',
|
||||
},
|
||||
ROUND_ACTIVE: {
|
||||
label: 'Active',
|
||||
description: 'Round is live. Projects can be processed.',
|
||||
bgClass: 'bg-emerald-100 text-emerald-700',
|
||||
dotClass: 'bg-emerald-500 animate-pulse',
|
||||
dotColor: '#10b981',
|
||||
pulse: true,
|
||||
timelineIcon: Clock,
|
||||
timelineIconColor: 'text-emerald-500',
|
||||
pipelineContainer: 'bg-blue-50 border-blue-300 text-blue-700 ring-2 ring-blue-400/30 shadow-lg shadow-blue-500/10',
|
||||
},
|
||||
ROUND_CLOSED: {
|
||||
label: 'Closed',
|
||||
description: 'No longer accepting changes. Results are final.',
|
||||
bgClass: 'bg-blue-100 text-blue-700',
|
||||
dotClass: 'bg-blue-500',
|
||||
dotColor: '#3b82f6',
|
||||
pulse: false,
|
||||
timelineIcon: CheckCircle2,
|
||||
timelineIconColor: 'text-blue-500',
|
||||
pipelineContainer: 'bg-emerald-50 border-emerald-200 text-emerald-600',
|
||||
},
|
||||
ROUND_ARCHIVED: {
|
||||
label: 'Archived',
|
||||
description: 'Historical record only.',
|
||||
bgClass: 'bg-muted text-muted-foreground',
|
||||
dotClass: 'bg-muted-foreground',
|
||||
dotColor: '#6b7280',
|
||||
pulse: false,
|
||||
timelineIcon: CheckCircle2,
|
||||
timelineIconColor: 'text-gray-400',
|
||||
pipelineContainer: 'bg-gray-50 border-gray-200 text-gray-400 opacity-60',
|
||||
},
|
||||
}
|
||||
|
||||
// ── Project Round State Colors ───────────────────────────────────────────────
|
||||
|
||||
export type ProjectStateConfig = {
|
||||
label: string
|
||||
/** Background color class for bars and dots */
|
||||
bg: string
|
||||
/** Text color class */
|
||||
text: string
|
||||
/** Badge variant classes */
|
||||
badgeClass: string
|
||||
}
|
||||
|
||||
export const projectStateConfig: Record<ProjectRoundStateValue, ProjectStateConfig> = {
|
||||
PENDING: {
|
||||
label: 'Pending',
|
||||
bg: 'bg-slate-300',
|
||||
text: 'text-slate-700',
|
||||
badgeClass: 'bg-gray-100 text-gray-700',
|
||||
},
|
||||
IN_PROGRESS: {
|
||||
label: 'In Progress',
|
||||
bg: 'bg-blue-400',
|
||||
text: 'text-blue-700',
|
||||
badgeClass: 'bg-blue-100 text-blue-700',
|
||||
},
|
||||
PASSED: {
|
||||
label: 'Passed',
|
||||
bg: 'bg-emerald-500',
|
||||
text: 'text-emerald-700',
|
||||
badgeClass: 'bg-emerald-100 text-emerald-700',
|
||||
},
|
||||
REJECTED: {
|
||||
label: 'Rejected',
|
||||
bg: 'bg-red-400',
|
||||
text: 'text-red-700',
|
||||
badgeClass: 'bg-red-100 text-red-700',
|
||||
},
|
||||
COMPLETED: {
|
||||
label: 'Completed',
|
||||
bg: 'bg-[#557f8c]',
|
||||
text: 'text-teal-700',
|
||||
badgeClass: 'bg-teal-100 text-teal-700',
|
||||
},
|
||||
WITHDRAWN: {
|
||||
label: 'Withdrawn',
|
||||
bg: 'bg-slate-400',
|
||||
text: 'text-slate-600',
|
||||
badgeClass: 'bg-orange-100 text-orange-700',
|
||||
},
|
||||
}
|
||||
|
||||
// ── Award Status Configuration ───────────────────────────────────────────────
|
||||
|
||||
export const awardStatusConfig = {
|
||||
DRAFT: { label: 'Draft', color: 'text-gray-500', bgClass: 'bg-gray-100 text-gray-700' },
|
||||
NOMINATIONS_OPEN: { label: 'Nominations Open', color: 'text-amber-600', bgClass: 'bg-amber-100 text-amber-700' },
|
||||
VOTING_OPEN: { label: 'Voting Open', color: 'text-emerald-600', bgClass: 'bg-emerald-100 text-emerald-700' },
|
||||
CLOSED: { label: 'Closed', color: 'text-blue-500', bgClass: 'bg-blue-100 text-blue-700' },
|
||||
ARCHIVED: { label: 'Archived', color: 'text-gray-400', bgClass: 'bg-gray-100 text-gray-600' },
|
||||
} as const
|
||||
|
||||
// ── Round type option list (for selects/filters) ────────────────────────────
|
||||
|
||||
export const ROUND_TYPE_OPTIONS = Object.entries(roundTypeConfig).map(([value, cfg]) => ({
|
||||
value: value as RoundType,
|
||||
label: cfg.label,
|
||||
}))
|
||||
Reference in New Issue
Block a user