diff --git a/viz/app/dom.js b/viz/app/dom.js index eff8d4d..5b765bd 100644 --- a/viz/app/dom.js +++ b/viz/app/dom.js @@ -17,9 +17,38 @@ export function esc(s) { export function prettyTs(iso) { const s = String(iso || ""); if (!s) return ""; - return s.replace("T", " "); + + const d = new Date(s); + if (!Number.isFinite(d.getTime())) return ""; + + const parts = new Intl.DateTimeFormat("en-US", { + timeZone: "America/Vancouver", + month: "long", + day: "numeric", + hour: "numeric", + minute: "2-digit", + hour12: true, + }).formatToParts(d); + + let month = ""; + let day = ""; + let hour = ""; + let minute = ""; + let dayPeriod = ""; + + for (const p of parts) { + if (p.type === "month") month = p.value; + else if (p.type === "day") day = p.value; + else if (p.type === "hour") hour = p.value; + else if (p.type === "minute") minute = p.value; + else if (p.type === "dayPeriod") dayPeriod = p.value; + } + + const ampm = String(dayPeriod || "").toLowerCase(); // "am"/"pm" + return `${month} ${day} ${hour}:${minute}${ampm}`; } + export function renderThumbHtml(imgUrl, cls = "thumb") { const img = normImg(imgUrl); if (!img) return `
`; diff --git a/viz/app/search_page.js b/viz/app/search_page.js index 0b83381..bca5259 100644 --- a/viz/app/search_page.js +++ b/viz/app/search_page.js @@ -213,25 +213,46 @@ export function renderSearch($app) { $results.innerHTML = `
Type to search…
`; return; } - + const canon = typeof canonicalSkuFn === "function" ? canonicalSkuFn : (x) => x; - - const days = Number.isFinite(Number(recent?.windowDays)) ? Number(recent.windowDays) : 3; - + + const nowMs = Date.now(); + const cutoffMs = nowMs - 24 * 60 * 60 * 1000; + + function eventMs(r) { + const t = String(r?.ts || ""); + const ms = t ? Date.parse(t) : NaN; + if (Number.isFinite(ms)) return ms; + + // fallback: date-only => treat as start of day UTC-ish + const d = String(r?.date || ""); + const ms2 = d ? Date.parse(d + "T00:00:00Z") : NaN; + return Number.isFinite(ms2) ? ms2 : 0; + } + + const inWindow = items.filter((r) => { + const ms = eventMs(r); + return ms >= cutoffMs && ms <= nowMs; + }); + + if (!inWindow.length) { + $results.innerHTML = `
No changes in the last 24 hours.
`; + return; + } + // rank + sort (custom) - const ranked = items + const ranked = inWindow .map((r) => ({ r, meta: rankRecent(r, canon) })) .sort((a, b) => { if (b.meta.score !== a.meta.score) return b.meta.score - a.meta.score; if (b.meta.tie !== a.meta.tie) return b.meta.tie - a.meta.tie; - // stable-ish fallback return String(a.meta.sku || "").localeCompare(String(b.meta.sku || "")); }); - + const limited = ranked.slice(0, 140); - + $results.innerHTML = - `
Recently changed (last ${esc(days)} day(s)):
` + + `
Recently changed (last 24 hours):
` + limited .map(({ r, meta }) => { const kindLabel = @@ -248,22 +269,22 @@ export function renderSearch($app) { : meta.kind === "price_change" ? "PRICE" : "CHANGE"; - + const priceLine = meta.kind === "new" || meta.kind === "restored" || meta.kind === "removed" ? `${esc(r.price || "")}` : `${esc(r.oldPrice || "")} → ${esc(r.newPrice || "")}`; - + const when = r.ts ? prettyTs(r.ts) : r.date || ""; - + const sku = meta.sku; const agg = aggBySku.get(sku) || null; const img = agg?.img || ""; - + // show "+N" if the canonical SKU exists in other stores (via SKU mapping) const storeCount = agg?.stores?.size || 0; const plus = storeCount > 1 ? ` +${storeCount - 1}` : ""; - + const href = String(r.url || "").trim(); const storeBadge = href ? `` : `${esc((r.storeLabel || "") + plus)}`; - + // date as a badge so it sits nicely in the single meta row const dateBadge = when ? `${esc(when)}` : ""; - + // subtle styles (inline so you don’t need to touch CSS) const offBadge = meta.kind === "price_down" && meta.pctOff !== null @@ -283,12 +304,12 @@ export function renderSearch($app) { meta.pctOff )}% Off]` : ""; - + const kindBadgeStyle = meta.kind === "new" && meta.isNewUnique ? ` style="color:rgba(20,110,40,0.95); background:rgba(20,110,40,0.10); border:1px solid rgba(20,110,40,0.20);"` : ""; - + return `
@@ -313,7 +334,7 @@ export function renderSearch($app) { `; }) .join(""); - + for (const el of Array.from($results.querySelectorAll(".item"))) { el.addEventListener("click", () => { const sku = el.getAttribute("data-sku") || "";