85 lines
3.5 KiB
JavaScript
85 lines
3.5 KiB
JavaScript
|
|
// Approximate-location fallback.
|
||
|
|
//
|
||
|
|
// The only reason this exists: a visitor who declines (or cannot use) browser
|
||
|
|
// geolocation currently hits a dead end — search or the map picker, or nothing.
|
||
|
|
// This asks the backend for a coarse guess derived from the request's own IP,
|
||
|
|
// and offers it as a *suggestion*.
|
||
|
|
//
|
||
|
|
// Three rules the UI must never break, because the accuracy gap is enormous
|
||
|
|
// (browser geolocation is metres; a city-level IP database is tens of
|
||
|
|
// kilometres, against a ~2 mile grid cell):
|
||
|
|
//
|
||
|
|
// 1. Say it is a guess, in the visitor's own words, right next to the result.
|
||
|
|
// Never present it as "your location".
|
||
|
|
// 2. Put the correction one obvious tap away, permanently visible — not
|
||
|
|
// hidden behind a dismissed toast.
|
||
|
|
// 3. Never persist it. An explicit choice is remembered (localStorage) and
|
||
|
|
// written into the URL hash; a guess is neither. A guess that became
|
||
|
|
// sticky would be indistinguishable from a choice on the next visit, and
|
||
|
|
// a guessed URL would propagate someone else's rough location when shared.
|
||
|
|
//
|
||
|
|
// The endpoint answers 204 for every "no" — feature off, no database, private
|
||
|
|
// or unparsable address, no match, match too coarse — so there is exactly one
|
||
|
|
// branch here and it is "behave exactly as the site does today".
|
||
|
|
import { uv } from "./account.js";
|
||
|
|
import { track } from "./digest.js";
|
||
|
|
import { esc } from "./shared.js";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Ask the backend for an approximate location.
|
||
|
|
* Resolves to the suggestion object, or null when there is nothing to suggest.
|
||
|
|
* Never rejects: a failed fetch is just "no suggestion".
|
||
|
|
*/
|
||
|
|
export async function fetchApprox() {
|
||
|
|
try {
|
||
|
|
const res = await fetch(uv("geoip"), { headers: { Accept: "application/json" } });
|
||
|
|
if (res.status !== 200) return null; // 204 is the normal "no"
|
||
|
|
const data = await res.json();
|
||
|
|
if (!data || !data.approximate) return null; // never trust an unlabelled payload
|
||
|
|
if (typeof data.lat !== "number" || typeof data.lon !== "number") return null;
|
||
|
|
return data;
|
||
|
|
} catch (e) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function attributionHtml(a) {
|
||
|
|
// Both candidate databases are free *with attribution* and both ask for the
|
||
|
|
// credit on the page that displays a result — so it rides with the result
|
||
|
|
// rather than living only in a footer that this view doesn't have.
|
||
|
|
if (!a || !a.text) return "";
|
||
|
|
const text = esc(a.text);
|
||
|
|
return a.url
|
||
|
|
? ` <a class="geo-approx-credit" href="${esc(a.url)}" rel="noopener nofollow" target="_blank">${text}</a>`
|
||
|
|
: ` <span class="geo-approx-credit">${text}</span>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Render the suggestion banner into `host` and load the guessed place.
|
||
|
|
*
|
||
|
|
* @param host container element (hidden until there is something to show)
|
||
|
|
* @param data a suggestion from fetchApprox()
|
||
|
|
* @param onFix called when the visitor corrects the guess
|
||
|
|
*/
|
||
|
|
export function renderApprox(host, data, onFix) {
|
||
|
|
if (!host) return;
|
||
|
|
host.innerHTML =
|
||
|
|
`<p class="geo-approx-line">Showing weather near <b>${esc(data.label)}</b>.</p>` +
|
||
|
|
`<p class="geo-approx-note">That is a rough guess from your internet connection, ` +
|
||
|
|
`not from your device — it can be tens of kilometres out.` +
|
||
|
|
attributionHtml(data.attribution) + `</p>` +
|
||
|
|
`<button type="button" class="btn-ghost geo-approx-fix">Not right? Choose your spot</button>`;
|
||
|
|
host.hidden = false;
|
||
|
|
host.querySelector(".geo-approx-fix")?.addEventListener("click", () => {
|
||
|
|
track("home.geoip_fixed");
|
||
|
|
onFix?.();
|
||
|
|
});
|
||
|
|
track("home.geoip_shown");
|
||
|
|
}
|
||
|
|
|
||
|
|
export function clearApprox(host) {
|
||
|
|
if (!host) return;
|
||
|
|
host.hidden = true;
|
||
|
|
host.innerHTML = "";
|
||
|
|
}
|