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,8 +1,8 @@
'use client'
import { ResponsiveBar, type ComputedDatum } from '@nivo/bar'
import { BarChart } from '@tremor/react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { nivoTheme } from './chart-theme'
import { BRAND_DARK_BLUE } from './chart-theme'
interface JurorWorkloadData {
id: string
@@ -16,14 +16,6 @@ interface JurorWorkloadProps {
data: JurorWorkloadData[]
}
type WorkloadBarDatum = {
juror: string
completed: number
remaining: number
completionRate: number
fullName: string
}
export function JurorWorkloadChart({ data }: JurorWorkloadProps) {
if (!data?.length) return null
@@ -36,12 +28,10 @@ export function JurorWorkloadChart({ data }: JurorWorkloadProps) {
(a, b) => b.completionRate - a.completionRate,
)
const chartData: WorkloadBarDatum[] = sortedData.map((d) => ({
const chartData = sortedData.map((d) => ({
juror: d.name.length > 25 ? d.name.substring(0, 25) + '...' : d.name,
completed: d.completed,
remaining: d.assigned - d.completed,
completionRate: d.completionRate,
fullName: d.name,
Completed: d.completed,
Remaining: d.assigned - d.completed,
}))
return (
@@ -55,66 +45,17 @@ export function JurorWorkloadChart({ data }: JurorWorkloadProps) {
</CardTitle>
</CardHeader>
<CardContent>
<div
<BarChart
data={chartData}
index="juror"
categories={['Completed', 'Remaining']}
colors={[BRAND_DARK_BLUE, '#e5e7eb'] as string[]}
layout="horizontal"
stack={true}
yAxisWidth={160}
className={`h-[${Math.max(300, data.length * 35)}px]`}
style={{ height: `${Math.max(300, data.length * 35)}px` }}
>
<ResponsiveBar
data={chartData}
keys={['completed', 'remaining']}
indexBy="juror"
layout="horizontal"
theme={nivoTheme}
colors={['#053d57', '#e5e7eb']}
borderRadius={2}
enableLabel={true}
label={(d: ComputedDatum<WorkloadBarDatum>) => {
if (d.id === 'completed') {
return `${d.data.completionRate}%`
}
return ''
}}
labelSkipWidth={40}
labelTextColor={(d) => {
const datum = d as unknown as { data: ComputedDatum<WorkloadBarDatum> }
return datum.data.id === 'completed' ? '#ffffff' : '#374151'
}}
margin={{ top: 10, right: 30, bottom: 30, left: 160 }}
padding={0.25}
groupMode="stacked"
tooltip={({ id, value, data: rowData }) => (
<div
style={{
background: '#ffffff',
padding: '8px 12px',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
border: '1px solid #e5e7eb',
fontSize: 12,
}}
>
<strong>{rowData.fullName}</strong>
<br />
{id === 'completed' ? 'Completed' : 'Remaining'}: {value}
<br />
Completion: {rowData.completionRate}%
</div>
)}
legends={[
{
dataFrom: 'keys',
anchor: 'bottom',
direction: 'row',
translateY: 30,
itemsSpacing: 20,
itemWidth: 100,
itemHeight: 18,
symbolSize: 12,
symbolShape: 'square',
},
]}
animate={true}
/>
</div>
/>
</CardContent>
</Card>
)