Platform-wide visual overhaul, team invites, analytics improvements, and deployment hardening
UI overhaul applying jury dashboard design patterns across all pages: - Stat cards with border-l-4 accent + icon pills on admin, observer, mentor, applicant dashboards and reports - Card section headers with color-coded icon pills throughout - Hover lift effects (translate-y + shadow) on cards and list items - Gradient progress bars (brand-teal to brand-blue) platform-wide - AnimatedCard stagger animations on all dashboard sections - Auth pages with gradient accent strip and polished icon containers - EmptyState component upgraded with rounded icon pill containers - Replaced AI-looking icons (Brain/Sparkles/Bot/Wand2/Cpu) with descriptive alternatives across 12 files - Removed gradient overlay from jury dashboard header - Quick actions restyled as card links with group hover effects Backend improvements: - Team member invite emails with account setup flow and notification logging - Analytics routers accept edition-wide queries (programId) in addition to roundId - Round detail endpoint returns inline progress data (eliminates extra getProgress call) - Award voting endpoints parallelized with Promise.all - Bulk invite supports optional sendInvitation flag - AwardVote composite index migration for query performance Infrastructure: - Docker entrypoint with migration retry loop (configurable retries/delay) - docker-compose pull_policy: always for automatic image refresh - Simplified deploy/update scripts using docker compose up -d --pull always - Updated deployment documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,48 +52,228 @@ type DashboardContentProps = {
|
||||
sessionName: string
|
||||
}
|
||||
|
||||
function formatEntity(entityType: string | null): string {
|
||||
if (!entityType) return 'record'
|
||||
// Insert space before uppercase letters (PascalCase → words), then lowercase
|
||||
return entityType
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.replace(/_/g, ' ')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function formatAction(action: string, entityType: string | null): string {
|
||||
const entity = entityType?.toLowerCase() || 'record'
|
||||
const entity = formatEntity(entityType)
|
||||
const actionMap: Record<string, string> = {
|
||||
// Generic CRUD
|
||||
CREATE: `created a ${entity}`,
|
||||
UPDATE: `updated a ${entity}`,
|
||||
DELETE: `deleted a ${entity}`,
|
||||
LOGIN: 'logged in',
|
||||
EXPORT: `exported ${entity} data`,
|
||||
SUBMIT: `submitted an ${entity}`,
|
||||
ASSIGN: `assigned a ${entity}`,
|
||||
INVITE: `invited a user`,
|
||||
STATUS_CHANGE: `changed ${entity} status`,
|
||||
BULK_UPDATE: `bulk updated ${entity}s`,
|
||||
IMPORT: `imported ${entity}s`,
|
||||
EXPORT: `exported ${entity} data`,
|
||||
REORDER: `reordered ${entity}s`,
|
||||
|
||||
// Auth
|
||||
LOGIN: 'logged in',
|
||||
LOGIN_SUCCESS: 'logged in',
|
||||
LOGIN_FAILED: 'failed to log in',
|
||||
PASSWORD_SET: 'set their password',
|
||||
PASSWORD_CHANGED: 'changed their password',
|
||||
REQUEST_PASSWORD_RESET: 'requested a password reset',
|
||||
COMPLETE_ONBOARDING: 'completed onboarding',
|
||||
DELETE_OWN_ACCOUNT: 'deleted their account',
|
||||
|
||||
// Evaluations
|
||||
EVALUATION_SUBMITTED: 'submitted an evaluation',
|
||||
COI_DECLARED: 'declared a conflict of interest',
|
||||
COI_REVIEWED: 'reviewed a COI declaration',
|
||||
REMINDERS_TRIGGERED: 'triggered evaluation reminders',
|
||||
DISCUSSION_COMMENT_ADDED: 'added a discussion comment',
|
||||
DISCUSSION_CLOSED: 'closed a discussion',
|
||||
|
||||
// Assignments
|
||||
ASSIGN: `assigned a ${entity}`,
|
||||
BULK_CREATE: `bulk created ${entity}s`,
|
||||
BULK_ASSIGN: 'bulk assigned users',
|
||||
BULK_DELETE: `bulk deleted ${entity}s`,
|
||||
BULK_UPDATE: `bulk updated ${entity}s`,
|
||||
BULK_UPDATE_STATUS: 'bulk updated statuses',
|
||||
APPLY_SUGGESTIONS: 'applied assignment suggestions',
|
||||
ASSIGN_PROJECTS_TO_ROUND: 'assigned projects to round',
|
||||
REMOVE_PROJECTS_FROM_ROUND: 'removed projects from round',
|
||||
ADVANCE_PROJECTS: 'advanced projects to next round',
|
||||
BULK_ASSIGN_TO_ROUND: 'bulk assigned to round',
|
||||
REORDER_ROUNDS: 'reordered rounds',
|
||||
|
||||
// Status
|
||||
STATUS_CHANGE: `changed ${entity} status`,
|
||||
UPDATE_STATUS: `updated ${entity} status`,
|
||||
ROLE_CHANGED: 'changed a user role',
|
||||
|
||||
// Invitations
|
||||
INVITE: 'invited a user',
|
||||
SEND_INVITATION: 'sent an invitation',
|
||||
BULK_SEND_INVITATIONS: 'sent bulk invitations',
|
||||
|
||||
// Files
|
||||
UPLOAD_FILE: 'uploaded a file',
|
||||
DELETE_FILE: 'deleted a file',
|
||||
REPLACE_FILE: 'replaced a file',
|
||||
FILE_DOWNLOADED: 'downloaded a file',
|
||||
|
||||
// Filtering
|
||||
EXECUTE_FILTERING: 'ran project filtering',
|
||||
FINALIZE_FILTERING: 'finalized filtering results',
|
||||
OVERRIDE: `overrode a ${entity} result`,
|
||||
BULK_OVERRIDE: 'bulk overrode filtering results',
|
||||
REINSTATE: 'reinstated a project',
|
||||
BULK_REINSTATE: 'bulk reinstated projects',
|
||||
|
||||
// AI
|
||||
AI_TAG: 'ran AI tagging',
|
||||
START_AI_TAG_JOB: 'started AI tagging job',
|
||||
EVALUATION_SUMMARY: 'generated an AI summary',
|
||||
AWARD_ELIGIBILITY: 'ran award eligibility check',
|
||||
PROJECT_TAGGING: 'ran project tagging',
|
||||
FILTERING: 'ran AI filtering',
|
||||
MENTOR_MATCHING: 'ran mentor matching',
|
||||
|
||||
// Tags
|
||||
ADD_TAG: 'added a tag',
|
||||
REMOVE_TAG: 'removed a tag',
|
||||
BULK_CREATE_TAGS: 'bulk created tags',
|
||||
|
||||
// Mentor
|
||||
MENTOR_ASSIGN: 'assigned a mentor',
|
||||
MENTOR_UNASSIGN: 'unassigned a mentor',
|
||||
MENTOR_AUTO_ASSIGN: 'auto-assigned mentors',
|
||||
MENTOR_BULK_ASSIGN: 'bulk assigned mentors',
|
||||
CREATE_MENTOR_NOTE: 'created a mentor note',
|
||||
COMPLETE_MILESTONE: 'completed a milestone',
|
||||
|
||||
// Messages & Webhooks
|
||||
SEND_MESSAGE: 'sent a message',
|
||||
CREATE_MESSAGE_TEMPLATE: 'created a message template',
|
||||
UPDATE_MESSAGE_TEMPLATE: 'updated a message template',
|
||||
DELETE_MESSAGE_TEMPLATE: 'deleted a message template',
|
||||
CREATE_WEBHOOK: 'created a webhook',
|
||||
UPDATE_WEBHOOK: 'updated a webhook',
|
||||
DELETE_WEBHOOK: 'deleted a webhook',
|
||||
TEST_WEBHOOK: 'tested a webhook',
|
||||
REGENERATE_WEBHOOK_SECRET: 'regenerated a webhook secret',
|
||||
|
||||
// Settings
|
||||
UPDATE_SETTING: 'updated a setting',
|
||||
UPDATE_SETTINGS_BATCH: 'updated settings',
|
||||
UPDATE_NOTIFICATION_PREFERENCES: 'updated notification preferences',
|
||||
UPDATE_DIGEST_SETTINGS: 'updated digest settings',
|
||||
UPDATE_ANALYTICS_SETTINGS: 'updated analytics settings',
|
||||
UPDATE_AUDIT_SETTINGS: 'updated audit settings',
|
||||
UPDATE_LOCALIZATION_SETTINGS: 'updated localization settings',
|
||||
UPDATE_RETENTION_CONFIG: 'updated retention config',
|
||||
|
||||
// Live Voting
|
||||
START_VOTING: 'started live voting',
|
||||
END_SESSION: 'ended a live voting session',
|
||||
UPDATE_SESSION_CONFIG: 'updated session config',
|
||||
|
||||
// Round Templates
|
||||
CREATE_ROUND_TEMPLATE: 'created a round template',
|
||||
CREATE_ROUND_TEMPLATE_FROM_ROUND: 'saved round as template',
|
||||
UPDATE_ROUND_TEMPLATE: 'updated a round template',
|
||||
DELETE_ROUND_TEMPLATE: 'deleted a round template',
|
||||
UPDATE_EVALUATION_FORM: 'updated the evaluation form',
|
||||
|
||||
// Grace Period
|
||||
GRANT_GRACE_PERIOD: 'granted a grace period',
|
||||
UPDATE_GRACE_PERIOD: 'updated a grace period',
|
||||
REVOKE_GRACE_PERIOD: 'revoked a grace period',
|
||||
BULK_GRANT_GRACE_PERIOD: 'bulk granted grace periods',
|
||||
|
||||
// Awards
|
||||
SET_AWARD_WINNER: 'set an award winner',
|
||||
|
||||
// Reports & Applications
|
||||
REPORT_GENERATED: 'generated a report',
|
||||
DRAFT_SUBMITTED: 'submitted a draft application',
|
||||
SUBMIT: `submitted a ${entity}`,
|
||||
}
|
||||
return actionMap[action] || `${action.toLowerCase()} ${entity}`
|
||||
if (actionMap[action]) return actionMap[action]
|
||||
|
||||
// Fallback: convert ACTION_NAME to readable text
|
||||
return action.toLowerCase().replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
function getActionIcon(action: string) {
|
||||
switch (action) {
|
||||
case 'CREATE': return <Plus className="h-3.5 w-3.5" />
|
||||
case 'UPDATE': return <FileEdit className="h-3.5 w-3.5" />
|
||||
case 'DELETE': return <Trash2 className="h-3.5 w-3.5" />
|
||||
case 'LOGIN': return <LogIn className="h-3.5 w-3.5" />
|
||||
case 'EXPORT': return <ArrowRight className="h-3.5 w-3.5" />
|
||||
case 'SUBMIT': return <Send className="h-3.5 w-3.5" />
|
||||
case 'ASSIGN': return <Users className="h-3.5 w-3.5" />
|
||||
case 'INVITE': return <UserPlus className="h-3.5 w-3.5" />
|
||||
default: return <Eye className="h-3.5 w-3.5" />
|
||||
case 'CREATE':
|
||||
case 'BULK_CREATE':
|
||||
return <Plus className="h-3.5 w-3.5" />
|
||||
case 'UPDATE':
|
||||
case 'UPDATE_STATUS':
|
||||
case 'BULK_UPDATE':
|
||||
case 'BULK_UPDATE_STATUS':
|
||||
case 'STATUS_CHANGE':
|
||||
case 'ROLE_CHANGED':
|
||||
return <FileEdit className="h-3.5 w-3.5" />
|
||||
case 'DELETE':
|
||||
case 'BULK_DELETE':
|
||||
return <Trash2 className="h-3.5 w-3.5" />
|
||||
case 'LOGIN':
|
||||
case 'LOGIN_SUCCESS':
|
||||
case 'LOGIN_FAILED':
|
||||
case 'PASSWORD_SET':
|
||||
case 'PASSWORD_CHANGED':
|
||||
case 'COMPLETE_ONBOARDING':
|
||||
return <LogIn className="h-3.5 w-3.5" />
|
||||
case 'EXPORT':
|
||||
case 'REPORT_GENERATED':
|
||||
return <ArrowRight className="h-3.5 w-3.5" />
|
||||
case 'SUBMIT':
|
||||
case 'EVALUATION_SUBMITTED':
|
||||
case 'DRAFT_SUBMITTED':
|
||||
return <Send className="h-3.5 w-3.5" />
|
||||
case 'ASSIGN':
|
||||
case 'BULK_ASSIGN':
|
||||
case 'APPLY_SUGGESTIONS':
|
||||
case 'ASSIGN_PROJECTS_TO_ROUND':
|
||||
case 'MENTOR_ASSIGN':
|
||||
case 'MENTOR_BULK_ASSIGN':
|
||||
return <Users className="h-3.5 w-3.5" />
|
||||
case 'INVITE':
|
||||
case 'SEND_INVITATION':
|
||||
case 'BULK_SEND_INVITATIONS':
|
||||
return <UserPlus className="h-3.5 w-3.5" />
|
||||
case 'IMPORT':
|
||||
return <Upload className="h-3.5 w-3.5" />
|
||||
default:
|
||||
return <Eye className="h-3.5 w-3.5" />
|
||||
}
|
||||
}
|
||||
|
||||
export function DashboardContent({ editionId, sessionName }: DashboardContentProps) {
|
||||
const { data, isLoading } = trpc.dashboard.getStats.useQuery(
|
||||
const { data, isLoading, error } = trpc.dashboard.getStats.useQuery(
|
||||
{ editionId },
|
||||
{ enabled: !!editionId }
|
||||
{ enabled: !!editionId, retry: 1 }
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return <DashboardSkeleton />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<AlertTriangle className="h-12 w-12 text-destructive/50" />
|
||||
<p className="mt-2 font-medium">Failed to load dashboard</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{error.message || 'An unexpected error occurred. Please try refreshing the page.'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Card>
|
||||
@@ -204,69 +384,85 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
|
||||
{/* Stats Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<AnimatedCard index={0}>
|
||||
<Card className="transition-all hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Rounds</CardTitle>
|
||||
<CircleDot className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalRoundCount}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{activeRoundCount} active round{activeRoundCount !== 1 ? 's' : ''}
|
||||
</p>
|
||||
<Card className="border-l-4 border-l-blue-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Rounds</p>
|
||||
<p className="text-2xl font-bold mt-1">{totalRoundCount}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{activeRoundCount} active round{activeRoundCount !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-blue-50 p-3">
|
||||
<CircleDot className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<AnimatedCard index={1}>
|
||||
<Card className="transition-all hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Projects</CardTitle>
|
||||
<ClipboardList className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{projectCount}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{newProjectsThisWeek > 0
|
||||
? `${newProjectsThisWeek} new this week`
|
||||
: 'In this edition'}
|
||||
</p>
|
||||
<Card className="border-l-4 border-l-emerald-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Projects</p>
|
||||
<p className="text-2xl font-bold mt-1">{projectCount}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{newProjectsThisWeek > 0
|
||||
? `${newProjectsThisWeek} new this week`
|
||||
: 'In this edition'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-emerald-50 p-3">
|
||||
<ClipboardList className="h-5 w-5 text-emerald-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<AnimatedCard index={2}>
|
||||
<Card className="transition-all hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Jury Members</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalJurors}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{activeJurors} active{invitedJurors > 0 && `, ${invitedJurors} invited`}
|
||||
</p>
|
||||
<Card className="border-l-4 border-l-violet-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Jury Members</p>
|
||||
<p className="text-2xl font-bold mt-1">{totalJurors}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{activeJurors} active{invitedJurors > 0 && `, ${invitedJurors} invited`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-violet-50 p-3">
|
||||
<Users className="h-5 w-5 text-violet-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<AnimatedCard index={3}>
|
||||
<Card className="transition-all hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Evaluations</CardTitle>
|
||||
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{submittedCount}
|
||||
{totalAssignments > 0 && (
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
{' '}/ {totalAssignments}
|
||||
</span>
|
||||
)}
|
||||
<Card className="border-l-4 border-l-brand-teal transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Evaluations</p>
|
||||
<p className="text-2xl font-bold mt-1">
|
||||
{submittedCount}
|
||||
{totalAssignments > 0 && (
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
{' '}/ {totalAssignments}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-brand-teal/10 p-3">
|
||||
<CheckCircle2 className="h-5 w-5 text-brand-teal" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<Progress value={completionRate} className="h-2" />
|
||||
<Progress value={completionRate} className="h-2" gradient />
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{completionRate.toFixed(0)}% completion rate
|
||||
</p>
|
||||
@@ -277,25 +473,34 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/admin/rounds/new">
|
||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||
New Round
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/admin/projects/new">
|
||||
<Upload className="mr-1.5 h-3.5 w-3.5" />
|
||||
Import Projects
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/admin/members">
|
||||
<UserPlus className="mr-1.5 h-3.5 w-3.5" />
|
||||
Invite Jury
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<Link href="/admin/rounds/new" className="group flex items-center gap-3 rounded-xl border border-border/60 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-blue-500/30 hover:bg-blue-500/5">
|
||||
<div className="rounded-xl bg-blue-50 p-2.5 transition-colors group-hover:bg-blue-100">
|
||||
<Plus className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">New Round</p>
|
||||
<p className="text-xs text-muted-foreground">Create a voting round</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href="/admin/projects/new" className="group flex items-center gap-3 rounded-xl border border-border/60 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-emerald-500/30 hover:bg-emerald-500/5">
|
||||
<div className="rounded-xl bg-emerald-50 p-2.5 transition-colors group-hover:bg-emerald-100">
|
||||
<Upload className="h-4 w-4 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Import Projects</p>
|
||||
<p className="text-xs text-muted-foreground">Upload a CSV file</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href="/admin/members" className="group flex items-center gap-3 rounded-xl border border-border/60 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-violet-500/30 hover:bg-violet-500/5">
|
||||
<div className="rounded-xl bg-violet-50 p-2.5 transition-colors group-hover:bg-violet-100">
|
||||
<UserPlus className="h-4 w-4 text-violet-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Invite Jury</p>
|
||||
<p className="text-xs text-muted-foreground">Add jury members</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Two-Column Content */}
|
||||
@@ -303,11 +508,17 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
|
||||
{/* Left Column */}
|
||||
<div className="space-y-6 lg:col-span-7">
|
||||
{/* Rounds Card (enhanced) */}
|
||||
<AnimatedCard index={4}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Rounds</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2.5">
|
||||
<div className="rounded-lg bg-blue-500/10 p-1.5">
|
||||
<CircleDot className="h-4 w-4 text-blue-500" />
|
||||
</div>
|
||||
Rounds
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Voting rounds in {edition.name}
|
||||
</CardDescription>
|
||||
@@ -363,7 +574,7 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
|
||||
</div>
|
||||
</div>
|
||||
{round.totalEvals > 0 && (
|
||||
<Progress value={round.evalPercent} className="mt-3 h-1.5" />
|
||||
<Progress value={round.evalPercent} className="mt-3 h-1.5" gradient />
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
@@ -372,13 +583,20 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
{/* Latest Projects Card */}
|
||||
<AnimatedCard index={5}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Latest Projects</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2.5">
|
||||
<div className="rounded-lg bg-emerald-500/10 p-1.5">
|
||||
<ClipboardList className="h-4 w-4 text-emerald-500" />
|
||||
</div>
|
||||
Latest Projects
|
||||
</CardTitle>
|
||||
<CardDescription>Recently submitted projects</CardDescription>
|
||||
</div>
|
||||
<Link
|
||||
@@ -453,15 +671,19 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
<div className="space-y-6 lg:col-span-5">
|
||||
{/* Pending Actions Card */}
|
||||
<AnimatedCard index={6}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<CardTitle className="flex items-center gap-2.5">
|
||||
<div className="rounded-lg bg-amber-500/10 p-1.5">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
||||
</div>
|
||||
Pending Actions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -503,12 +725,16 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
{/* Evaluation Progress Card */}
|
||||
<AnimatedCard index={7}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
<CardTitle className="flex items-center gap-2.5">
|
||||
<div className="rounded-lg bg-brand-teal/10 p-1.5">
|
||||
<TrendingUp className="h-4 w-4 text-brand-teal" />
|
||||
</div>
|
||||
Evaluation Progress
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -532,7 +758,7 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
|
||||
{round.evalPercent}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={round.evalPercent} className="h-2" />
|
||||
<Progress value={round.evalPercent} className="h-2" gradient />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{round.submittedEvals} of {round.totalEvals} evaluations submitted
|
||||
</p>
|
||||
@@ -542,12 +768,16 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
{/* Category Breakdown Card */}
|
||||
<AnimatedCard index={8}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4" />
|
||||
<CardTitle className="flex items-center gap-2.5">
|
||||
<div className="rounded-lg bg-violet-500/10 p-1.5">
|
||||
<Layers className="h-4 w-4 text-violet-500" />
|
||||
</div>
|
||||
Project Categories
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -607,12 +837,16 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
{/* Recent Activity Card */}
|
||||
<AnimatedCard index={9}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4" />
|
||||
<CardTitle className="flex items-center gap-2.5">
|
||||
<div className="rounded-lg bg-blue-500/10 p-1.5">
|
||||
<Activity className="h-4 w-4 text-blue-500" />
|
||||
</div>
|
||||
Recent Activity
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -646,12 +880,16 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
{/* Upcoming Deadlines Card */}
|
||||
<AnimatedCard index={10}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<CardTitle className="flex items-center gap-2.5">
|
||||
<div className="rounded-lg bg-rose-500/10 p-1.5">
|
||||
<Calendar className="h-4 w-4 text-rose-500" />
|
||||
</div>
|
||||
Upcoming Deadlines
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -688,6 +926,7 @@ export function DashboardContent({ editionId, sessionName }: DashboardContentPro
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user