Addresses 28 issues identified in UX audit (P0–P3 severity): P0 — Critical: - Fix compare URL sharing: seed ComparisonContext from SSR initialData when localStorage is empty, making /compare?urns=... links shareable - Remove permanently broken "Avg. Scaled Score" column from school detail historical data table P1 — High priority: - Add radius selector (0.5–10 mi) to postcode search in FilterBar - Make Add to Compare a toggle (remove) on SchoolCards - Hide hero title/description once a search is active - Show school count + quick-search prompts on empty landing page - Compare empty state opens in-page school search modal directly - Remove URN from school detail header (irrelevant to end users) - Move map above performance chart in school detail page - Add ← Back navigation to school detail page - Add sort controls to search results (RWM%, distance, A–Z) - Show metric descriptions below metric selector - Expand ComparisonToast to list school names with per-school remove - Add progress score explainer (0 = national average) throughout P2 — Medium: - Remove console.log statements from ComparisonView - Colour-code comparison school cards to match chart line colours - Replace plain loading text with LoadingSkeleton in ComparisonView - Rankings empty state uses shared EmptyState component - Rankings year filter shows actual year e.g. "2023 (Latest)" - Rankings subtitle shows top-N count - Add View link alongside Add button in rankings table - Remove placeholder Privacy Policy / Terms links from footer - Replace untappable 10px info icons with visible metric hint text - Show active filter chips in search results header P3 — Polish: - Remove redundant "Home" nav link (logo already links home) - Add / and Ctrl+K keyboard shortcut to focus search input - Add Share button to compare page (copies URL to clipboard) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
328 lines
13 KiB
TypeScript
328 lines
13 KiB
TypeScript
/**
|
|
* SchoolDetailView Component
|
|
* Displays comprehensive school information with performance charts
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
import { useComparison } from '@/hooks/useComparison';
|
|
import { PerformanceChart } from './PerformanceChart';
|
|
import { SchoolMap } from './SchoolMap';
|
|
import type { School, SchoolResult, AbsenceData } from '@/lib/types';
|
|
import { formatPercentage, formatProgress, calculateTrend } from '@/lib/utils';
|
|
import styles from './SchoolDetailView.module.css';
|
|
|
|
interface SchoolDetailViewProps {
|
|
schoolInfo: School;
|
|
yearlyData: SchoolResult[];
|
|
absenceData: AbsenceData | null;
|
|
}
|
|
|
|
export function SchoolDetailView({ schoolInfo, yearlyData, absenceData }: SchoolDetailViewProps) {
|
|
const router = useRouter();
|
|
const { addSchool, removeSchool, isSelected } = useComparison();
|
|
const isInComparison = isSelected(schoolInfo.urn);
|
|
|
|
// Get latest results
|
|
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
|
|
|
|
// Handle add/remove from comparison
|
|
const handleComparisonToggle = () => {
|
|
if (isInComparison) {
|
|
removeSchool(schoolInfo.urn);
|
|
} else {
|
|
addSchool(schoolInfo);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
{/* Back Navigation */}
|
|
<div className={styles.backNav}>
|
|
<button onClick={() => router.back()} className={styles.backButton}>← Back</button>
|
|
</div>
|
|
|
|
{/* Header Section */}
|
|
<header className={styles.header}>
|
|
<div className={styles.headerContent}>
|
|
<div className={styles.titleSection}>
|
|
<h1 className={styles.schoolName}>{schoolInfo.school_name}</h1>
|
|
<div className={styles.meta}>
|
|
{schoolInfo.local_authority && (
|
|
<span className={styles.metaItem}>
|
|
{schoolInfo.local_authority}
|
|
</span>
|
|
)}
|
|
{schoolInfo.school_type && (
|
|
<span className={styles.metaItem}>
|
|
{schoolInfo.school_type}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{schoolInfo.address && (
|
|
<p className={styles.address}>
|
|
{schoolInfo.address}
|
|
{schoolInfo.postcode && `, ${schoolInfo.postcode}`}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className={styles.actions}>
|
|
<button
|
|
onClick={handleComparisonToggle}
|
|
className={isInComparison ? styles.btnRemove : styles.btnAdd}
|
|
>
|
|
{isInComparison ? '✓ In Comparison' : '+ Add to Compare'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Latest Results Summary */}
|
|
{latestResults && (
|
|
<section className={styles.summary}>
|
|
<h2 className={styles.sectionTitle}>
|
|
Latest Results ({latestResults.year})
|
|
</h2>
|
|
<div className={styles.metricsGrid}>
|
|
{latestResults.rwm_expected_pct !== null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
RWM Expected Standard
|
|
</div>
|
|
<div className={styles.metricValue}>
|
|
{formatPercentage(latestResults.rwm_expected_pct)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{latestResults.rwm_high_pct !== null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
RWM Higher Standard
|
|
</div>
|
|
<div className={styles.metricValue}>
|
|
{formatPercentage(latestResults.rwm_high_pct)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{latestResults.reading_progress !== null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
Reading Progress
|
|
</div>
|
|
<div className={styles.metricValue}>
|
|
{formatProgress(latestResults.reading_progress)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{latestResults.writing_progress !== null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
Writing Progress
|
|
</div>
|
|
<div className={styles.metricValue}>
|
|
{formatProgress(latestResults.writing_progress)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{latestResults.maths_progress !== null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
Maths Progress
|
|
</div>
|
|
<div className={styles.metricValue}>
|
|
{formatProgress(latestResults.maths_progress)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
{(latestResults.reading_progress !== null || latestResults.writing_progress !== null || latestResults.maths_progress !== null) && (
|
|
<p className={styles.progressNote}>Progress scores: 0 = national average. Positive = above average, negative = below average.</p>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
{/* Map */}
|
|
{schoolInfo.latitude && schoolInfo.longitude && (
|
|
<section className={styles.mapSection}>
|
|
<h2 className={styles.sectionTitle}>Location</h2>
|
|
<div className={styles.mapContainer}>
|
|
<SchoolMap
|
|
schools={[schoolInfo]}
|
|
center={[schoolInfo.latitude, schoolInfo.longitude]}
|
|
zoom={15}
|
|
/>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Performance Over Time */}
|
|
{yearlyData.length > 0 && (
|
|
<section className={styles.chartsSection}>
|
|
<h2 className={styles.sectionTitle}>Performance Over Time</h2>
|
|
<div className={styles.chartContainer}>
|
|
<PerformanceChart
|
|
data={yearlyData}
|
|
schoolName={schoolInfo.school_name}
|
|
/>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Detailed Metrics */}
|
|
{latestResults && (
|
|
<section className={styles.detailedMetrics}>
|
|
<h2 className={styles.sectionTitle}>Detailed Metrics</h2>
|
|
<div className={styles.metricGroupsGrid}>
|
|
{/* Reading Metrics */}
|
|
<div className={styles.metricGroup}>
|
|
<h3 className={styles.metricGroupTitle}>Reading</h3>
|
|
<div className={styles.metricTable}>
|
|
{latestResults.reading_expected_pct !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>Expected</span>
|
|
<span className={styles.metricValue}>{formatPercentage(latestResults.reading_expected_pct)}</span>
|
|
</div>
|
|
)}
|
|
{latestResults.reading_high_pct !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>Higher</span>
|
|
<span className={styles.metricValue}>{formatPercentage(latestResults.reading_high_pct)}</span>
|
|
</div>
|
|
)}
|
|
{latestResults.reading_progress !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>Progress</span>
|
|
<span className={styles.metricValue}>{formatProgress(latestResults.reading_progress)}</span>
|
|
</div>
|
|
)}
|
|
{latestResults.reading_avg_score !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>Avg Score</span>
|
|
<span className={styles.metricValue}>{latestResults.reading_avg_score.toFixed(1)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Writing Metrics */}
|
|
<div className={styles.metricGroup}>
|
|
<h3 className={styles.metricGroupTitle}>Writing</h3>
|
|
<div className={styles.metricTable}>
|
|
{latestResults.writing_expected_pct !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>Expected</span>
|
|
<span className={styles.metricValue}>{formatPercentage(latestResults.writing_expected_pct)}</span>
|
|
</div>
|
|
)}
|
|
{latestResults.writing_high_pct !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>Higher</span>
|
|
<span className={styles.metricValue}>{formatPercentage(latestResults.writing_high_pct)}</span>
|
|
</div>
|
|
)}
|
|
{latestResults.writing_progress !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>Progress</span>
|
|
<span className={styles.metricValue}>{formatProgress(latestResults.writing_progress)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Maths Metrics */}
|
|
<div className={styles.metricGroup}>
|
|
<h3 className={styles.metricGroupTitle}>Maths</h3>
|
|
<div className={styles.metricTable}>
|
|
{latestResults.maths_expected_pct !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>Expected</span>
|
|
<span className={styles.metricValue}>{formatPercentage(latestResults.maths_expected_pct)}</span>
|
|
</div>
|
|
)}
|
|
{latestResults.maths_high_pct !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>Higher</span>
|
|
<span className={styles.metricValue}>{formatPercentage(latestResults.maths_high_pct)}</span>
|
|
</div>
|
|
)}
|
|
{latestResults.maths_progress !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>Progress</span>
|
|
<span className={styles.metricValue}>{formatProgress(latestResults.maths_progress)}</span>
|
|
</div>
|
|
)}
|
|
{latestResults.maths_avg_score !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>Avg Score</span>
|
|
<span className={styles.metricValue}>{latestResults.maths_avg_score.toFixed(1)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Absence Data */}
|
|
{absenceData && (
|
|
<section className={styles.absenceSection}>
|
|
<h2 className={styles.sectionTitle}>Absence Data</h2>
|
|
<div className={styles.absenceGrid}>
|
|
{absenceData.overall_absence_rate !== null && (
|
|
<div className={styles.absenceCard}>
|
|
<div className={styles.absenceLabel}>Overall Absence Rate</div>
|
|
<div className={styles.absenceValue}>{formatPercentage(absenceData.overall_absence_rate)}</div>
|
|
</div>
|
|
)}
|
|
{absenceData.persistent_absence_rate !== null && (
|
|
<div className={styles.absenceCard}>
|
|
<div className={styles.absenceLabel}>Persistent Absence</div>
|
|
<div className={styles.absenceValue}>{formatPercentage(absenceData.persistent_absence_rate)}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* All Years Data Table */}
|
|
{yearlyData.length > 0 && (
|
|
<section className={styles.historySection}>
|
|
<h2 className={styles.sectionTitle}>Historical Data</h2>
|
|
<div className={styles.tableWrapper}>
|
|
<table className={styles.dataTable}>
|
|
<thead>
|
|
<tr>
|
|
<th>Year</th>
|
|
<th>RWM Expected</th>
|
|
<th>RWM Higher</th>
|
|
<th>Reading Progress</th>
|
|
<th>Writing Progress</th>
|
|
<th>Maths Progress</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{yearlyData.map((result) => (
|
|
<tr key={result.year}>
|
|
<td className={styles.yearCell}>{result.year}</td>
|
|
<td>{result.rwm_expected_pct !== null ? formatPercentage(result.rwm_expected_pct) : '-'}</td>
|
|
<td>{result.rwm_high_pct !== null ? formatPercentage(result.rwm_high_pct) : '-'}</td>
|
|
<td>{result.reading_progress !== null ? formatProgress(result.reading_progress) : '-'}</td>
|
|
<td>{result.writing_progress !== null ? formatProgress(result.writing_progress) : '-'}</td>
|
|
<td>{result.maths_progress !== null ? formatProgress(result.maths_progress) : '-'}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|