From 17cc14b48bd37807d84873cfde5b7cc4fbda8bd4 Mon Sep 17 00:00:00 2001 From: emi Date: Sun, 26 Jul 2026 18:36:39 +0000 Subject: [PATCH] bookmarks: map + account UI for saved locations --- frontend/static/subscriptions.js | 99 +++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/frontend/static/subscriptions.js b/frontend/static/subscriptions.js index 22dc124..24701cc 100644 --- a/frontend/static/subscriptions.js +++ b/frontend/static/subscriptions.js @@ -2,10 +2,16 @@ // them add a city (via the shared map picker), tune the percentile threshold and // watched metrics, toggle active, and remove. Auth-gated: signed-out visitors get // a sign-in prompt. All calls go through account.js's cookie-aware apiFetch. +// +// Also the closest thing to an account page, so it doubles as the Saved +// locations surface: bookmarks.js is the single source of truth for those (see +// its header comment), this just lists/renders/edits them the same way it does +// alert subscriptions, reusing .sub-list/.sub-card. import "./nav.js"; import { apiFetch, openAuth, onAuthChange, uv } from "./account.js"; import { open as openPicker } from "./mappicker.js"; import * as pushClient from "./push-client.js"; +import * as bookmarks from "./bookmarks.js"; // Metric keys a user can watch, with friendly labels. (fmax/fmin exist server-side // but are omitted from the picker to keep it focused; labelled here if present.) @@ -24,6 +30,7 @@ METRIC_LABEL.fmin = "Feels-like low"; const DEFAULT_METRICS = ["tmax", "feels", "precip"]; const TRASH_IC = ``; +const PENCIL_IC = ``; const body = document.getElementById("alerts-body"); let subs = []; @@ -33,6 +40,25 @@ function esc(s) { ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } +// Small stylesheet for the bits of the Saved locations section that don't +// already have a home in .sub-list/.sub-card — injected once, not appended to +// the shared style.css, so this stays self-contained. +(function injectBkmStyle() { + if (document.getElementById("tg-bkm-alerts-style")) return; + const s = document.createElement("style"); + s.id = "tg-bkm-alerts-style"; + s.textContent = ` +.bkm-heading { font-size: 15px; margin: 26px 0 4px; } +.bkm-import-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin: 0 0 12px; } +.bkm-import-status { margin: -4px 0 10px; } +.bkm-card-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; } +.bkm-rename { background: none; border: 0; color: var(--muted); cursor: pointer; padding: 7px; border-radius: 6px; display: inline-flex; } +.bkm-rename:hover { color: var(--text); background: var(--border); } +.bkm-card-loc { color: var(--muted); font-size: 12.5px; } +`; + document.head.appendChild(s); +})(); + async function readErr(res) { try { const d = await res.json(); @@ -80,17 +106,84 @@ function render() {

Each alert sends at most one notification per week.

- `; + +
`; body.querySelector("#add-alert").onclick = startAdd; renderPushBar(); const list = body.querySelector("#sub-list"); if (!subs.length) { list.innerHTML = `
  • No alerts yet. Add a city to get started.
  • `; - return; + } else { + subs.forEach((s) => list.appendChild(subCard(s))); } - subs.forEach((s) => list.appendChild(subCard(s))); + renderBookmarksSection(); } +// --- Saved locations (bookmarks) --------------------------------------------- +function bkmCard(b) { + const li = document.createElement("li"); + li.className = "sub-card"; + const label = b.label || `${b.lat.toFixed(3)}, ${b.lon.toFixed(3)}`; + li.innerHTML = ` +
    +
    + ${esc(label)} + ${b.label ? `${esc(b.lat.toFixed(3))}, ${esc(b.lon.toFixed(3))}` : ""} +
    + + + + +
    `; + li.querySelector(".bkm-rename").onclick = () => { + const next = window.prompt("Rename this saved location:", b.label || ""); + if (next && next.trim()) bookmarks.rename(b.id, next.trim()); + }; + li.querySelector(".sub-remove").onclick = () => bookmarks.remove(b.id); + return li; +} + +function renderBookmarksSection() { + const el = document.getElementById("bkm-alerts-section"); + if (!el) return; + const list = bookmarks.list(); + el.innerHTML = ` +

    Saved locations

    +
    +

    Places you've starred on the map, kept in sync with your account.

    + +
    + + `; + const ul = el.querySelector("#bkm-list"); + if (!list.length) { + ul.innerHTML = `
  • No saved locations yet. Star a spot on the map to see it here.
  • `; + } else { + list.forEach((b) => ul.appendChild(bkmCard(b))); + } + el.querySelector("#bkm-import-btn").onclick = async () => { + const btn = el.querySelector("#bkm-import-btn"); + const status = el.querySelector("#bkm-import-status"); + btn.disabled = true; + try { + const res = await bookmarks.importAllLocal(); + status.hidden = false; + status.textContent = res.imported + ? `Imported ${res.imported} location${res.imported === 1 ? "" : "s"}${res.skipped ? ` (${res.skipped} already saved)` : ""}.` + : "Nothing new to import from this device."; + } catch (e) { + status.hidden = false; + status.textContent = e.message || "Couldn't import right now."; + } finally { + btn.disabled = false; + } + }; +} + +// Bookmarks can change from outside this render cycle (rename/remove above, +// or an import banner) — keep the section live rather than only painting once. +bookmarks.subscribe(() => renderBookmarksSection()); + // --- push notifications toggle (this device) --------------------------------- // Delivery over OS push is per-device: a subscription lives in each browser, so // this control reflects/toggles *this* device, separate from the account-wide