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 } from '@nivo/bar'
import { BarChart } from '@tremor/react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { nivoTheme, scoreGradient } from './chart-theme'
import { BRAND_TEAL } from './chart-theme'
interface ProjectRankingData {
id: string
@@ -18,14 +18,6 @@ interface ProjectRankingsProps {
limit?: number
}
type RankingBarDatum = {
project: string
score: number
fullTitle: string
teamName: string
evaluationCount: number
}
export function ProjectRankingsChart({
data,
limit = 20,
@@ -37,21 +29,12 @@ export function ProjectRankingsChart({
if (!scoredData.length) return null
const averageScore =
scoredData.length > 0
? scoredData.reduce((sum, d) => sum + d.averageScore, 0) /
scoredData.length
: 0
const displayData = scoredData.slice(0, limit)
const chartData: RankingBarDatum[] = displayData.map((d) => ({
const chartData = displayData.map((d) => ({
project:
d.title.length > 30 ? d.title.substring(0, 30) + '...' : d.title,
score: d.averageScore,
fullTitle: d.title,
teamName: d.teamName ?? '',
evaluationCount: d.evaluationCount,
Score: parseFloat(d.averageScore.toFixed(2)),
}))
return (
@@ -65,75 +48,18 @@ export function ProjectRankingsChart({
</CardTitle>
</CardHeader>
<CardContent>
<div
style={{
height: `${Math.max(400, displayData.length * 30)}px`,
}}
>
<ResponsiveBar
data={chartData}
keys={['score']}
indexBy="project"
layout="horizontal"
theme={nivoTheme}
colors={(bar) => scoreGradient(bar.data.score as number)}
valueScale={{ type: 'linear', max: 10 }}
borderRadius={4}
enableLabel={true}
label={(d) => {
const v = d.value
return v != null ? Number(v).toFixed(1) : ''
}}
labelSkipWidth={30}
labelTextColor="#ffffff"
margin={{ top: 10, right: 30, bottom: 30, left: 200 }}
padding={0.2}
markers={[
{
axis: 'x',
value: averageScore,
lineStyle: {
stroke: '#6b7280',
strokeWidth: 2,
strokeDasharray: '6 4',
},
legend: `Avg: ${averageScore.toFixed(1)}`,
legendPosition: 'top',
textStyle: {
fill: '#6b7280',
fontSize: 11,
},
},
]}
tooltip={({ 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.fullTitle}</strong>
{rowData.teamName && (
<>
<br />
<span style={{ color: '#6b7280' }}>
{rowData.teamName}
</span>
</>
)}
<br />
Score: {Number(rowData.score).toFixed(2)}
<br />
Evaluations: {rowData.evaluationCount}
</div>
)}
animate={true}
/>
</div>
<BarChart
data={chartData}
index="project"
categories={['Score']}
colors={[BRAND_TEAL] as string[]}
layout="horizontal"
yAxisWidth={200}
maxValue={10}
showLegend={false}
className={`h-[${Math.max(400, displayData.length * 30)}px]`}
style={{ height: `${Math.max(400, displayData.length * 30)}px` }}
/>
</CardContent>
</Card>
)