Improve map view with compact school list and interactions
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 57s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 57s
- Add compact school list items on right side of map view - Show school name, distance, type, authority, RWM %, and pupils - Click list item to center map and highlight marker - Click map marker to scroll and highlight list item - Add "Details" button to open school modal from list - Store markers by URN for map centering functionality Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
115
frontend/app.js
115
frontend/app.js
@@ -39,6 +39,7 @@ let comparisonChart = null;
|
|||||||
let schoolDetailChart = null;
|
let schoolDetailChart = null;
|
||||||
let modalMap = null;
|
let modalMap = null;
|
||||||
let resultsMapInstance = null;
|
let resultsMapInstance = null;
|
||||||
|
let resultsMapMarkers = new Map(); // Store markers by school URN
|
||||||
|
|
||||||
// Chart colors
|
// Chart colors
|
||||||
const CHART_COLORS = [
|
const CHART_COLORS = [
|
||||||
@@ -541,15 +542,16 @@ async function loadSchools() {
|
|||||||
// Show location info banner if location search is active
|
// Show location info banner if location search is active
|
||||||
updateLocationInfoBanner(data.search_location);
|
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
|
// Update view toggle visibility
|
||||||
updateViewToggle();
|
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() {
|
async function loadFeaturedSchools() {
|
||||||
@@ -999,6 +1001,9 @@ function initializeResultsMap(schools) {
|
|||||||
.addTo(resultsMapInstance)
|
.addTo(resultsMapInstance)
|
||||||
.bindPopup(`<strong>Search location</strong><br>${state.locationSearch.postcode}`);
|
.bindPopup(`<strong>Search location</strong><br>${state.locationSearch.postcode}`);
|
||||||
|
|
||||||
|
// Clear existing markers
|
||||||
|
resultsMapMarkers.clear();
|
||||||
|
|
||||||
// Add school markers
|
// Add school markers
|
||||||
const bounds = L.latLngBounds([[lat, lng]]);
|
const bounds = L.latLngBounds([[lat, lng]]);
|
||||||
schools.forEach((school) => {
|
schools.forEach((school) => {
|
||||||
@@ -1010,9 +1015,16 @@ function initializeResultsMap(schools) {
|
|||||||
${school.distance !== undefined ? school.distance.toFixed(1) + " miles away" : ""}
|
${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
|
// Click handler to highlight card
|
||||||
marker.on("click", () => {
|
marker.on("click", () => {
|
||||||
highlightSchoolCard(school.urn);
|
highlightSchoolCard(school.urn, false); // Don't center map, already at marker
|
||||||
});
|
});
|
||||||
|
|
||||||
bounds.extend([school.latitude, school.longitude]);
|
bounds.extend([school.latitude, school.longitude]);
|
||||||
@@ -1028,19 +1040,28 @@ function initializeResultsMap(schools) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Highlight a school card and scroll it into view
|
* 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) {
|
function highlightSchoolCard(urn, centerMap = true) {
|
||||||
// Remove highlight from all cards
|
// Remove highlight from all cards and compact items
|
||||||
document.querySelectorAll(".school-card").forEach((card) => {
|
document.querySelectorAll(".school-card, .school-list-item").forEach((card) => {
|
||||||
card.classList.remove("highlighted");
|
card.classList.remove("highlighted");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add highlight to selected card
|
// Add highlight to selected card/item
|
||||||
const card = document.querySelector(`.school-card[data-urn="${urn}"]`);
|
const card = document.querySelector(`.school-card[data-urn="${urn}"], .school-list-item[data-urn="${urn}"]`);
|
||||||
if (card) {
|
if (card) {
|
||||||
card.classList.add("highlighted");
|
card.classList.add("highlighted");
|
||||||
card.scrollIntoView({ behavior: "smooth", block: "center" });
|
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;
|
resultsMapInstance = null;
|
||||||
}
|
}
|
||||||
|
resultsMapMarkers.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1084,9 +1106,13 @@ function setResultsView(view) {
|
|||||||
btn.classList.toggle("active", btn.dataset.view === view);
|
btn.classList.toggle("active", btn.dataset.view === view);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update container class
|
// Update container class and render appropriate view
|
||||||
if (view === "map") {
|
if (view === "map") {
|
||||||
elements.resultsContainer.classList.add("map-view");
|
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
|
// Initialize map if location search is active
|
||||||
if (state.locationSearch.active) {
|
if (state.locationSearch.active) {
|
||||||
initializeResultsMap(state.schools);
|
initializeResultsMap(state.schools);
|
||||||
@@ -1094,9 +1120,72 @@ function setResultsView(view) {
|
|||||||
} else {
|
} else {
|
||||||
elements.resultsContainer.classList.remove("map-view");
|
elements.resultsContainer.classList.remove("map-view");
|
||||||
destroyResultsMap();
|
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
|
||||||
|
? `<span class="distance-badge">${school.distance.toFixed(1)} mi</span>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="school-list-item" data-urn="${school.urn}">
|
||||||
|
<div class="school-list-item-content">
|
||||||
|
<div class="school-list-item-header">
|
||||||
|
<h4 class="school-list-item-name">${escapeHtml(school.school_name)}</h4>
|
||||||
|
${distanceBadge}
|
||||||
|
</div>
|
||||||
|
<div class="school-list-item-meta">
|
||||||
|
<span>${escapeHtml(school.school_type || "")}</span>
|
||||||
|
<span>${escapeHtml(school.local_authority || "")}</span>
|
||||||
|
</div>
|
||||||
|
<div class="school-list-item-stats">
|
||||||
|
<span class="school-list-item-stat">
|
||||||
|
<strong>${formatMetricValue(school.rwm_expected_pct, "rwm_expected_pct")}</strong> RWM
|
||||||
|
</span>
|
||||||
|
<span class="school-list-item-stat">
|
||||||
|
<strong>${school.total_pupils || "-"}</strong> pupils
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary school-list-item-details" data-urn="${school.urn}">Details</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.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
|
// RENDER FUNCTIONS
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
@@ -492,11 +492,88 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Highlighted card in map view */
|
/* Highlighted card in map view */
|
||||||
.school-card.highlighted {
|
.school-card.highlighted,
|
||||||
|
.school-list-item.highlighted {
|
||||||
border-color: var(--accent-teal);
|
border-color: var(--accent-teal);
|
||||||
box-shadow: 0 0 0 2px rgba(45, 125, 125, 0.2);
|
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 on map */
|
||||||
.search-location-marker {
|
.search-location-marker {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|||||||
Reference in New Issue
Block a user