Files
school_compare/nextjs-app/components/school/NearbySchoolsSection.tsx
T
TudorandClaude Opus 5.5 ccfa44389e
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m19s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m32s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m18s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 14s
fix(school): drop the nearby section's lede, which repeated its heading
"Other schools nearby" was followed by "Other primary schools near
<school>.", which says the same thing again. The heading now stands alone.

nearbyNoun() and the phase and schoolName props existed only to build that
line, so they go with it. Its bottom margin was the only gap between the
heading and the cards, so the header row carries that gap now, and centres
the heading against the carousel arrows now that it is a single line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:10:36 +01:00

121 lines
4.3 KiB
TypeScript

/**
* NearbySchoolsSection — the nearest eligible schools, closest first. Server
* component; only the carousel, the compare bar and the add-to-compare button
* are client-side.
*
* "Other schools nearby", not "similar" ones: the order is distance and only
* distance. The hard filters upstream still guarantee the set is comparable —
* same phase, same selectivity, mainstream never beside special — but nothing
* here ranks by how alike two schools are, so the heading does not say it does.
*
* The chips report what a school shares, and may be absent entirely. That is
* information for the reader to weigh, not a verdict this section has already
* reached on their behalf.
*
* There is deliberately no "how these are chosen" panel: the method is already
* visible in the chips and the distances. The single caption line is not a
* method note — it is the one thing a card cannot self-correct.
*
* Nor is there a lede: "Other primary schools near X" only restated the heading.
*/
import Link from 'next/link';
import type { NearbySchool } from '@/lib/types';
import { schoolUrl } from '@/lib/utils';
import { AddToCompareButton } from './AddToCompareButton';
import { NearbySchoolsCarousel } from './NearbySchoolsCarousel';
import { NearbySchoolsCompareBar } from './NearbySchoolsCompareBar';
import { Section } from './sectionShared';
import styles from './NearbySchools.module.css';
const MINIMUM = 2;
export function shouldRenderNearby(nearby?: NearbySchool[] | null): boolean {
return (nearby?.length ?? 0) >= MINIMUM;
}
function metricLabel(key: string): string {
return key === 'attainment_8_score' ? 'Attainment 8' : 'Reading, writing & maths';
}
function formatMetric(value: number | null, key: string): string {
if (value == null) return 'Not published';
return key === 'attainment_8_score' ? value.toFixed(1) : `${Math.round(value)}%`;
}
export function NearbySchoolsSection({
urn,
thisMetricValue,
nearby,
}: {
urn: number;
thisMetricValue: number | null;
nearby?: NearbySchool[] | null;
}) {
if (!shouldRenderNearby(nearby)) return null;
const schools = nearby as NearbySchool[];
const metricKey = schools[0].metric_key;
return (
<Section id="nearby">
<NearbySchoolsCarousel
count={schools.length}
labelledBy="nearby-schools-heading"
header={
<h2 id="nearby-schools-heading" className={styles.heading}>
Other schools nearby
</h2>
}
>
{schools.map((school) => (
<li key={school.urn} className={styles.school}>
<p className={styles.distance}>{school.distance_miles} miles away</p>
<h3 className={styles.name}>
<Link href={schoolUrl(school.urn, school.school_name)}>
{school.school_name}
</Link>
</h3>
<p className={styles.meta}>
{[school.school_type, school.age_range && `Ages ${school.age_range}`]
.filter(Boolean)
.join(' · ')}
</p>
{school.shared.length > 0 && (
<ul className={styles.shared}>
{school.shared.map((label) => (
<li key={label} className={styles.chip}>{label}</li>
))}
</ul>
)}
<div className={styles.metric}>
<p
className={
school.metric_value == null ? styles.valueAbsent : styles.value
}
>
{formatMetric(school.metric_value, school.metric_key)}
</p>
<p className={styles.metricLabel}>{metricLabel(school.metric_key)}</p>
{thisMetricValue != null && (
<p className={styles.metricRef}>
{formatMetric(thisMetricValue, metricKey)} at this school
</p>
)}
</div>
<AddToCompareButton school={school} />
</li>
))}
</NearbySchoolsCarousel>
<NearbySchoolsCompareBar thisUrn={urn} candidates={schools} />
{/* The one caveat the cards cannot make on their own: a reader who takes
"0.6 miles away" for the walk has been misled, and nothing else here
corrects that. */}
<p className={styles.caption}>
Distances are straight-line from this school, not road distance.
</p>
</Section>
);
}