diff --git a/frontend/app.js b/frontend/app.js index c7e3181..d6c2536 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -39,6 +39,7 @@ let comparisonChart = null; let schoolDetailChart = null; let modalMap = null; let resultsMapInstance = null; +let resultsMapMarkers = new Map(); // Store markers by school URN // Chart colors const CHART_COLORS = [ @@ -541,15 +542,16 @@ async function loadSchools() { // Show location info banner if location search is active updateLocationInfoBanner(data.search_location); - renderSchools(state.schools); + // Render appropriate view based on current state + if (state.resultsView === "map" && state.locationSearch.active) { + renderCompactSchoolList(state.schools); + initializeResultsMap(state.schools); + } else { + 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() { @@ -999,6 +1001,9 @@ function initializeResultsMap(schools) { .addTo(resultsMapInstance) .bindPopup(`Search location
${state.locationSearch.postcode}`); + // Clear existing markers + resultsMapMarkers.clear(); + // Add school markers const bounds = L.latLngBounds([[lat, lng]]); schools.forEach((school) => { @@ -1010,9 +1015,16 @@ function initializeResultsMap(schools) { ${school.distance !== undefined ? school.distance.toFixed(1) + " miles away" : ""} `); + // Store marker reference + resultsMapMarkers.set(school.urn, { + marker, + lat: school.latitude, + lng: school.longitude, + }); + // Click handler to highlight card marker.on("click", () => { - highlightSchoolCard(school.urn); + highlightSchoolCard(school.urn, false); // Don't center map, already at marker }); bounds.extend([school.latitude, school.longitude]); @@ -1028,19 +1040,28 @@ function initializeResultsMap(schools) { /** * Highlight a school card and scroll it into view + * @param {number} urn - School URN + * @param {boolean} centerMap - Whether to center the map on the school (default: true) */ -function highlightSchoolCard(urn) { - // Remove highlight from all cards - document.querySelectorAll(".school-card").forEach((card) => { +function highlightSchoolCard(urn, centerMap = true) { + // Remove highlight from all cards and compact items + document.querySelectorAll(".school-card, .school-list-item").forEach((card) => { card.classList.remove("highlighted"); }); - // Add highlight to selected card - const card = document.querySelector(`.school-card[data-urn="${urn}"]`); + // Add highlight to selected card/item + const card = document.querySelector(`.school-card[data-urn="${urn}"], .school-list-item[data-urn="${urn}"]`); if (card) { card.classList.add("highlighted"); card.scrollIntoView({ behavior: "smooth", block: "center" }); } + + // Center map on the school and open popup + if (centerMap && resultsMapInstance && resultsMapMarkers.has(urn)) { + const { marker, lat, lng } = resultsMapMarkers.get(urn); + resultsMapInstance.setView([lat, lng], 15, { animate: true }); + marker.openPopup(); + } } /** @@ -1055,6 +1076,7 @@ function destroyResultsMap() { } resultsMapInstance = null; } + resultsMapMarkers.clear(); } /** @@ -1084,9 +1106,13 @@ function setResultsView(view) { btn.classList.toggle("active", btn.dataset.view === view); }); - // Update container class + // Update container class and render appropriate view if (view === "map") { elements.resultsContainer.classList.add("map-view"); + // Render compact list for map view + if (state.schools.length > 0) { + renderCompactSchoolList(state.schools); + } // Initialize map if location search is active if (state.locationSearch.active) { initializeResultsMap(state.schools); @@ -1094,9 +1120,72 @@ function setResultsView(view) { } else { elements.resultsContainer.classList.remove("map-view"); destroyResultsMap(); + // Re-render full cards for list view + if (state.schools.length > 0 && state.locationSearch.active) { + renderSchools(state.schools); + } } } +/** + * Render compact school list items for map view + */ +function renderCompactSchoolList(schools) { + const html = schools + .map((school) => { + const distanceBadge = + school.distance !== undefined && school.distance !== null + ? `${school.distance.toFixed(1)} mi` + : ""; + + return ` +
+
+
+

${escapeHtml(school.school_name)}

+ ${distanceBadge} +
+
+ ${escapeHtml(school.school_type || "")} + ${escapeHtml(school.local_authority || "")} +
+
+ + ${formatMetricValue(school.rwm_expected_pct, "rwm_expected_pct")} RWM + + + ${school.total_pupils || "-"} pupils + +
+
+ +
+ `; + }) + .join(""); + + elements.schoolsGrid.innerHTML = html; + + // Add click handlers for list items (to highlight on map) + elements.schoolsGrid.querySelectorAll(".school-list-item").forEach((item) => { + item.addEventListener("click", (e) => { + // Don't trigger if clicking the details button + if (e.target.closest(".school-list-item-details")) return; + const urn = parseInt(item.dataset.urn, 10); + highlightSchoolCard(urn, true); + }); + }); + + // Add click handlers for details buttons + elements.schoolsGrid.querySelectorAll(".school-list-item-details").forEach((btn) => { + btn.addEventListener("click", (e) => { + e.stopPropagation(); + const urn = parseInt(btn.dataset.urn, 10); + openSchoolModal(urn); + }); + }); +} + // ============================================================================= // RENDER FUNCTIONS // ============================================================================= diff --git a/frontend/styles.css b/frontend/styles.css index 8aba774..5756d9e 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -492,11 +492,88 @@ body { } /* Highlighted card in map view */ -.school-card.highlighted { +.school-card.highlighted, +.school-list-item.highlighted { border-color: var(--accent-teal); box-shadow: 0 0 0 2px rgba(45, 125, 125, 0.2); } +/* Compact school list items for map view */ +.school-list-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.875rem 1rem; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + cursor: pointer; + transition: var(--transition); +} + +.school-list-item:hover { + border-color: var(--text-muted); +} + +.school-list-item-content { + flex: 1; + min-width: 0; +} + +.school-list-item-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.25rem; +} + +.school-list-item-name { + font-size: 0.95rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.school-list-item-header .distance-badge { + flex-shrink: 0; + font-size: 0.75rem; + padding: 0.15rem 0.5rem; +} + +.school-list-item-meta { + display: flex; + gap: 0.5rem; + font-size: 0.8rem; + color: var(--text-muted); + margin-bottom: 0.375rem; +} + +.school-list-item-meta span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.school-list-item-stats { + display: flex; + gap: 1rem; + font-size: 0.8rem; + color: var(--text-secondary); +} + +.school-list-item-stat strong { + color: var(--text-primary); +} + +.school-list-item-details { + flex-shrink: 0; + padding: 0.5rem 0.875rem; + font-size: 0.8rem; +} + /* Search location marker on map */ .search-location-marker { background: transparent;