feat(map): fetch all schools for map view, add reference pin, cap radius at 5mi
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 45s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m6s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 31s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s

- Remove 10-mile radius option; cap backend radius max at 5 miles
- Raise backend page_size max to 500 so map can fetch all schools in one call
- HomeView: when map view is active, fetch all schools within radius
  (page_size=500) instead of showing only the paginated first page;
  falls back to initial SSR schools while loading
- SchoolMap/LeafletMapInner: accept referencePoint prop and render a
  distinctive coral circle pin at the search postcode location

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-30 09:13:14 +01:00
parent daf24e4739
commit d6a45b8e12
5 changed files with 54 additions and 10 deletions

View File

@@ -145,7 +145,6 @@ export function FilterBar({ filters, isHero, resultFilters }: FilterBarProps) {
<option value="1">1 mile</option>
<option value="3">3 miles</option>
<option value="5">5 miles</option>
<option value="10">10 miles</option>
</select>
</div>
)}

View File

@@ -35,6 +35,8 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
const [hasMore, setHasMore] = useState(initialSchools.total_pages > 1);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [laAverages, setLaAverages] = useState<Record<string, number>>({});
const [mapSchools, setMapSchools] = useState<School[]>([]);
const [isLoadingMap, setIsLoadingMap] = useState(false);
const prevSearchParamsRef = useRef(searchParams.toString());
const hasSearch = searchParams.get('search') || searchParams.get('postcode');
@@ -52,6 +54,7 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
setAllSchools(initialSchools.schools);
setCurrentPage(initialSchools.page);
setHasMore(initialSchools.total_pages > 1);
setMapSchools([]);
}
}, [searchParams, initialSchools]);
@@ -60,6 +63,20 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
setSelectedMapSchool(null);
}, [resultsView, searchParams]);
// Fetch all schools within radius when map view is active
useEffect(() => {
if (resultsView !== 'map' || !isLocationSearch) return;
setIsLoadingMap(true);
const params: Record<string, any> = {};
searchParams.forEach((value, key) => { params[key] = value; });
params.page = 1;
params.page_size = 500;
fetchSchools(params, { cache: 'no-store' })
.then(r => setMapSchools(r.schools))
.catch(() => setMapSchools(initialSchools.schools))
.finally(() => setIsLoadingMap(false));
}, [resultsView, searchParams]);
// Fetch LA averages when secondary schools are visible
useEffect(() => {
if (!isSecondaryView) return;
@@ -214,13 +231,14 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
<div className={styles.mapViewContainer}>
<div className={styles.mapContainer}>
<SchoolMap
schools={initialSchools.schools}
schools={isLoadingMap ? initialSchools.schools : mapSchools}
center={initialSchools.location_info?.coordinates}
referencePoint={initialSchools.location_info?.coordinates}
onMarkerClick={setSelectedMapSchool}
/>
</div>
<div className={styles.compactList}>
{initialSchools.schools.map((school) => (
{(isLoadingMap ? initialSchools.schools : mapSchools).map((school) => (
<div
key={school.urn}
className={`${styles.listItemWrapper} ${selectedMapSchool?.urn === school.urn ? styles.highlightedItem : ''}`}

View File

@@ -23,12 +23,14 @@ interface LeafletMapInnerProps {
schools: School[];
center: [number, number];
zoom: number;
referencePoint?: [number, number];
onMarkerClick?: (school: School) => void;
}
export default function LeafletMapInner({ schools, center, zoom, onMarkerClick }: LeafletMapInnerProps) {
export default function LeafletMapInner({ schools, center, zoom, referencePoint, onMarkerClick }: LeafletMapInnerProps) {
const mapRef = useRef<L.Map | null>(null);
const mapContainerRef = useRef<HTMLDivElement>(null);
const refMarkerRef = useRef<L.Marker | null>(null);
useEffect(() => {
if (!mapContainerRef.current) return;
@@ -43,13 +45,36 @@ export default function LeafletMapInner({ schools, center, zoom, onMarkerClick }
}).addTo(mapRef.current);
}
// Clear existing markers
// Clear existing school markers (not the reference pin)
mapRef.current.eachLayer((layer) => {
if (layer instanceof L.Marker) {
if (layer instanceof L.Marker && layer !== refMarkerRef.current) {
mapRef.current!.removeLayer(layer);
}
});
// Add reference pin (search location)
if (refMarkerRef.current) {
refMarkerRef.current.remove();
refMarkerRef.current = null;
}
if (referencePoint && mapRef.current) {
const refIcon = L.divIcon({
html: `<div style="
width: 20px; height: 20px;
background: #e07256;
border: 3px solid white;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0,0,0,0.35);
"></div>`,
iconSize: [20, 20],
iconAnchor: [10, 10],
className: '',
});
refMarkerRef.current = L.marker(referencePoint, { icon: refIcon, zIndexOffset: 1000 })
.addTo(mapRef.current)
.bindPopup('<strong>Search location</strong>');
}
// Add markers for schools
schools.forEach((school) => {
if (school.latitude && school.longitude && mapRef.current) {
@@ -89,7 +114,7 @@ export default function LeafletMapInner({ schools, center, zoom, onMarkerClick }
return () => {
// Don't destroy map on every update, just clean markers
};
}, [schools, center, zoom, onMarkerClick]);
}, [schools, center, zoom, referencePoint, onMarkerClick]);
// Cleanup map on unmount
useEffect(() => {

View File

@@ -24,10 +24,11 @@ interface SchoolMapProps {
schools: School[];
center?: [number, number];
zoom?: number;
referencePoint?: [number, number];
onMarkerClick?: (school: School) => void;
}
export function SchoolMap({ schools, center, zoom = 13, onMarkerClick }: SchoolMapProps) {
export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarkerClick }: SchoolMapProps) {
// Calculate center if not provided
const mapCenter: [number, number] = center || (() => {
if (schools.length === 0) return [51.5074, -0.1278]; // Default to London
@@ -50,6 +51,7 @@ export function SchoolMap({ schools, center, zoom = 13, onMarkerClick }: SchoolM
schools={schools}
center={mapCenter}
zoom={zoom}
referencePoint={referencePoint}
onMarkerClick={onMarkerClick}
/>
</div>