Give climate/city/record pages full header parity + working °F/°C (#124)
The server-rendered SEO pages shared base.html.j2's nav but carried no JS, so
they lacked the °F/°C toggle and account/bell, and their temperatures were baked
as dual-unit strings that couldn't respond to a toggle.
- Load units.js + account.js + climate.js on base.html.j2, so every SEO page gets
the same header as the interactive pages (nav + °F/°C toggle + account + bell).
- Emit each temperature as <span class="temp" data-temp-f> (default °F text, real
for crawlers/no-JS); climate.js rewrites them to the active unit on load and on
toggle. Tint classes stay pinned to absolute °F. Drops the inline "(NN°C)".
- Make account.js base-aware: derive the app base from import.meta.url so its
api/v2 fetches and the alerts links resolve from deep /climate/{slug} URLs
instead of 404ing. Equivalent on the depth-1 interactive pages.
- Default the unit from the browser locale for first-time visitors (no stored
pref): en-US → °F, everyone else → °C. A stored choice always wins.
This commit is contained in:
parent
ca4d8035a9
commit
595a3b13d8
3 changed files with 49 additions and 7 deletions
|
|
@ -8,10 +8,17 @@
|
|||
let currentUser = null; // {id, email, display_name} or null
|
||||
const authCbs = []; // notified on login/logout so pages can re-gate
|
||||
|
||||
// The app's base path (e.g. "/thermograph"), derived from this module's own URL
|
||||
// — it's always served from `${base}/account.js`, so this resolves correctly on
|
||||
// both the depth-1 interactive pages and the deep SEO URLs (/climate/{slug}/…),
|
||||
// where a directory-relative "api/v2/…" would otherwise resolve wrong and 404.
|
||||
const APP_BASE = new URL(".", import.meta.url).pathname.replace(/\/$/, "");
|
||||
const u = (path) => `${APP_BASE}/${String(path).replace(/^\//, "")}`;
|
||||
|
||||
// --- shared fetch helper -----------------------------------------------------
|
||||
// Base-relative URL (resolves under /thermograph automatically), cookie sent,
|
||||
// and a constant X-TG-Auth header the server can require as cheap CSRF defense
|
||||
// (trivial same-origin, impossible cross-site without a preflight we never grant).
|
||||
// Absolute app-base URL (so it works from any page depth), cookie sent, and a
|
||||
// constant X-TG-Auth header the server can require as cheap CSRF defense (trivial
|
||||
// same-origin, impossible cross-site without a preflight we never grant).
|
||||
export async function apiFetch(path, { method = "GET", json, form } = {}) {
|
||||
const opts = { method, credentials: "same-origin", headers: { "X-TG-Auth": "1" } };
|
||||
if (json !== undefined) {
|
||||
|
|
@ -21,7 +28,7 @@ export async function apiFetch(path, { method = "GET", json, form } = {}) {
|
|||
opts.headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
opts.body = new URLSearchParams(form).toString();
|
||||
}
|
||||
return fetch(path, opts);
|
||||
return fetch(u(path), opts);
|
||||
}
|
||||
|
||||
// Parse a JSON response, throwing a friendly Error on failure. fastapi-users
|
||||
|
|
@ -285,7 +292,7 @@ function renderHeader() {
|
|||
<button type="button" class="notif-readall">Mark all read</button>
|
||||
</div>
|
||||
<ul class="notif-list"></ul>
|
||||
<a href="alerts" class="notif-manage">Manage alerts →</a>
|
||||
<a href="${APP_BASE}/alerts" class="notif-manage">Manage alerts →</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="acct-menu">
|
||||
|
|
@ -293,7 +300,7 @@ function renderHeader() {
|
|||
${USER_IC}<span class="acct-name">${escapeHtml(name)}</span>
|
||||
</button>
|
||||
<div class="acct-pop" hidden>
|
||||
<a href="alerts" class="acct-pop-link">My alerts</a>
|
||||
<a href="${APP_BASE}/alerts" class="acct-pop-link">My alerts</a>
|
||||
<button type="button" class="acct-pop-link acct-signout">Sign out</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
|
|
|||
18
static/climate.js
Normal file
18
static/climate.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Client-side temperature converter for the server-rendered climate/city/record
|
||||
// pages. Those pages render every temperature as `<span class="temp"
|
||||
// data-temp-f="72.3">72°F</span>` (see content.py `_temp`/`_temp_bare`) so the
|
||||
// value is real in the HTML for crawlers and no-JS visitors. Here we rewrite each
|
||||
// span to the active unit on load and whenever the °F/°C toggle changes — reusing
|
||||
// the same unit machinery as the interactive pages. Importing units.js also boots
|
||||
// its toggle-injection IIFE, so this one script gives the pages a working toggle.
|
||||
import { fmtTemp, onUnitChange } from "./units.js";
|
||||
|
||||
function paint() {
|
||||
for (const el of document.querySelectorAll(".temp[data-temp-f]")) {
|
||||
const f = parseFloat(el.dataset.tempF);
|
||||
if (!Number.isNaN(f)) el.textContent = fmtTemp(f, !el.hasAttribute("data-bare"));
|
||||
}
|
||||
}
|
||||
|
||||
paint();
|
||||
onUnitChange(paint);
|
||||
|
|
@ -4,7 +4,24 @@
|
|||
// stored/plotted values stay in °F and only the numbers shown flip.
|
||||
|
||||
const UNIT_KEY = "thermograph:unit";
|
||||
let _unit = (localStorage.getItem(UNIT_KEY) === "C") ? "C" : "F";
|
||||
|
||||
// First-time default: pick °F only for the countries that actually use it (the US
|
||||
// plus a handful of territories/nations), otherwise °C — read off the browser
|
||||
// locale's region subtag (en-US → F, de-DE → C). A stored choice always wins, so
|
||||
// this only affects visitors who've never toggled. Not persisted, so an unstored
|
||||
// visitor keeps re-deriving the same locale default until they pick one.
|
||||
const F_REGIONS = new Set(["US", "PR", "GU", "VI", "AS", "MP", "UM", "BS", "BZ", "KY", "PW", "FM", "MH", "LR"]);
|
||||
function defaultUnit() {
|
||||
const langs = (navigator.languages && navigator.languages.length)
|
||||
? navigator.languages : [navigator.language];
|
||||
for (const l of langs) {
|
||||
const m = /-([A-Za-z]{2})\b/.exec(l || "");
|
||||
if (m) return F_REGIONS.has(m[1].toUpperCase()) ? "F" : "C";
|
||||
}
|
||||
return "F"; // no region subtag to go on → keep the historical default
|
||||
}
|
||||
const _stored = localStorage.getItem(UNIT_KEY);
|
||||
let _unit = (_stored === "C" || _stored === "F") ? _stored : defaultUnit();
|
||||
const _unitCbs = [];
|
||||
|
||||
export function getUnit() { return _unit; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue