/** * ComparisonView Component * Client-side comparison interface with charts and tables */ 'use client'; import { useEffect, useState } from 'react'; import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import { useComparison } from '@/hooks/useComparison'; import { ComparisonChart } from './ComparisonChart'; import { SchoolSearchModal } from './SchoolSearchModal'; import { EmptyState } from './EmptyState'; import { LoadingSkeleton } from './LoadingSkeleton'; import type { ComparisonData, MetricDefinition } from '@/lib/types'; import { formatPercentage, formatProgress, CHART_COLORS } from '@/lib/utils'; import { fetchComparison } from '@/lib/api'; import styles from './ComparisonView.module.css'; interface ComparisonViewProps { initialData: Record | null; initialUrns: number[]; metrics: MetricDefinition[]; selectedMetric: string; } export function ComparisonView({ initialData, initialUrns, metrics, selectedMetric: initialMetric, }: ComparisonViewProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const { selectedSchools, removeSchool, addSchool, isInitialized } = useComparison(); const [selectedMetric, setSelectedMetric] = useState(initialMetric); const [isModalOpen, setIsModalOpen] = useState(false); const [comparisonData, setComparisonData] = useState(initialData); const [shareConfirm, setShareConfirm] = useState(false); // Seed context from initialData when component mounts and localStorage is empty useEffect(() => { if (!isInitialized) return; if (selectedSchools.length === 0 && initialUrns.length > 0 && initialData) { initialUrns.forEach(urn => { const data = initialData[String(urn)]; if (data?.school_info) { addSchool(data.school_info); } }); } }, [isInitialized]); // eslint-disable-line react-hooks/exhaustive-deps // Sync URL with selected schools useEffect(() => { const urns = selectedSchools.map((s) => s.urn).join(','); const params = new URLSearchParams(searchParams); if (urns) { params.set('urns', urns); } else { params.delete('urns'); } params.set('metric', selectedMetric); const newUrl = `${pathname}?${params.toString()}`; router.replace(newUrl, { scroll: false }); // Fetch comparison data if (selectedSchools.length > 0) { fetchComparison(urns, { cache: 'no-store' }) .then((data) => { setComparisonData(data.comparison); }) .catch((err) => { console.error('Failed to fetch comparison:', err); setComparisonData(null); }); } else { setComparisonData(null); } }, [selectedSchools, selectedMetric, pathname, searchParams, router]); const handleMetricChange = (metric: string) => { setSelectedMetric(metric); }; const handleRemoveSchool = (urn: number) => { removeSchool(urn); }; const handleShare = async () => { try { await navigator.clipboard.writeText(window.location.href); setShareConfirm(true); setTimeout(() => setShareConfirm(false), 2000); } catch { /* fallback: do nothing */ } }; // Get metric definition const currentMetricDef = metrics.find((m) => m.key === selectedMetric); const metricLabel = currentMetricDef?.label || selectedMetric; // No schools selected if (selectedSchools.length === 0) { return (

Compare Schools

Add schools to your comparison basket to see side-by-side performance data

setIsModalOpen(true), }} /> setIsModalOpen(false)} />
); } // Get years for table const years = comparisonData && Object.keys(comparisonData).length > 0 ? comparisonData[Object.keys(comparisonData)[0]].yearly_data.map((d) => d.year) : []; return (
{/* Header */}

Compare Schools

Comparing {selectedSchools.length} school{selectedSchools.length !== 1 ? 's' : ''}

{/* Metric Selector */}
{currentMetricDef?.description && (

{currentMetricDef.description}

)}
{/* Progress score explanation */} {selectedMetric.includes('progress') && (

Progress scores measure pupils' progress from KS1 to KS2. A score of 0 equals the national average; positive scores are above average.

)} {/* School Cards */}
{selectedSchools.map((school, index) => (

{school.school_name}

{school.local_authority && ( {school.local_authority} )} {school.school_type && ( {school.school_type} )}
{/* Latest metric value */} {comparisonData && comparisonData[school.urn] && (
{metricLabel}
{(() => { const yearlyData = comparisonData[school.urn].yearly_data; if (yearlyData.length === 0) return '-'; const latestData = yearlyData[yearlyData.length - 1]; const value = latestData[selectedMetric as keyof typeof latestData]; if (value === null || value === undefined) return '-'; // Format based on metric type if (selectedMetric.includes('progress')) { return formatProgress(value as number); } else if (selectedMetric.includes('pct') || selectedMetric.includes('rate')) { return formatPercentage(value as number); } else { return typeof value === 'number' ? value.toFixed(1) : String(value); } })()}
)}
))}
{/* Comparison Chart */} {comparisonData && Object.keys(comparisonData).length > 0 ? (

Performance Over Time

) : selectedSchools.length > 0 ? (
) : null} {/* Comparison Table */} {comparisonData && Object.keys(comparisonData).length > 0 && years.length > 0 && (

Detailed Comparison

{selectedSchools.map((school) => ( ))} {years.map((year) => ( {selectedSchools.map((school) => { const schoolData = comparisonData[school.urn]; if (!schoolData) return ; const yearData = schoolData.yearly_data.find((d) => d.year === year); if (!yearData) return ; const value = yearData[selectedMetric as keyof typeof yearData]; if (value === null || value === undefined) { return ; } // Format based on metric type let displayValue: string; if (selectedMetric.includes('progress')) { displayValue = formatProgress(value as number); } else if (selectedMetric.includes('pct') || selectedMetric.includes('rate')) { displayValue = formatPercentage(value as number); } else { displayValue = typeof value === 'number' ? value.toFixed(1) : String(value); } return ; })} ))}
Year{school.school_name}
{year}---{displayValue}
)} {/* School Search Modal */} setIsModalOpen(false)} />
); }