thermograph/static/nav.js
Emi Griffith 4d3a5a1053 Convert the frontend to ES modules; split nav.js by concern (#48)
The frontend was classic scripts sharing one global scope, with a
load-order contract enforced only by comments (leaflet -> nav ->
shared -> mappicker -> page) and hand-rolled window.Thermograph /
window.LocationPicker namespaces. Page scripts now import what they use;
the dependency graph replaces the ordering contract, and no app globals
remain (Leaflet stays a classic script / global L, loaded first).

nav.js had grown four concerns; it's now three single-purpose modules:
- nav.js: last-location memory + header view-links + locHash.
- units.js: the °F/°C toggle and unit-aware formatting. The compare
  special case is gone — pages that want the toggle import units.js;
  compare simply doesn't.
- cache.js: the IndexedDB response cache, SWR getJSON, bundle-seeded
  view prefetch and neighbor warming. prefetchViews(lat, lon, ownViews)
  now takes the calling page's own view names instead of a page-identity
  map (VIEW_OWN) — adding a page no longer means editing this module.
  The slice->URL map stays here as bundle-contract knowledge.

frontend/package.json ({"type": "module"}) makes CI's node --check
parse the files as modules.

Verified: node --check on all files as modules; 108 backend tests;
headless-Chromium smoke across all five pages against live data — zero
console/page errors, all render assertions pass (cards, chart, calendar
grid + metric switch, ladders, compare ranking, legend scales).
2026-07-11 20:28:33 +00:00

57 lines
2.1 KiB
JavaScript

// Cross-view navigation + last-location memory. Remembers the most recently
// selected spot in localStorage and keeps the header's view links
// (Map · Calendar · Day · Compare) pointing at that spot — so switching views,
// or coming back to the map, lands you where you were instead of an empty map.
const LOC_KEY = "thermograph:loc";
function readStored() {
try {
const o = JSON.parse(localStorage.getItem(LOC_KEY));
if (o && typeof o.lat === "number" && typeof o.lon === "number") return o;
} catch (e) {}
return null;
}
// Where should this page start? A hash (a shared/opened link) always wins;
// otherwise fall back to the last place the user looked at.
export function loadLastLocation() {
const p = new URLSearchParams(location.hash.slice(1));
const lat = parseFloat(p.get("lat")), lon = parseFloat(p.get("lon"));
if (!isNaN(lat) && !isNaN(lon)) return { lat, lon, date: p.get("date") || null };
return readStored();
}
// Remember a spot. Passing no `date` preserves whatever date was stored before,
// so hopping through the calendar (which has no date) doesn't wipe the day you
// were on elsewhere.
export function saveLastLocation(lat, lon, date) {
let d = date;
if (d === undefined) { const prev = readStored(); d = prev ? prev.date : null; }
try { localStorage.setItem(LOC_KEY, JSON.stringify({ lat, lon, date: d || null })); } catch (e) {}
refreshNav(lat, lon, d);
}
export function locHash(lat, lon, date) {
let h = `#lat=${lat.toFixed(5)}&lon=${lon.toFixed(5)}`;
if (date) h += `&date=${date}`;
return h;
}
// Point every header view-link at the current spot so a tap keeps your place.
function refreshNav(lat, lon, date) {
if (lat == null || lon == null) return;
const h = locHash(lat, lon, date);
document.querySelectorAll(".view-nav a[data-view]").forEach((a) => {
a.href = (a.dataset.base || "") + h;
});
}
// Point the header links at the current spot as soon as the page loads.
(function initNav() {
document.querySelectorAll(".view-nav a[data-view]").forEach((a) => {
a.dataset.base = a.getAttribute("href");
});
const loc = loadLastLocation();
if (loc) refreshNav(loc.lat, loc.lon, loc.date);
})();