Files
school_compare/nextjs-app/components/SchoolDetailView.tsx
Tudor ff7f5487e6
Some checks failed
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 1m26s
Build and Push Docker Images / Build Frontend (Next.js) (push) Failing after 1m48s
Build and Push Docker Images / Trigger Portainer Update (push) Has been skipped
Complete Next.js migration with SSR and Docker deployment
- Migrate from vanilla JavaScript SPA to Next.js 16 with App Router
- Add server-side rendering for all pages (Home, Compare, Rankings)
- Create individual school pages with dynamic routing (/school/[urn])
- Implement Chart.js and Leaflet map integrations
- Add comprehensive SEO with sitemap, robots.txt, and JSON-LD
- Set up Docker multi-service architecture (PostgreSQL, FastAPI, Next.js)
- Update CI/CD pipeline to build both backend and frontend images
- Fix Dockerfile to include devDependencies for TypeScript compilation
- Add Jest testing configuration
- Implement performance optimizations (code splitting, caching)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-02 20:34:35 +00:00

322 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SchoolDetailView Component
* Displays comprehensive school information with performance charts
*/
'use client';
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 { 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}>
{/* 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>
)}
<span className={styles.metaItem}>
🔢 URN: {schoolInfo.urn}
</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>
</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>
{/* 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 Standard</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 Standard</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 Score</span>
<span className={styles.metricValue}>{formatProgress(latestResults.reading_progress)}</span>
</div>
)}
{latestResults.reading_avg_score !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Average Scaled 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 Standard</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 Standard</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 Score</span>
<span className={styles.metricValue}>{formatProgress(latestResults.writing_progress)}</span>
</div>
)}
</div>
</div>
{/* Maths Metrics */}
<div className={styles.metricGroup}>
<h3 className={styles.metricGroupTitle}>🔢 Mathematics</h3>
<div className={styles.metricTable}>
{latestResults.maths_expected_pct !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Expected Standard</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 Standard</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 Score</span>
<span className={styles.metricValue}>{formatProgress(latestResults.maths_progress)}</span>
</div>
)}
{latestResults.maths_avg_score !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Average Scaled Score</span>
<span className={styles.metricValue}>{latestResults.maths_avg_score.toFixed(1)}</span>
</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>
)}
{/* 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>
)}
{/* 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>
<th>Avg. Scaled Score</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>
<td>-</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)}
</div>
);
}