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
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:
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user