Observer platform redesign Phase 4: migrate charts to Tremor, redesign all pages
Some checks failed
Build and Push Docker Image / build (push) Failing after 23s

- Migrate 9 chart components from Nivo to @tremor/react (BarChart, AreaChart, DonutChart, ScatterChart)
- Remove @nivo/*, @react-spring/web dependencies (45 packages removed)
- Redesign dashboard: 6 stat tiles, competition pipeline, score distribution, juror workload, activity feed
- Add new /observer/projects page with search, filters, sorting, pagination, CSV export
- Restructure reports page from 5 tabs to 3 (Progress, Jurors, Scores & Analytics) with per-tab CSV export
- Redesign project detail: breadcrumb nav, score card header, 3-tab layout (Overview/Evaluations/Files)
- Update loading skeletons to match new layouts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 21:45:01 +01:00
parent 77cbc64b33
commit 8125ca6567
24 changed files with 3412 additions and 3401 deletions

View File

@@ -1,10 +1,9 @@
'use client'
import { ResponsivePie } from '@nivo/pie'
import { ResponsiveBar } from '@nivo/bar'
import { DonutChart, BarChart } from '@tremor/react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { nivoTheme, BRAND_COLORS } from './chart-theme'
import { BRAND_COLORS } from './chart-theme'
interface DiversityData {
total: number
@@ -49,10 +48,10 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
)
}
// Top countries for pie chart (max 10, others grouped)
// Top countries for donut chart (max 10, others grouped)
const topCountries = (data.byCountry || []).slice(0, 10)
const otherCountries = (data.byCountry || []).slice(10)
const countryPieData = otherCountries.length > 0
const countryData = otherCountries.length > 0
? [...topCountries, {
country: 'Others',
count: otherCountries.reduce((sum, c) => sum + c.count, 0),
@@ -60,21 +59,19 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
}]
: topCountries
const nivoPieData = countryPieData.map((c) => ({
id: c.country === 'Others' ? 'Others' : c.country.toUpperCase(),
label: getCountryName(c.country),
const donutData = countryData.map((c) => ({
name: getCountryName(c.country),
value: c.count,
}))
// Pre-format category and ocean issue data for display
const formattedCategories = (data.byCategory || []).slice(0, 10).map((c) => ({
const categoryData = (data.byCategory || []).slice(0, 10).map((c) => ({
category: formatLabel(c.category),
count: c.count,
Count: c.count,
}))
const formattedOceanIssues = (data.byOceanIssue || []).slice(0, 15).map((o) => ({
const oceanIssueData = (data.byOceanIssue || []).slice(0, 15).map((o) => ({
issue: formatLabel(o.issue),
count: o.count,
Count: o.count,
}))
return (
@@ -114,45 +111,17 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
<CardTitle>Geographic Distribution</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '400px' }}>
{nivoPieData.length > 0 ? <ResponsivePie
data={nivoPieData}
theme={nivoTheme}
colors={[...BRAND_COLORS]}
innerRadius={0.4}
padAngle={0.5}
cornerRadius={3}
activeOuterRadiusOffset={8}
margin={{ top: 40, right: 80, bottom: 80, left: 80 }}
enableArcLinkLabels={true}
arcLinkLabelsSkipAngle={10}
arcLinkLabelsTextColor="#374151"
arcLinkLabelsThickness={2}
arcLinkLabelsColor={{ from: 'color' }}
enableArcLabels={true}
arcLabelsSkipAngle={10}
arcLabelsTextColor={{ from: 'color', modifiers: [['darker', 2]] }}
legends={[
{
anchor: 'bottom',
direction: 'row',
justify: false,
translateX: 0,
translateY: 56,
itemsSpacing: 0,
itemWidth: 100,
itemHeight: 18,
itemTextColor: '#374151',
itemDirection: 'left-to-right',
itemOpacity: 1,
symbolSize: 12,
symbolShape: 'circle',
},
]}
/> : (
<p className="text-muted-foreground text-center py-8">No geographic data</p>
)}
</div>
{donutData.length > 0 ? (
<DonutChart
data={donutData}
category="value"
index="name"
colors={[...BRAND_COLORS] as string[]}
className="h-[400px]"
/>
) : (
<p className="text-muted-foreground text-center py-8">No geographic data</p>
)}
</CardContent>
</Card>
@@ -162,29 +131,17 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
<CardTitle>Competition Categories</CardTitle>
</CardHeader>
<CardContent>
{formattedCategories.length > 0 ? (
<div style={{ height: '400px' }}>
<ResponsiveBar
data={formattedCategories}
theme={nivoTheme}
keys={['count']}
indexBy="category"
layout="horizontal"
colors={[BRAND_COLORS[0]]}
borderRadius={4}
margin={{ top: 10, right: 30, bottom: 10, left: 120 }}
padding={0.3}
enableLabel={true}
labelTextColor="#ffffff"
enableGridX={true}
enableGridY={false}
axisBottom={null}
axisLeft={{
tickSize: 0,
tickPadding: 8,
}}
/>
</div>
{categoryData.length > 0 ? (
<BarChart
data={categoryData}
index="category"
categories={['Count']}
colors={[BRAND_COLORS[0]] as string[]}
layout="horizontal"
yAxisWidth={120}
showLegend={false}
className="h-[400px]"
/>
) : (
<p className="text-muted-foreground text-center py-8">No category data</p>
)}
@@ -193,38 +150,22 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
</div>
{/* Ocean Issues */}
{formattedOceanIssues.length > 0 && (
{oceanIssueData.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Ocean Issues Addressed</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '400px' }}>
<ResponsiveBar
data={formattedOceanIssues}
theme={nivoTheme}
keys={['count']}
indexBy="issue"
layout="vertical"
colors={[BRAND_COLORS[2]]}
borderRadius={4}
margin={{ top: 20, right: 30, bottom: 80, left: 40 }}
padding={0.3}
enableLabel={true}
labelTextColor="#ffffff"
enableGridX={false}
enableGridY={true}
axisBottom={{
tickSize: 0,
tickPadding: 8,
tickRotation: -35,
}}
axisLeft={{
tickSize: 0,
tickPadding: 8,
}}
/>
</div>
<BarChart
data={oceanIssueData}
index="issue"
categories={['Count']}
colors={[BRAND_COLORS[2]] as string[]}
showLegend={false}
yAxisWidth={40}
className="h-[400px]"
rotateLabelX={{ angle: -35, xAxisHeight: 80 }}
/>
</CardContent>
</Card>
)}