diff --git a/backend/app.py b/backend/app.py
index d012d8e..6c932c6 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -350,7 +350,7 @@ async def get_schools(
"page": page,
"page_size": page_size,
"total_pages": (total + page_size - 1) // page_size if page_size > 0 else 0,
- "search_location": {"postcode": postcode, "radius": radius}
+ "search_location": {"postcode": postcode, "radius": radius, "lat": search_coords[0], "lng": search_coords[1]}
if search_coords
else None,
}
diff --git a/frontend/app.js b/frontend/app.js
index d51b305..c7e3181 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -22,7 +22,9 @@ const state = {
active: false,
postcode: null,
radius: 5,
+ coords: null, // Lat/lng of search location
},
+ resultsView: "list", // "list" or "map"
loading: {
schools: false,
filters: false,
@@ -36,6 +38,7 @@ const state = {
let comparisonChart = null;
let schoolDetailChart = null;
let modalMap = null;
+let resultsMapInstance = null;
// Chart colors
const CHART_COLORS = [
@@ -189,6 +192,11 @@ const elements = {
radiusSelect: document.getElementById("radius-select"),
locationSearchBtn: document.getElementById("location-search-btn"),
typeFilterLocation: document.getElementById("type-filter-location"),
+ // Results view
+ viewToggle: document.getElementById("view-toggle"),
+ viewToggleBtns: document.querySelectorAll(".view-toggle-btn"),
+ resultsContainer: document.getElementById("results-container"),
+ resultsMap: document.getElementById("results-map"),
// Schools grid
schoolsGrid: document.getElementById("schools-grid"),
compareSearch: document.getElementById("compare-search"),
@@ -522,10 +530,26 @@ async function loadSchools() {
state.pagination.totalPages = data.total_pages;
state.isShowingFeatured = false;
+ // Store search coordinates if available
+ if (data.search_location && data.search_location.lat && data.search_location.lng) {
+ state.locationSearch.coords = {
+ lat: data.search_location.lat,
+ lng: data.search_location.lng,
+ };
+ }
+
// Show location info banner if location search is active
updateLocationInfoBanner(data.search_location);
renderSchools(state.schools);
+
+ // Update view toggle visibility
+ updateViewToggle();
+
+ // If map view is active, reinitialize the map with new results
+ if (state.resultsView === "map" && state.locationSearch.active) {
+ initializeResultsMap(state.schools);
+ }
}
async function loadFeaturedSchools() {
@@ -548,6 +572,9 @@ async function loadFeaturedSchools() {
state.isShowingFeatured = true;
renderFeaturedSchools(state.schools);
+
+ // Hide view toggle for featured schools
+ updateViewToggle();
}
function updateLocationInfoBanner(searchLocation) {
@@ -908,6 +935,168 @@ function openMapModal(lat, lng, schoolName) {
document.addEventListener("keydown", escHandler);
}
+/**
+ * Initialize the results map for location search
+ */
+function initializeResultsMap(schools) {
+ // Check if Leaflet is loaded
+ if (typeof L === "undefined") {
+ console.warn("Leaflet not loaded, skipping results map initialization");
+ return;
+ }
+
+ // Destroy existing map if any
+ if (resultsMapInstance) {
+ try {
+ resultsMapInstance.remove();
+ } catch (e) {
+ // Ignore cleanup errors
+ }
+ resultsMapInstance = null;
+ }
+
+ // Need search coords to center the map
+ if (!state.locationSearch.coords) {
+ console.warn("No search coordinates available for results map");
+ return;
+ }
+
+ const { lat, lng } = state.locationSearch.coords;
+
+ // Ensure container has dimensions
+ setTimeout(() => {
+ const mapContainer = elements.resultsMap;
+ if (!mapContainer || mapContainer.offsetWidth === 0 || mapContainer.offsetHeight === 0) {
+ console.warn("Results map container has no dimensions");
+ return;
+ }
+
+ // Create map centered on search location
+ resultsMapInstance = L.map(mapContainer, {
+ center: [lat, lng],
+ zoom: 14,
+ scrollWheelZoom: true,
+ });
+
+ // Add tile layer
+ L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
+ maxZoom: 19,
+ attribution: '© OpenStreetMap',
+ }).addTo(resultsMapInstance);
+
+ // Create custom icon for search location (blue)
+ const searchIcon = L.divIcon({
+ className: "search-location-marker",
+ html: ``,
+ iconSize: [32, 32],
+ iconAnchor: [16, 16],
+ });
+
+ // Add search location marker
+ L.marker([lat, lng], { icon: searchIcon })
+ .addTo(resultsMapInstance)
+ .bindPopup(`Search location
${state.locationSearch.postcode}`);
+
+ // Add school markers
+ const bounds = L.latLngBounds([[lat, lng]]);
+ schools.forEach((school) => {
+ if (school.latitude && school.longitude) {
+ const marker = L.marker([school.latitude, school.longitude])
+ .addTo(resultsMapInstance)
+ .bindPopup(`
+ ${escapeHtml(school.school_name)}
+ ${school.distance !== undefined ? school.distance.toFixed(1) + " miles away" : ""}
+ `);
+
+ // Click handler to highlight card
+ marker.on("click", () => {
+ highlightSchoolCard(school.urn);
+ });
+
+ bounds.extend([school.latitude, school.longitude]);
+ }
+ });
+
+ // Fit bounds to show all markers with padding
+ if (schools.length > 0) {
+ resultsMapInstance.fitBounds(bounds, { padding: [30, 30] });
+ }
+ }, 100);
+}
+
+/**
+ * Highlight a school card and scroll it into view
+ */
+function highlightSchoolCard(urn) {
+ // Remove highlight from all cards
+ document.querySelectorAll(".school-card").forEach((card) => {
+ card.classList.remove("highlighted");
+ });
+
+ // Add highlight to selected card
+ const card = document.querySelector(`.school-card[data-urn="${urn}"]`);
+ if (card) {
+ card.classList.add("highlighted");
+ card.scrollIntoView({ behavior: "smooth", block: "center" });
+ }
+}
+
+/**
+ * Destroy the results map instance
+ */
+function destroyResultsMap() {
+ if (resultsMapInstance) {
+ try {
+ resultsMapInstance.remove();
+ } catch (e) {
+ // Ignore cleanup errors
+ }
+ resultsMapInstance = null;
+ }
+}
+
+/**
+ * Update the view toggle visibility and state
+ */
+function updateViewToggle() {
+ // Only show toggle for location search results
+ if (state.locationSearch.active && state.schools.length > 0) {
+ elements.viewToggle.style.display = "flex";
+ } else {
+ elements.viewToggle.style.display = "none";
+ // Reset to list view when hiding toggle
+ if (state.resultsView === "map") {
+ setResultsView("list");
+ }
+ }
+}
+
+/**
+ * Set the results view mode (list or map)
+ */
+function setResultsView(view) {
+ state.resultsView = view;
+
+ // Update toggle button states
+ elements.viewToggleBtns.forEach((btn) => {
+ btn.classList.toggle("active", btn.dataset.view === view);
+ });
+
+ // Update container class
+ if (view === "map") {
+ elements.resultsContainer.classList.add("map-view");
+ // Initialize map if location search is active
+ if (state.locationSearch.active) {
+ initializeResultsMap(state.schools);
+ }
+ } else {
+ elements.resultsContainer.classList.remove("map-view");
+ destroyResultsMap();
+ }
+}
+
// =============================================================================
// RENDER FUNCTIONS
// =============================================================================
@@ -1709,11 +1898,14 @@ function setupEventListeners() {
// Clear the inactive mode's state
if (mode === "name") {
// Clear location search state
- state.locationSearch = { active: false, postcode: null, radius: 5 };
+ state.locationSearch = { active: false, postcode: null, radius: 5, coords: null };
elements.postcodeSearch.value = "";
elements.radiusSelect.value = "5";
elements.typeFilterLocation.value = "";
updateLocationInfoBanner(null);
+ // Reset to list view and hide toggle
+ setResultsView("list");
+ updateViewToggle();
} else {
// Clear name search state
elements.schoolSearch.value = "";
@@ -1726,6 +1918,16 @@ function setupEventListeners() {
});
});
+ // View toggle (list/map)
+ elements.viewToggleBtns.forEach((btn) => {
+ btn.addEventListener("click", () => {
+ const view = btn.dataset.view;
+ if (view !== state.resultsView) {
+ setResultsView(view);
+ }
+ });
+ });
+
// Name search and filters
let searchTimeout;
elements.schoolSearch.addEventListener("input", () => {
diff --git a/frontend/index.html b/frontend/index.html
index fccf816..8992dcf 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -154,8 +154,32 @@
-