2026-08-21 18:15:10 +01:00
|
|
|
/**
|
|
|
|
|
* Client for the places API.
|
|
|
|
|
*
|
|
|
|
|
* Two namespaces, matching the backend: towns and localities share
|
|
|
|
|
* /schools/[place]; authorities take /schools/authority/[la]. 67 town names
|
|
|
|
|
* collide with an authority name and neither set contains the other, so one
|
|
|
|
|
* namespace would publish near-duplicate pages.
|
|
|
|
|
*/
|
|
|
|
|
import type { School } from '@/lib/types';
|
|
|
|
|
|
|
|
|
|
export interface PlaceSummary {
|
|
|
|
|
kind: string;
|
|
|
|
|
slug: string;
|
|
|
|
|
name: string;
|
|
|
|
|
count: number;
|
2026-08-21 20:43:07 +01:00
|
|
|
/** Phases that clear the threshold on their own, so the page links
|
|
|
|
|
* variants that exist rather than 404s. Absent on the registry listing. */
|
|
|
|
|
phases?: string[];
|
2026-08-21 18:15:10 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-21 22:19:23 +01:00
|
|
|
export interface PlaceAuthority {
|
|
|
|
|
name: string;
|
2026-08-22 17:22:11 +01:00
|
|
|
/** null when that authority has no page of its own — two English
|
|
|
|
|
* authorities hold fewer schools than the threshold. */
|
|
|
|
|
slug: string | null;
|
2026-08-21 22:19:23 +01:00
|
|
|
count: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-21 18:15:10 +01:00
|
|
|
export interface PlaceDetail {
|
2026-08-21 22:19:23 +01:00
|
|
|
place: PlaceSummary & {
|
|
|
|
|
parent_authority: string | null;
|
|
|
|
|
/** Every authority the place meaningfully sits in, largest first. SW19 is
|
|
|
|
|
* mostly Merton but partly Wandsworth. */
|
|
|
|
|
authorities?: PlaceAuthority[];
|
|
|
|
|
};
|
2026-08-21 18:15:10 +01:00
|
|
|
schools: School[];
|
|
|
|
|
averages: {
|
|
|
|
|
rwm_expected_pct: number | null;
|
|
|
|
|
attainment_8_score: number | null;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function placeUrl(kind: string, slug: string, phase?: string): string {
|
|
|
|
|
const base =
|
|
|
|
|
kind === 'authority' ? `/schools/authority/${slug}`
|
|
|
|
|
: kind === 'outcode' ? `/schools/near/${slug}`
|
|
|
|
|
: `/schools/${slug}`;
|
|
|
|
|
return phase ? `${base}/${phase}` : base;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 17:22:11 +01:00
|
|
|
/**
|
|
|
|
|
* An authority name as it appears in a URL.
|
|
|
|
|
*
|
|
|
|
|
* Only a fallback: the API sends the slug it built, and that is what should
|
|
|
|
|
* be used. This mirrors `_slugify` in backend/app.py, collapsed runs and
|
|
|
|
|
* trimmed hyphens included, so the two cannot disagree about a name like
|
|
|
|
|
* "Bristol, City of".
|
|
|
|
|
*/
|
2026-08-21 18:15:10 +01:00
|
|
|
export function authoritySlug(name: string): string {
|
2026-08-22 17:22:11 +01:00
|
|
|
return name.toLowerCase().trim()
|
|
|
|
|
.replace(/[^\w\s-]/g, '')
|
|
|
|
|
.replace(/\s+/g, '-')
|
|
|
|
|
.replace(/-+/g, '-')
|
|
|
|
|
.replace(/^-|-$/g, '');
|
2026-08-21 18:15:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const API = process.env.FASTAPI_URL || process.env.NEXT_PUBLIC_API_URL
|
|
|
|
|
|| 'http://localhost:8000/api';
|
|
|
|
|
|
|
|
|
|
export async function fetchPlaces(): Promise<PlaceSummary[]> {
|
|
|
|
|
const res = await fetch(`${API}/places`, { next: { revalidate: 604800 } });
|
|
|
|
|
if (!res.ok) return [];
|
|
|
|
|
return (await res.json()).places ?? [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchPlace(
|
|
|
|
|
kind: string, slug: string, phase?: string,
|
|
|
|
|
): Promise<PlaceDetail | null> {
|
|
|
|
|
const q = phase ? `?phase=${encodeURIComponent(phase)}` : '';
|
|
|
|
|
const res = await fetch(`${API}/places/${kind}/${slug}${q}`,
|
|
|
|
|
{ next: { revalidate: 604800 } });
|
|
|
|
|
if (!res.ok) return null;
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|