From ef0a08197712f0ddab5ad1deb781f64178d0033f Mon Sep 17 00:00:00 2001 From: "Brennan Wilkes (Text Groove)" Date: Tue, 20 Jan 2026 14:13:46 -0800 Subject: [PATCH] feat: Better URL rendering --- viz/app/linker_page.js | 29 +++++++++++++++++++--- viz/app/search_page.js | 55 +++++++++++++++++++++++++++++++++++++----- viz/style.css | 14 +++++++---- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/viz/app/linker_page.js b/viz/app/linker_page.js index c010007..f820e05 100644 --- a/viz/app/linker_page.js +++ b/viz/app/linker_page.js @@ -353,6 +353,20 @@ export async function renderSkuLinker($app) { const idx = await loadIndex(); const allRows = Array.isArray(idx.items) ? idx.items : []; + // skuKey -> storeLabel -> url + const URL_BY_SKU_STORE = new Map(); + for (const r of allRows) { + if (!r || r.removed) continue; + const skuKey = String(keySkuForRow(r) || "").trim(); + if (!skuKey) continue; + const storeLabel = String(r.storeLabel || r.store || "").trim(); + const url = String(r.url || "").trim(); + if (!storeLabel || !url) continue; + let m = URL_BY_SKU_STORE.get(skuKey); + if (!m) URL_BY_SKU_STORE.set(skuKey, (m = new Map())); + if (!m.has(storeLabel)) m.set(storeLabel, url); + } + // candidates for this page (hide unknown u: entirely) const allAgg = aggregateBySku(allRows, (x) => x).filter((it) => !isUnknownSkuKey(it.sku)); @@ -375,8 +389,12 @@ export async function renderSkuLinker($app) { const price = it.cheapestPriceStr ? it.cheapestPriceStr : "(no price)"; const store = it.cheapestStoreLabel || ([...it.stores][0] || "Store"); - // NEW: store badge is the link (use first store url) - const href = String(it.sampleUrl || "").trim(); + // IMPORTANT: link must match displayed store label + const href = + URL_BY_SKU_STORE.get(String(it.sku || ""))?.get(String(store || "")) || + String(it.sampleUrl || "").trim() || + ""; + const storeBadge = href ? `${esc(it.name || "(no name)")} ${esc(displaySku(it.sku))} + +
${esc(price)} +
+
${storeBadge}
+ ${pinned ? `
Pinned (click again to unpin)
` : ``} @@ -640,7 +663,7 @@ export async function renderSkuLinker($app) { return; } if (isIgnoredPair(a, b)) { - $status.textContent = "Not allowed: unknown SKUs cannot be ignored."; + $status.textContent = "This pair is already ignored."; return; } diff --git a/viz/app/search_page.js b/viz/app/search_page.js index 2897163..b52a6fc 100644 --- a/viz/app/search_page.js +++ b/viz/app/search_page.js @@ -1,6 +1,6 @@ /* viz/app/search_page.js */ import { esc, renderThumbHtml, prettyTs } from "./dom.js"; -import { tokenizeQuery, matchesAllTokens, displaySku } from "./sku.js"; +import { tokenizeQuery, matchesAllTokens, displaySku, keySkuForRow } from "./sku.js"; import { loadIndex, loadRecent, loadSavedQuery, saveQuery } from "./state.js"; import { aggregateBySku } from "./catalog.js"; import { loadSkuRules } from "./mapping.js"; @@ -32,6 +32,40 @@ export function renderSearch($app) { let allAgg = []; let indexReady = false; + // sku(canonical) -> storeLabel -> url + let URL_BY_SKU_STORE = new Map(); + + function buildUrlMap(listings, canonicalSkuFn) { + const out = new Map(); + for (const r of Array.isArray(listings) ? listings : []) { + if (!r || r.removed) continue; + const skuKey = String(keySkuForRow(r) || "").trim(); + if (!skuKey) continue; + + const sku = String(canonicalSkuFn ? canonicalSkuFn(skuKey) : skuKey); + if (!sku) continue; + + const storeLabel = String(r.storeLabel || r.store || "").trim(); + const url = String(r.url || "").trim(); + if (!storeLabel || !url) continue; + + let m = out.get(sku); + if (!m) out.set(sku, (m = new Map())); + if (!m.has(storeLabel)) m.set(storeLabel, url); + } + return out; + } + + function urlForAgg(it, storeLabel) { + const sku = String(it?.sku || ""); + const s = String(storeLabel || ""); + return ( + URL_BY_SKU_STORE.get(sku)?.get(s) || + String(it?.sampleUrl || "").trim() || + "" + ); + } + function renderAggregates(items) { if (!items.length) { $results.innerHTML = `
No matches.
`; @@ -46,8 +80,8 @@ export function renderSearch($app) { const price = it.cheapestPriceStr ? it.cheapestPriceStr : "(no price)"; const store = it.cheapestStoreLabel || ([...it.stores][0] || "Store"); - // NEW: store badge is the link (use first store url) - const href = String(it.sampleUrl || "").trim(); + // IMPORTANT: link must match the displayed store label + const href = urlForAgg(it, store); const storeBadge = href ? `
${esc(it.name || "(no name)")}
${esc(displaySku(it.sku))} - + + +
${esc(price)} +
+
${storeBadge}
@@ -131,7 +169,6 @@ export function renderSearch($app) { const img = aggBySku.get(sku)?.img || ""; - // NEW: store badge links to this row's url const href = String(r.url || "").trim(); const storeBadge = href ? `
${esc(r.name || "(no name)")}
${esc(displaySku(sku))} - + + +
${esc(kind)} ${storeBadge} +
+
${esc(priceLine)}
@@ -197,6 +238,8 @@ export function renderSearch($app) { const listings = Array.isArray(idx.items) ? idx.items : []; allAgg = aggregateBySku(listings, rules.canonicalSku); aggBySku = new Map(allAgg.map((x) => [String(x.sku || ""), x])); + URL_BY_SKU_STORE = buildUrlMap(listings, rules.canonicalSku); + indexReady = true; $q.focus(); diff --git a/viz/style.css b/viz/style.css index 51017d6..080cdc3 100644 --- a/viz/style.css +++ b/viz/style.css @@ -19,8 +19,9 @@ body { a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; } -/* NEW: don't underline badge-links on hover */ -a.badge:hover { text-decoration: none; } +/* Make badge-links look like badges (not accent-blue) + clearly clickable */ +a.badge { color: var(--muted); } +a.badge:hover { text-decoration: underline; cursor: pointer; } .container { max-width: 980px; @@ -128,16 +129,19 @@ a.badge:hover { text-decoration: none; } } .badge { - font-size: 12px; + font-size: 13px; color: var(--muted); border: 1px solid var(--border); - padding: 2px 8px; + padding: 3px 10px; border-radius: 999px; white-space: nowrap; + display: inline-flex; + align-items: center; + gap: 6px; } .meta { - margin-top: 6px; + margin-top: 8px; display: flex; gap: 10px; flex-wrap: wrap;