'use client';
/**
* CutoffMapPanel — "Where the last place went".
*
* The rings and the postcode check live in one component because they are one
* question asked twice: the form answers "how far are we?" and the map shows
* that answer against the cut-offs. Entering a postcode drops a pin on the same
* rings rather than producing a separate verdict elsewhere on the page.
*
* The map is not rendered until asked for. Before a postcode is entered it is a
* circle drawn round a school, and it costs a Leaflet bundle and 240px of
* section height to say that; a successful check opens it automatically,
* because that is the point at which it starts answering something.
*
* The postcode never leaves the browser except to postcodes.io for a lat/long,
* and nothing is stored — this is a client-side measurement, not a lookup
* against the family.
*/
import { useState, useMemo, type FormEvent } from 'react';
import dynamic from 'next/dynamic';
import type { School } from '@/lib/types';
import { geocodePostcode, calculateDistance } from '@/lib/api';
import { isValidPostcode } from '@/lib/utils';
import {
compareToCutoffs,
type CutoffYearRow, type CutoffCheckResult, type CutoffVerdict,
} from './lastDistanceOffered';
import type { CutoffRing } from '../LeafletCutoffMapInner';
import styles from './schoolSections.module.css';
const CutoffMap = dynamic(() => import('../LeafletCutoffMapInner'), {
ssr: false,
loading: () =>
,
});
const VERDICT_MARK: Record = {
inside: '✓',
outside: '✕',
'too-close': '≈',
unknown: '–',
};
const VERDICT_TITLE: Record = {
inside: 'Inside that year’s cut-off',
outside: 'Outside that year’s cut-off',
'too-close': 'Too close to the cut-off to say',
unknown: 'No published figure for this year',
};
// Written out rather than composed from the verdict string. A computed
// `styles[...]` key silently yields undefined when a class is renamed, and an
// unstyled "outside" chip would look exactly like an "inside" one.
const VERDICT_CLASS: Record = {
inside: styles.cutoffYearInside,
outside: styles.cutoffYearOutside,
'too-close': styles.cutoffYearTooClose,
unknown: styles.cutoffYearUnknown,
};
export function CutoffMapPanel({
schoolInfo,
rows,
}: {
schoolInfo: School;
rows: CutoffYearRow[];
}) {
const [postcode, setPostcode] = useState('');
const [home, setHome] = useState<{ lat: number; lng: number } | null>(null);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const [busy, setBusy] = useState(false);
/* The map is not rendered until it is asked for. It costs a Leaflet bundle
and 240px of section height, and before a postcode is entered it is a
circle drawn round a school — it only starts answering a question once
there is a home to put beside the rings. A successful check therefore
opens it automatically. */
const [mapOpen, setMapOpen] = useState(false);
const lat = schoolInfo.latitude;
const lng = schoolInfo.longitude;
const rings: CutoffRing[] = useMemo(() => {
const published = rows.filter((r) => r.distanceM != null);
return published.map((r, i) => ({
year: r.year,
radiusM: r.distanceM as number,
// rows are newest-first, so the first published row is the current year.
latest: i === 0,
}));
}, [rows]);
// Without coordinates there is nothing to draw rings around and nothing to
// measure a postcode against.
if (lat == null || lng == null || rings.length === 0) return null;
const onCheck = async (e: FormEvent) => {
e.preventDefault();
const value = postcode.trim();
if (!value) return;
if (!isValidPostcode(value)) {
setError('That does not look like a UK postcode. Try one like SE23 3NA.');
setResult(null);
setHome(null);
return;
}
setBusy(true);
setError(null);
try {
const point = await geocodePostcode(value);
if (!point) {
setError('We could not find that postcode. Check it and try again.');
setResult(null);
setHome(null);
return;
}
setHome({ lat: point.latitude, lng: point.longitude });
setMapOpen(true);
// calculateDistance returns kilometres; everything here is metres.
const metres = calculateDistance(point.latitude, point.longitude, lat, lng) * 1000;
setResult(compareToCutoffs(metres, rows));
} catch {
setError('Something went wrong looking up that postcode. Try again in a moment.');
setResult(null);
setHome(null);
} finally {
setBusy(false);
}
};
const latestRing = rings[0];
return (
{/* No "Where the last place went" heading above this any more. It
introduced a map that now sits behind a button, so it named nothing,
and it echoed the section title directly above it. */}
How far away are you?
Straight-line distance from your postcode, compared with each year’s
cut-off. Not stored.