Complete Next.js migration with SSR and Docker deployment
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

- 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>
This commit is contained in:
Tudor
2026-02-02 20:34:35 +00:00
parent f4919db3b9
commit ff7f5487e6
72 changed files with 18636 additions and 20 deletions

View File

@@ -0,0 +1,321 @@
/**
* 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>
);
}