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
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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 : ''}`}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user