Files
school_compare/nextjs-app/components/SchoolCard.tsx
Tudor 5eff9af69c
Some checks failed
Build and Push Docker Images / Build Frontend (Next.js) (push) Has been cancelled
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Has been cancelled
Build and Push Docker Images / Trigger Portainer Update (push) Has been cancelled
Build and Push Docker Images / Build Backend (FastAPI) (push) Has been cancelled
feat: add secondary school support with KS4 data and metric tooltips
- Backend: replace INNER JOIN ks2 with UNION ALL (ks2 + ks4) so primary
  and secondary schools both appear in the main DataFrame
- Backend: add /api/national-averages endpoint computing means from live
  data, replacing the hardcoded NATIONAL_AVG constant on the frontend
- Backend: add phase filter param to /api/schools; return phases from
  /api/filters; fix hardcoded "phase": "Primary" in school detail endpoint
- Backend: add KS4 metric definitions (Attainment 8, Progress 8, EBacc,
  English & Maths pass rates) to METRIC_DEFINITIONS and RANKING_COLUMNS
- Frontend: SchoolDetailView is now phase-aware — secondary schools show
  a GCSE Results section (Att8, P8, E&M, EBacc) instead of SATs; phonics
  tab hidden for secondary; admissions says Year 7 instead of Year 3;
  history table shows KS4 columns; chart datasets switch for secondary
- Frontend: new MetricTooltip component (CSS-only ⓘ icon) backed by
  METRIC_EXPLANATIONS — added to RWM, GPS, SEN, EAL, IDACI, progress
  scores and all KS4 metrics throughout SchoolDetailView and SchoolCard
- Frontend: METRIC_EXPLANATIONS extended with KS4 terms (Attainment 8,
  Progress 8, EBacc) and previously missing terms (SEN, EHCP, EAL, IDACI)
- Frontend: SchoolCard expands "RWM" to "Reading, Writing & Maths" and
  shows Attainment 8 / English & Maths Grade 4+ for secondary schools
- Frontend: FilterBar adds Phase dropdown (Primary / Secondary / All-through)
- Frontend: HomeView hero copy updated; compact list shows phase-aware metric
- Global metadata updated to remove "primary only" framing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 14:59:40 +00:00

164 lines
6.3 KiB
TypeScript

/**
* SchoolCard Component
* Displays school information with metrics and actions
*/
import Link from 'next/link';
import type { School } from '@/lib/types';
import { formatPercentage, formatProgress, calculateTrend, getTrendColor } from '@/lib/utils';
import styles from './SchoolCard.module.css';
interface SchoolCardProps {
school: School;
onAddToCompare?: (school: School) => void;
onRemoveFromCompare?: (urn: number) => void;
showDistance?: boolean;
distance?: number;
isInCompare?: boolean;
}
export function SchoolCard({ school, onAddToCompare, onRemoveFromCompare, showDistance, distance, isInCompare = false }: SchoolCardProps) {
const trend = calculateTrend(school.rwm_expected_pct, school.prev_rwm_expected_pct);
const trendColor = getTrendColor(trend);
return (
<div className={`${styles.card} ${isInCompare ? styles.cardInCompare : ''}`}>
<div className={styles.header}>
<h3 className={styles.title}>
<Link href={`/school/${school.urn}`}>
{school.school_name}
</Link>
</h3>
{showDistance && distance !== undefined && (
<span className={styles.distance}>
{(distance / 1.60934).toFixed(1)} miles away
</span>
)}
</div>
<div className={styles.meta}>
{school.local_authority && (
<span className={styles.metaItem}>{school.local_authority}</span>
)}
{school.school_type && (
<span className={styles.metaItem}>{school.school_type}</span>
)}
{school.religious_denomination && school.religious_denomination !== 'Does not apply' && (
<span className={styles.metaItem}>{school.religious_denomination}</span>
)}
</div>
{(school.rwm_expected_pct != null || school.attainment_8_score != null || school.reading_progress !== null) && (
<div className={styles.metrics}>
{/* KS4 card metrics for secondary schools */}
{school.attainment_8_score != null && (
<div className={styles.metric}>
<span className={styles.metricLabel}>
Attainment 8
<span className={styles.metricHint}>avg grade across best 8 GCSEs</span>
</span>
<div className={styles.metricValue}>
<strong>{school.attainment_8_score.toFixed(1)}</strong>
</div>
</div>
)}
{school.english_maths_standard_pass_pct != null && (
<div className={styles.metric}>
<span className={styles.metricLabel}>
English &amp; Maths Grade 4+
<span className={styles.metricHint}>% standard pass in both</span>
</span>
<div className={styles.metricValue}>
<strong>{formatPercentage(school.english_maths_standard_pass_pct)}</strong>
</div>
</div>
)}
{school.rwm_expected_pct !== null && (
<div className={styles.metric}>
<span className={styles.metricLabel}>
Reading, Writing &amp; Maths
<span className={styles.metricHint}>% meeting expected standard</span>
</span>
<div className={styles.metricValue}>
<strong>{formatPercentage(school.rwm_expected_pct)}</strong>
{school.prev_rwm_expected_pct !== null && (
<span
className={`${styles.trend} ${styles[`trend${trend.charAt(0).toUpperCase() + trend.slice(1)}`]}`}
title={`Previous year: ${formatPercentage(school.prev_rwm_expected_pct)}`}
>
{trend === 'up' && (
<svg viewBox="0 0 16 16" fill="none" className={styles.trendIcon} aria-label="Trend up">
<path
d="M8 3L14 10H2L8 3Z"
fill="currentColor"
/>
</svg>
)}
{trend === 'down' && (
<svg viewBox="0 0 16 16" fill="none" className={styles.trendIcon} aria-label="Trend down">
<path
d="M8 13L2 6H14L8 13Z"
fill="currentColor"
/>
</svg>
)}
{trend === 'stable' && (
<svg viewBox="0 0 16 16" fill="none" className={styles.trendIcon} aria-label="Trend stable">
<rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor" />
</svg>
)}
</span>
)}
</div>
</div>
)}
{school.reading_progress !== null && (
<div className={styles.metric}>
<span className={styles.metricLabel}>
Reading
<span className={styles.metricHint}>progress score (0 = avg)</span>
</span>
<strong>{formatProgress(school.reading_progress)}</strong>
</div>
)}
{school.writing_progress !== null && (
<div className={styles.metric}>
<span className={styles.metricLabel}>
Writing
<span className={styles.metricHint}>progress score (0 = avg)</span>
</span>
<strong>{formatProgress(school.writing_progress)}</strong>
</div>
)}
{school.maths_progress !== null && (
<div className={styles.metric}>
<span className={styles.metricLabel}>
Maths
<span className={styles.metricHint}>progress score (0 = avg)</span>
</span>
<strong>{formatProgress(school.maths_progress)}</strong>
</div>
)}
</div>
)}
<div className={styles.actions}>
<Link href={`/school/${school.urn}`} className="btn btn-primary">
View Details
</Link>
{onAddToCompare && (
<button
onClick={() => isInCompare ? onRemoveFromCompare?.(school.urn) : onAddToCompare(school)}
className={isInCompare ? 'btn btn-active' : 'btn btn-secondary'}
>
{isInCompare ? '✓ Comparing' : '+ Compare'}
</button>
)}
</div>
</div>
);
}