Compare commits
6 Commits
add-contac
...
fb30f43ef7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb30f43ef7 | ||
|
|
782c68a7ce | ||
|
|
e0e3bb788e | ||
|
|
e843394d57 | ||
|
|
7919c7b8a5 | ||
|
|
c27b31220e |
@@ -350,7 +350,7 @@ async def get_schools(
|
|||||||
"page": page,
|
"page": page,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
"total_pages": (total + page_size - 1) // page_size if page_size > 0 else 0,
|
"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
|
if search_coords
|
||||||
else None,
|
else None,
|
||||||
}
|
}
|
||||||
|
|||||||
312
frontend/app.js
312
frontend/app.js
@@ -22,7 +22,9 @@ const state = {
|
|||||||
active: false,
|
active: false,
|
||||||
postcode: null,
|
postcode: null,
|
||||||
radius: 5,
|
radius: 5,
|
||||||
|
coords: null, // Lat/lng of search location
|
||||||
},
|
},
|
||||||
|
resultsView: "list", // "list" or "map"
|
||||||
loading: {
|
loading: {
|
||||||
schools: false,
|
schools: false,
|
||||||
filters: false,
|
filters: false,
|
||||||
@@ -36,6 +38,8 @@ const state = {
|
|||||||
let comparisonChart = null;
|
let comparisonChart = null;
|
||||||
let schoolDetailChart = null;
|
let schoolDetailChart = null;
|
||||||
let modalMap = null;
|
let modalMap = null;
|
||||||
|
let resultsMapInstance = null;
|
||||||
|
let resultsMapMarkers = new Map(); // Store markers by school URN
|
||||||
|
|
||||||
// Chart colors
|
// Chart colors
|
||||||
const CHART_COLORS = [
|
const CHART_COLORS = [
|
||||||
@@ -189,6 +193,11 @@ const elements = {
|
|||||||
radiusSelect: document.getElementById("radius-select"),
|
radiusSelect: document.getElementById("radius-select"),
|
||||||
locationSearchBtn: document.getElementById("location-search-btn"),
|
locationSearchBtn: document.getElementById("location-search-btn"),
|
||||||
typeFilterLocation: document.getElementById("type-filter-location"),
|
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
|
// Schools grid
|
||||||
schoolsGrid: document.getElementById("schools-grid"),
|
schoolsGrid: document.getElementById("schools-grid"),
|
||||||
compareSearch: document.getElementById("compare-search"),
|
compareSearch: document.getElementById("compare-search"),
|
||||||
@@ -522,10 +531,27 @@ async function loadSchools() {
|
|||||||
state.pagination.totalPages = data.total_pages;
|
state.pagination.totalPages = data.total_pages;
|
||||||
state.isShowingFeatured = false;
|
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
|
// 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
|
||||||
|
updateViewToggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFeaturedSchools() {
|
async function loadFeaturedSchools() {
|
||||||
@@ -548,6 +574,9 @@ async function loadFeaturedSchools() {
|
|||||||
state.isShowingFeatured = true;
|
state.isShowingFeatured = true;
|
||||||
|
|
||||||
renderFeaturedSchools(state.schools);
|
renderFeaturedSchools(state.schools);
|
||||||
|
|
||||||
|
// Hide view toggle for featured schools
|
||||||
|
updateViewToggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLocationInfoBanner(searchLocation) {
|
function updateLocationInfoBanner(searchLocation) {
|
||||||
@@ -908,6 +937,255 @@ function openMapModal(lat, lng, schoolName) {
|
|||||||
document.addEventListener("keydown", escHandler);
|
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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
|
}).addTo(resultsMapInstance);
|
||||||
|
|
||||||
|
// Create custom icon for search location (blue)
|
||||||
|
const searchIcon = L.divIcon({
|
||||||
|
className: "search-location-marker",
|
||||||
|
html: `<svg viewBox="0 0 24 24" fill="#3498db" stroke="#2980b9" stroke-width="1" width="32" height="32">
|
||||||
|
<circle cx="12" cy="12" r="8"/>
|
||||||
|
</svg>`,
|
||||||
|
iconSize: [32, 32],
|
||||||
|
iconAnchor: [16, 16],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add search location marker
|
||||||
|
L.marker([lat, lng], { icon: searchIcon })
|
||||||
|
.addTo(resultsMapInstance)
|
||||||
|
.bindPopup(`<strong>Search location</strong><br>${state.locationSearch.postcode}`);
|
||||||
|
|
||||||
|
// Clear existing markers
|
||||||
|
resultsMapMarkers.clear();
|
||||||
|
|
||||||
|
// 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(`
|
||||||
|
<strong>${escapeHtml(school.school_name)}</strong><br>
|
||||||
|
${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, false); // Don't center map, already at marker
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
* @param {number} urn - School URN
|
||||||
|
* @param {boolean} centerMap - Whether to center the map on the school (default: true)
|
||||||
|
*/
|
||||||
|
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/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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the results map instance
|
||||||
|
*/
|
||||||
|
function destroyResultsMap() {
|
||||||
|
if (resultsMapInstance) {
|
||||||
|
try {
|
||||||
|
resultsMapInstance.remove();
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
resultsMapInstance = null;
|
||||||
|
}
|
||||||
|
resultsMapMarkers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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);
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
? `<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
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -1375,6 +1653,13 @@ async function openSchoolModal(urn) {
|
|||||||
const previous = sortedData[latestIndex + 1] || null;
|
const previous = sortedData[latestIndex + 1] || null;
|
||||||
const prevRwm = previous?.rwm_expected_pct;
|
const prevRwm = previous?.rwm_expected_pct;
|
||||||
|
|
||||||
|
// Find latest year with progress score data (not available for 2023-24, 2024-25)
|
||||||
|
const latestWithProgress = sortedData.find(
|
||||||
|
(d) => d.reading_progress !== null || d.writing_progress !== null || d.maths_progress !== null
|
||||||
|
);
|
||||||
|
const progressYear = latestWithProgress?.year || latest.year;
|
||||||
|
const progressData = latestWithProgress || latest;
|
||||||
|
|
||||||
elements.modalStats.innerHTML = `
|
elements.modalStats.innerHTML = `
|
||||||
<div class="modal-stats-section">
|
<div class="modal-stats-section">
|
||||||
<h4>KS2 Results (${latest.year})</h4>
|
<h4>KS2 Results (${latest.year})</h4>
|
||||||
@@ -1398,24 +1683,24 @@ async function openSchoolModal(urn) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-stats-section">
|
<div class="modal-stats-section">
|
||||||
<h4>Progress Scores ${createWarningTrigger("progress_scores_unavailable")}</h4>
|
<h4>Progress Scores (${progressYear}) ${createWarningTrigger("progress_scores_unavailable")}</h4>
|
||||||
<div class="modal-stats-grid">
|
<div class="modal-stats-grid">
|
||||||
<div class="modal-stat">
|
<div class="modal-stat">
|
||||||
<div class="modal-stat-value ${getProgressClass(latest.reading_progress)}">${formatMetricValue(latest.reading_progress, "reading_progress")}</div>
|
<div class="modal-stat-value ${getProgressClass(progressData.reading_progress)}">${formatMetricValue(progressData.reading_progress, "reading_progress")}</div>
|
||||||
<div class="modal-stat-label">Reading${createInfoTrigger("reading_progress")}</div>
|
<div class="modal-stat-label">Reading${createInfoTrigger("reading_progress")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-stat">
|
<div class="modal-stat">
|
||||||
<div class="modal-stat-value ${getProgressClass(latest.writing_progress)}">${formatMetricValue(latest.writing_progress, "writing_progress")}</div>
|
<div class="modal-stat-value ${getProgressClass(progressData.writing_progress)}">${formatMetricValue(progressData.writing_progress, "writing_progress")}</div>
|
||||||
<div class="modal-stat-label">Writing${createInfoTrigger("writing_progress")}</div>
|
<div class="modal-stat-label">Writing${createInfoTrigger("writing_progress")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-stat">
|
<div class="modal-stat">
|
||||||
<div class="modal-stat-value ${getProgressClass(latest.maths_progress)}">${formatMetricValue(latest.maths_progress, "maths_progress")}</div>
|
<div class="modal-stat-value ${getProgressClass(progressData.maths_progress)}">${formatMetricValue(progressData.maths_progress, "maths_progress")}</div>
|
||||||
<div class="modal-stat-label">Maths${createInfoTrigger("maths_progress")}</div>
|
<div class="modal-stat-label">Maths${createInfoTrigger("maths_progress")}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-stats-section">
|
<div class="modal-stats-section">
|
||||||
<h4>School Context</h4>
|
<h4>School Context (${latest.year})</h4>
|
||||||
<div class="modal-stats-grid">
|
<div class="modal-stats-grid">
|
||||||
<div class="modal-stat">
|
<div class="modal-stat">
|
||||||
<div class="modal-stat-value">${latest.total_pupils || "-"}</div>
|
<div class="modal-stat-value">${latest.total_pupils || "-"}</div>
|
||||||
@@ -1702,11 +1987,14 @@ function setupEventListeners() {
|
|||||||
// Clear the inactive mode's state
|
// Clear the inactive mode's state
|
||||||
if (mode === "name") {
|
if (mode === "name") {
|
||||||
// Clear location search state
|
// 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.postcodeSearch.value = "";
|
||||||
elements.radiusSelect.value = "5";
|
elements.radiusSelect.value = "5";
|
||||||
elements.typeFilterLocation.value = "";
|
elements.typeFilterLocation.value = "";
|
||||||
updateLocationInfoBanner(null);
|
updateLocationInfoBanner(null);
|
||||||
|
// Reset to list view and hide toggle
|
||||||
|
setResultsView("list");
|
||||||
|
updateViewToggle();
|
||||||
} else {
|
} else {
|
||||||
// Clear name search state
|
// Clear name search state
|
||||||
elements.schoolSearch.value = "";
|
elements.schoolSearch.value = "";
|
||||||
@@ -1719,6 +2007,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
|
// Name search and filters
|
||||||
let searchTimeout;
|
let searchTimeout;
|
||||||
elements.schoolSearch.addEventListener("input", () => {
|
elements.schoolSearch.addEventListener("input", () => {
|
||||||
|
|||||||
@@ -144,18 +144,40 @@
|
|||||||
<option value="1">1 mile</option>
|
<option value="1">1 mile</option>
|
||||||
<option value="2">2 miles</option>
|
<option value="2">2 miles</option>
|
||||||
</select>
|
</select>
|
||||||
<button id="location-search-btn" class="btn btn-primary location-btn">Find Nearby</button>
|
|
||||||
</div>
|
|
||||||
<div class="filter-row">
|
|
||||||
<select id="type-filter-location" class="filter-select">
|
<select id="type-filter-location" class="filter-select">
|
||||||
<option value="">All School Types</option>
|
<option value="">All School Types</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button id="location-search-btn" class="btn btn-primary location-btn">Find Nearby</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="schools-grid" id="schools-grid">
|
<div class="view-toggle" id="view-toggle" style="display: none;">
|
||||||
<!-- School cards populated by JS -->
|
<button class="view-toggle-btn active" data-view="list">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||||
|
<line x1="8" y1="6" x2="21" y2="6"/>
|
||||||
|
<line x1="8" y1="12" x2="21" y2="12"/>
|
||||||
|
<line x1="8" y1="18" x2="21" y2="18"/>
|
||||||
|
<line x1="3" y1="6" x2="3.01" y2="6"/>
|
||||||
|
<line x1="3" y1="12" x2="3.01" y2="12"/>
|
||||||
|
<line x1="3" y1="18" x2="3.01" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
List
|
||||||
|
</button>
|
||||||
|
<button class="view-toggle-btn" data-view="map">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||||
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
||||||
|
<circle cx="12" cy="10" r="3"/>
|
||||||
|
</svg>
|
||||||
|
Map
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="results-container" id="results-container">
|
||||||
|
<div class="results-map" id="results-map"></div>
|
||||||
|
<div class="schools-grid" id="schools-grid">
|
||||||
|
<!-- School cards populated by JS -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -348,19 +370,7 @@
|
|||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<div class="footer-contact">
|
<div class="footer-contact">
|
||||||
<h3>Contact Us</h3>
|
<a href="mailto:contact@schoolcompare.co.uk">Contact Us</a>
|
||||||
<p>Have questions, feedback, or suggestions? We'd love to hear from you.</p>
|
|
||||||
<form action="https://formsubmit.co/contact@schoolcompare.co.uk" method="POST" class="contact-form">
|
|
||||||
<input type="hidden" name="_subject" value="SchoolCompare Contact Form">
|
|
||||||
<input type="hidden" name="_captcha" value="false">
|
|
||||||
<input type="text" name="_honey" style="display:none">
|
|
||||||
<div class="form-row">
|
|
||||||
<input type="text" name="name" placeholder="Your Name" required class="form-input">
|
|
||||||
<input type="email" name="email" placeholder="Your Email" required class="form-input">
|
|
||||||
</div>
|
|
||||||
<textarea name="message" placeholder="Your Message" required class="form-input form-textarea"></textarea>
|
|
||||||
<button type="submit" class="btn btn-primary">Send Message</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-source">
|
<div class="footer-source">
|
||||||
<p>Data source: <a href="https://www.compare-school-performance.service.gov.uk/" target="_blank">UK Government - Compare School Performance</a></p>
|
<p>Data source: <a href="https://www.compare-school-performance.service.gov.uk/" target="_blank">UK Government - Compare School Performance</a></p>
|
||||||
|
|||||||
@@ -400,6 +400,185 @@ body {
|
|||||||
border-color: var(--accent-teal);
|
border-color: var(--accent-teal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* View Toggle */
|
||||||
|
.view-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toggle-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toggle-btn:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toggle-btn.active {
|
||||||
|
background: var(--accent-teal);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent-teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toggle-btn svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Results Container */
|
||||||
|
.results-container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container .results-map {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container.map-view {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 400px;
|
||||||
|
gap: 1.5rem;
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container.map-view .results-map {
|
||||||
|
display: block;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container.map-view .schools-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 600px;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container.map-view .schools-grid::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container.map-view .schools-grid::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg-main);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container.map-view .schools-grid::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container.map-view .schools-grid::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlighted card in map view */
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
/* Schools Grid */
|
/* Schools Grid */
|
||||||
.schools-grid {
|
.schools-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -669,6 +848,21 @@ body {
|
|||||||
.map-modal-content {
|
.map-modal-content {
|
||||||
height: 400px;
|
height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.results-container.map-view {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 350px auto;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container.map-view .results-map {
|
||||||
|
min-height: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container.map-view .schools-grid {
|
||||||
|
max-height: none;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Section Titles */
|
/* Section Titles */
|
||||||
@@ -1210,61 +1404,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer-contact {
|
.footer-contact {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
text-align: center;
|
||||||
|
|
||||||
.footer-contact h3 {
|
|
||||||
font-family: 'Playfair Display', serif;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-contact > p {
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-form .form-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-form .form-input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
background: var(--bg-card);
|
|
||||||
color: var(--text-primary);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-form .form-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-teal);
|
|
||||||
box-shadow: 0 0 0 3px rgba(45, 106, 100, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-form .form-input::placeholder {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-form .form-textarea {
|
|
||||||
min-height: 100px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-form .btn {
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-source {
|
.footer-source {
|
||||||
@@ -1282,16 +1423,6 @@ body {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.contact-form .form-row {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-form .btn {
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading State */
|
/* Loading State */
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user