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>
This commit is contained in:
321
nextjs-app/components/SchoolDetailView.tsx
Normal file
321
nextjs-app/components/SchoolDetailView.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user