promote: main → release #132
1 changed files with 96 additions and 3 deletions
|
|
@ -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 = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>`;
|
||||
const PENCIL_IC = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>`;
|
||||
|
||||
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() {
|
|||
<button type="button" class="find-btn" id="add-alert">+ Add a city</button>
|
||||
<p class="alerts-note muted">Each alert sends at most one notification per week.</p>
|
||||
</div>
|
||||
<ul class="sub-list" id="sub-list"></ul>`;
|
||||
<ul class="sub-list" id="sub-list"></ul>
|
||||
<section class="bkm-section" id="bkm-alerts-section" aria-labelledby="bkm-heading"></section>`;
|
||||
body.querySelector("#add-alert").onclick = startAdd;
|
||||
renderPushBar();
|
||||
const list = body.querySelector("#sub-list");
|
||||
if (!subs.length) {
|
||||
list.innerHTML = `<li class="sub-empty muted">No alerts yet. Add a city to get started.</li>`;
|
||||
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 = `
|
||||
<div class="sub-top">
|
||||
<div class="sub-title">
|
||||
<span class="sub-name">${esc(label)}</span>
|
||||
${b.label ? `<span class="bkm-card-loc">${esc(b.lat.toFixed(3))}, ${esc(b.lon.toFixed(3))}</span>` : ""}
|
||||
</div>
|
||||
<span class="bkm-card-actions">
|
||||
<button type="button" class="bkm-rename" title="Rename" aria-label="Rename ${esc(label)}">${PENCIL_IC}</button>
|
||||
<button type="button" class="sub-remove" aria-label="Remove ${esc(label)}">${TRASH_IC}</button>
|
||||
</span>
|
||||
</div>`;
|
||||
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 = `
|
||||
<h2 class="bkm-heading" id="bkm-heading">Saved locations</h2>
|
||||
<div class="bkm-import-row">
|
||||
<p class="alerts-note muted">Places you've starred on the map, kept in sync with your account.</p>
|
||||
<button type="button" class="find-btn" id="bkm-import-btn">Import saved locations from this device</button>
|
||||
</div>
|
||||
<p class="bkm-import-status muted" id="bkm-import-status" hidden></p>
|
||||
<ul class="sub-list" id="bkm-list"></ul>`;
|
||||
const ul = el.querySelector("#bkm-list");
|
||||
if (!list.length) {
|
||||
ul.innerHTML = `<li class="sub-empty muted">No saved locations yet. Star a spot on the map to see it here.</li>`;
|
||||
} 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue