From 9f7b76b3cbb8db282fb39b54fae913c5424b2d5d Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 20 Feb 2026 23:09:06 +0100 Subject: [PATCH] Dashboard layout overhaul + fix Tremor chart colors and tooltips - Restructure dashboard: score distribution + recently reviewed stacked in left column, full-width map at bottom, activity feed in middle row - Show all jurors in scrollable workload list (not just top 5) - Filter recently reviewed to exclude rejected/not-reviewed projects - Filter transition audit logs from activity feed - Remove completion progress bar from stat tile for equal card heights - Fix all Tremor charts: switch hex colors to named palette (cyan/teal/emerald/amber/rose) to fix black bar rendering - Fix transparent chart tooltips with global CSS overrides - Remove tilted text labels from cross-round comparison charts Co-Authored-By: Claude Opus 4.6 --- src/app/globals.css | 19 ++ src/components/charts/chart-theme.ts | 31 +- src/components/charts/criteria-scores.tsx | 3 +- .../charts/cross-round-comparison.tsx | 20 +- src/components/charts/diversity-metrics.tsx | 8 +- src/components/charts/evaluation-timeline.tsx | 3 +- src/components/charts/juror-consistency.tsx | 3 +- src/components/charts/juror-workload.tsx | 3 +- src/components/charts/project-rankings.tsx | 3 +- src/components/charts/score-distribution.tsx | 3 +- src/components/charts/status-breakdown.tsx | 4 +- .../observer/observer-dashboard-content.tsx | 309 +++++++++--------- 12 files changed, 223 insertions(+), 186 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 1c18cb4..6e06329 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -294,3 +294,22 @@ background: hsl(var(--muted-foreground) / 0.5); } } + +/* Tremor chart tooltip fix — ensure solid background */ +[class*="tremor-"] [role="tooltip"], +.recharts-tooltip-wrapper .recharts-default-tooltip, +div[class*="tremor"][class*="tooltip"], +div[class*="recharts-tooltip"] { + background-color: hsl(var(--card)) !important; + border: 1px solid hsl(var(--border)) !important; + border-radius: 0.5rem !important; + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1) !important; + opacity: 1 !important; +} + +.dark div[class*="tremor"][class*="tooltip"], +.dark .recharts-tooltip-wrapper .recharts-default-tooltip, +.dark div[class*="recharts-tooltip"] { + background-color: hsl(var(--card)) !important; + border-color: hsl(var(--border)) !important; +} diff --git a/src/components/charts/chart-theme.ts b/src/components/charts/chart-theme.ts index ae40a0a..f114efd 100644 --- a/src/components/charts/chart-theme.ts +++ b/src/components/charts/chart-theme.ts @@ -18,6 +18,35 @@ export const BRAND_COLORS = [ '#a83240', // Rose ] as const +// Tremor named colors for chart components +// These are the official Tremor palette names that render correctly +export const TREMOR_BRAND = 'cyan' as const +export const TREMOR_ACCENT = 'teal' as const +export const TREMOR_CHART_COLORS = [ + 'cyan', + 'teal', + 'blue', + 'emerald', + 'amber', + 'violet', + 'rose', + 'indigo', + 'lime', + 'fuchsia', +] as const + +// Donut / status chart colors (mapped to Tremor names) +export const TREMOR_STATUS_COLORS: Record = { + SUBMITTED: 'slate', + ELIGIBLE: 'cyan', + ASSIGNED: 'violet', + SEMIFINALIST: 'amber', + FINALIST: 'emerald', + REJECTED: 'rose', + DRAFT: 'gray', + WITHDRAWN: 'neutral', +} + // Project status colors — mapped to actual ProjectStatus enum values export const STATUS_COLORS: Record = { SUBMITTED: '#557f8c', // Teal @@ -76,7 +105,7 @@ function lerpColor(a: string, b: string, t: number): string { * Falls back to a neutral gray */ export function getStatusColor(status: string): string { - return STATUS_COLORS[status] || '#9ca3af' + return TREMOR_STATUS_COLORS[status] || 'gray' } /** diff --git a/src/components/charts/criteria-scores.tsx b/src/components/charts/criteria-scores.tsx index f82b9ce..5d30961 100644 --- a/src/components/charts/criteria-scores.tsx +++ b/src/components/charts/criteria-scores.tsx @@ -2,7 +2,6 @@ import { BarChart } from '@tremor/react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { BRAND_TEAL } from './chart-theme' interface CriteriaScoreData { id: string @@ -44,7 +43,7 @@ export function CriteriaScoresChart({ data }: CriteriaScoresProps) { data={chartData} index="criterion" categories={['Avg Score']} - colors={[BRAND_TEAL] as string[]} + colors={['teal']} maxValue={10} layout="vertical" yAxisWidth={160} diff --git a/src/components/charts/cross-round-comparison.tsx b/src/components/charts/cross-round-comparison.tsx index c748829..391613a 100644 --- a/src/components/charts/cross-round-comparison.tsx +++ b/src/components/charts/cross-round-comparison.tsx @@ -2,7 +2,6 @@ import { BarChart } from '@tremor/react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { BRAND_COLORS } from './chart-theme' interface StageComparison { roundId: string @@ -32,10 +31,7 @@ export function CrossStageComparisonChart({ } const baseData = data.map((round) => ({ - name: - round.roundName.length > 20 - ? round.roundName.slice(0, 20) + '...' - : round.roundName, + name: round.roundName, Projects: round.projectCount, Evaluations: round.evaluationCount, 'Completion Rate': round.completionRate, @@ -50,7 +46,7 @@ export function CrossStageComparisonChart({ Round Metrics Comparison -
+
Projects @@ -60,11 +56,10 @@ export function CrossStageComparisonChart({ data={baseData} index="name" categories={['Projects']} - colors={[BRAND_COLORS[0]] as string[]} + colors={['cyan']} showLegend={false} yAxisWidth={40} className="h-[200px]" - rotateLabelX={{ angle: -25, xAxisHeight: 40 }} /> @@ -80,11 +75,10 @@ export function CrossStageComparisonChart({ data={baseData} index="name" categories={['Evaluations']} - colors={[BRAND_COLORS[2]] as string[]} + colors={['teal']} showLegend={false} yAxisWidth={40} className="h-[200px]" - rotateLabelX={{ angle: -25, xAxisHeight: 40 }} /> @@ -100,13 +94,12 @@ export function CrossStageComparisonChart({ data={baseData} index="name" categories={['Completion Rate']} - colors={[BRAND_COLORS[1]] as string[]} + colors={['emerald']} showLegend={false} maxValue={100} yAxisWidth={40} valueFormatter={(v) => `${v}%`} className="h-[200px]" - rotateLabelX={{ angle: -25, xAxisHeight: 40 }} /> @@ -122,12 +115,11 @@ export function CrossStageComparisonChart({ data={baseData} index="name" categories={['Avg Score']} - colors={[BRAND_COLORS[0]] as string[]} + colors={['amber']} showLegend={false} maxValue={10} yAxisWidth={40} className="h-[200px]" - rotateLabelX={{ angle: -25, xAxisHeight: 40 }} /> diff --git a/src/components/charts/diversity-metrics.tsx b/src/components/charts/diversity-metrics.tsx index fff8a26..a56317b 100644 --- a/src/components/charts/diversity-metrics.tsx +++ b/src/components/charts/diversity-metrics.tsx @@ -3,7 +3,7 @@ import { DonutChart, BarChart } from '@tremor/react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' -import { BRAND_COLORS } from './chart-theme' +import { TREMOR_CHART_COLORS } from './chart-theme' interface DiversityData { total: number @@ -116,7 +116,7 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) { data={donutData} category="value" index="name" - colors={[...BRAND_COLORS] as string[]} + colors={[...TREMOR_CHART_COLORS]} className="h-[400px]" /> ) : ( @@ -136,7 +136,7 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) { data={categoryData} index="category" categories={['Count']} - colors={[BRAND_COLORS[0]] as string[]} + colors={['cyan']} layout="horizontal" yAxisWidth={120} showLegend={false} @@ -160,7 +160,7 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) { data={oceanIssueData} index="issue" categories={['Count']} - colors={[BRAND_COLORS[2]] as string[]} + colors={['teal']} showLegend={false} yAxisWidth={40} className="h-[400px]" diff --git a/src/components/charts/evaluation-timeline.tsx b/src/components/charts/evaluation-timeline.tsx index e207ea9..781656e 100644 --- a/src/components/charts/evaluation-timeline.tsx +++ b/src/components/charts/evaluation-timeline.tsx @@ -2,7 +2,6 @@ import { AreaChart } from '@tremor/react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { BRAND_DARK_BLUE, BRAND_TEAL } from './chart-theme' interface TimelineDataPoint { date: string @@ -44,7 +43,7 @@ export function EvaluationTimelineChart({ data }: EvaluationTimelineProps) { data={chartData} index="date" categories={['Cumulative', 'Daily']} - colors={[BRAND_DARK_BLUE, BRAND_TEAL] as string[]} + colors={['cyan', 'teal']} curveType="monotone" showGradient={true} yAxisWidth={50} diff --git a/src/components/charts/juror-consistency.tsx b/src/components/charts/juror-consistency.tsx index 75a9ae9..b5fdad8 100644 --- a/src/components/charts/juror-consistency.tsx +++ b/src/components/charts/juror-consistency.tsx @@ -12,7 +12,6 @@ import { TableRow, } from '@/components/ui/table' import { AlertTriangle } from 'lucide-react' -import { BRAND_DARK_BLUE, BRAND_RED } from './chart-theme' interface JurorMetric { userId: string @@ -77,7 +76,7 @@ export function JurorConsistencyChart({ data }: JurorConsistencyProps) { y="Std Deviation" category="category" size="size" - colors={[BRAND_DARK_BLUE, BRAND_RED] as string[]} + colors={['cyan', 'rose']} className="h-[400px]" />

diff --git a/src/components/charts/juror-workload.tsx b/src/components/charts/juror-workload.tsx index 1ad9d0b..98dc8ad 100644 --- a/src/components/charts/juror-workload.tsx +++ b/src/components/charts/juror-workload.tsx @@ -2,7 +2,6 @@ import { BarChart } from '@tremor/react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { BRAND_DARK_BLUE } from './chart-theme' interface JurorWorkloadData { id: string @@ -49,7 +48,7 @@ export function JurorWorkloadChart({ data }: JurorWorkloadProps) { data={chartData} index="juror" categories={['Completed', 'Remaining']} - colors={[BRAND_DARK_BLUE, '#e5e7eb'] as string[]} + colors={['cyan', 'gray']} layout="horizontal" stack={true} yAxisWidth={160} diff --git a/src/components/charts/project-rankings.tsx b/src/components/charts/project-rankings.tsx index d643b82..e2f45d5 100644 --- a/src/components/charts/project-rankings.tsx +++ b/src/components/charts/project-rankings.tsx @@ -2,7 +2,6 @@ import { BarChart } from '@tremor/react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { BRAND_TEAL } from './chart-theme' interface ProjectRankingData { id: string @@ -52,7 +51,7 @@ export function ProjectRankingsChart({ data={chartData} index="project" categories={['Score']} - colors={[BRAND_TEAL] as string[]} + colors={['teal']} layout="horizontal" yAxisWidth={200} maxValue={10} diff --git a/src/components/charts/score-distribution.tsx b/src/components/charts/score-distribution.tsx index 4018e08..ed5adf2 100644 --- a/src/components/charts/score-distribution.tsx +++ b/src/components/charts/score-distribution.tsx @@ -2,7 +2,6 @@ import { BarChart } from '@tremor/react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { BRAND_TEAL } from './chart-theme' interface ScoreDistributionProps { data: { score: number; count: number }[] @@ -37,7 +36,7 @@ export function ScoreDistributionChart({ data={chartData} index="score" categories={['Count']} - colors={[BRAND_TEAL] as (string)[]} + colors={['cyan']} yAxisWidth={40} showLegend={false} className="h-[300px]" diff --git a/src/components/charts/status-breakdown.tsx b/src/components/charts/status-breakdown.tsx index 72f25dd..e0052a7 100644 --- a/src/components/charts/status-breakdown.tsx +++ b/src/components/charts/status-breakdown.tsx @@ -2,7 +2,7 @@ import { DonutChart } from '@tremor/react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { getStatusColor, formatStatus } from './chart-theme' +import { formatStatus, getStatusColor } from './chart-theme' interface StatusDataPoint { status: string @@ -40,7 +40,7 @@ export function StatusBreakdownChart({ data }: StatusBreakdownProps) { data={chartData} category="value" index="name" - colors={colors as string[]} + colors={colors} showLabel={true} className="h-[300px]" /> diff --git a/src/components/observer/observer-dashboard-content.tsx b/src/components/observer/observer-dashboard-content.tsx index 642f171..44c3112 100644 --- a/src/components/observer/observer-dashboard-content.tsx +++ b/src/components/observer/observer-dashboard-content.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, Fragment } from 'react' +import { useState } from 'react' import Link from 'next/link' import type { Route } from 'next' import { trpc } from '@/lib/trpc/client' @@ -172,7 +172,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) { const avgScore = stats ? computeAvgScore(stats.scoreDistribution) : '—' - const topJurors = (jurorWorkload ?? []).slice(0, 5) + const allJurors = jurorWorkload ?? [] const scoreColors: Record = { '9-10': '#053d57', @@ -186,6 +186,13 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) { ? Math.max(...stats.scoreDistribution.map((b) => b.count), 1) : 1 + const recentlyReviewed = (projectsData?.projects ?? []).filter( + (p) => { + const status = p.observerStatus ?? p.status + return status !== 'REJECTED' && status !== 'NOT_REVIEWED' && status !== 'SUBMITTED' + }, + ) + return (

{/* Header */} @@ -243,10 +250,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {

{stats.completionRate}%

-
- -
-

Completion

+

Completion

@@ -345,55 +349,124 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) { {/* Middle Row */}
- {/* Score Distribution */} - - - - -
- -
- Score Distribution -
- Evaluation score buckets -
- - {stats ? ( -
- {stats.scoreDistribution.map((bucket) => ( -
- - {bucket.label} - -
-
0 ? (bucket.count / maxScoreCount) * 100 : 0}%`, - backgroundColor: scoreColors[bucket.label] ?? '#557f8c', - }} - /> + {/* Left column: Score Distribution + Recently Reviewed stacked */} +
+ {/* Score Distribution */} + + + + +
+ +
+ Score Distribution +
+
+ + {stats ? ( +
+ {stats.scoreDistribution.map((bucket) => ( +
+ + {bucket.label} + +
+
0 ? (bucket.count / maxScoreCount) * 100 : 0}%`, + backgroundColor: scoreColors[bucket.label] ?? '#557f8c', + }} + /> +
+ + {bucket.count} +
- - {bucket.count} - -
- ))} -
- ) : ( -
- {[...Array(5)].map((_, i) => ( - - ))} -
- )} -
-
-
+ ))} +
+ ) : ( +
+ {[...Array(5)].map((_, i) => ( + + ))} +
+ )} + + + - {/* Juror Workload */} + {/* Recently Reviewed */} + + + + +
+ +
+ Recently Reviewed +
+ Latest project reviews +
+ + {recentlyReviewed.length > 0 ? ( + <> + + + + Project + Status + Score + + + + {recentlyReviewed.map((project) => ( + + + + {project.title} + + + + + + + {project.evaluationCount > 0 && project.averageScore !== null + ? project.averageScore.toFixed(1) + : '—'} + + + ))} + +
+
+ + View All + +
+ + ) : ( +
+ {[...Array(3)].map((_, i) => ( + + ))} +
+ )} +
+
+
+
+ + {/* Juror Workload — scrollable list of all jurors */} - +
@@ -401,12 +474,12 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
Juror Workload
- Top 5 jurors by assignment + All jurors by assignment
- - {topJurors.length > 0 ? ( -
- {topJurors.map((juror) => { + + {allJurors.length > 0 ? ( +
+ {allJurors.map((juror) => { const isExpanded = expandedJurorId === juror.id return (
@@ -467,102 +540,9 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) { - {/* Project Origins */} - - {selectedProgramId ? ( - - ) : ( - - - - - Project Origins - - Geographic distribution of projects - - - - - - )} - -
- - {/* Bottom Row */} -
- {/* Recent Projects Table */} - - - - -
- -
- Recently Reviewed -
- Latest project reviews -
- - {projectsData && projectsData.projects.length > 0 ? ( - <> - - - - Project - Status - Score - - - - {projectsData.projects.map((project) => ( - - - - {project.title} - - {project.teamName && ( -

{project.teamName}

- )} -
- - - - - {project.evaluationCount > 0 && project.averageScore !== null - ? project.averageScore.toFixed(1) - : '—'} - -
- ))} -
-
-
- - View All - -
- - ) : ( -
- {[...Array(5)].map((_, i) => ( - - ))} -
- )} -
-
-
- {/* Activity Feed */} - - + +
@@ -575,7 +555,10 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) { {activityFeed && activityFeed.length > 0 ? (
- {activityFeed.slice(0, 5).map((item) => { + {activityFeed + .filter((item) => !item.eventType.includes('transitioned') && !item.eventType.includes('transition')) + .slice(0, 5) + .map((item) => { const iconDef = ACTIVITY_ICONS[item.eventType] const IconComponent = iconDef?.icon ?? Activity const iconColor = iconDef?.color ?? 'text-slate-400' @@ -607,6 +590,26 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
+ + {/* Full-width Map */} + + {selectedProgramId ? ( + + ) : ( + + + + + Project Origins + + Geographic distribution of projects + + + + + + )} +
) }