From f33ff1641946031000d5f48cbe92cd3658dc00df Mon Sep 17 00:00:00 2001 From: "Brennan Wilkes (Text Groove)" Date: Tue, 20 Jan 2026 14:24:40 -0800 Subject: [PATCH] feat: Better URL rendering --- viz/app/search_page.js | 56 +++++++++++++++++++----------------------- viz/style.css | 50 ++++++++----------------------------- 2 files changed, 35 insertions(+), 71 deletions(-) diff --git a/viz/app/search_page.js b/viz/app/search_page.js index ffd3c17..d535b9d 100644 --- a/viz/app/search_page.js +++ b/viz/app/search_page.js @@ -1,4 +1,3 @@ -/* viz/app/search_page.js */ import { esc, renderThumbHtml, prettyTs } from "./dom.js"; import { tokenizeQuery, matchesAllTokens, displaySku, keySkuForRow } from "./sku.js"; import { loadIndex, loadRecent, loadSavedQuery, saveQuery } from "./state.js"; @@ -32,7 +31,7 @@ export function renderSearch($app) { let allAgg = []; let indexReady = false; - // sku(canonical) -> storeLabel -> url + // canonicalSku -> storeLabel -> url let URL_BY_SKU_STORE = new Map(); function buildUrlMap(listings, canonicalSkuFn) { @@ -60,11 +59,7 @@ export function renderSearch($app) { 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() || - "" - ); + return URL_BY_SKU_STORE.get(sku)?.get(s) || ""; } function renderAggregates(items) { @@ -81,8 +76,8 @@ export function renderSearch($app) { const price = it.cheapestPriceStr ? it.cheapestPriceStr : "(no price)"; const store = it.cheapestStoreLabel || ([...it.stores][0] || "Store"); - // link must match displayed store label - const href = urlForAgg(it, store); + // link must match the displayed store label + const href = urlForAgg(it, store) || String(it.sampleUrl || "").trim(); const storeBadge = href ? `
-
${renderThumbHtml(it.img)}
- +
+ ${renderThumbHtml(it.img)} +
-
+
${esc(it.name || "(no name)")}
-
- -
-
${esc(price)}
- ${storeBadge} ${esc(displaySku(it.sku))}
+
+ ${esc(price)} + ${storeBadge} +
@@ -160,10 +155,8 @@ export function renderSearch($app) { : `${esc(r.oldPrice || "")} → ${esc(r.newPrice || "")}`; const when = r.ts ? prettyTs(r.ts) : r.date || ""; - const rawSku = String(r.sku || ""); const sku = canon(rawSku); - const img = aggBySku.get(sku)?.img || ""; const href = String(r.url || "").trim(); @@ -175,25 +168,26 @@ export function renderSearch($app) { )}
` : `${esc(r.storeLabel || "")}`; + // date as a badge so it sits nicely in the single meta row + const dateBadge = when ? `${esc(when)}` : ""; + return `
-
${renderThumbHtml(img)}
- +
+ ${renderThumbHtml(img)} +
-
+
${esc(r.name || "(no name)")}
-
- ${esc(kind)} -
-
- -
-
${esc(priceLine)}
- ${storeBadge} -
${esc(when)}
${esc(displaySku(sku))}
+
+ ${esc(kind)} + ${esc(priceLine)} + ${storeBadge} + ${dateBadge} +
diff --git a/viz/style.css b/viz/style.css index 7d34706..00d76e5 100644 --- a/viz/style.css +++ b/viz/style.css @@ -1,4 +1,3 @@ -/* viz/style.css */ :root { --bg: #0b0d10; --panel: #12161b; @@ -19,7 +18,7 @@ body { a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; } -/* badge links should look like badges, but clearly clickable */ +/* badge links: keep badge look, but clearly clickable */ a.badge { color: var(--muted); } a.badge:hover { text-decoration: underline; cursor: pointer; } @@ -82,7 +81,7 @@ a.badge:hover { text-decoration: underline; cursor: pointer; } .itemRow { display: flex; gap: 12px; - align-items: stretch; /* key: let thumb fill height */ + align-items: stretch; /* let thumb fill card height */ } .thumbBox { @@ -95,7 +94,6 @@ a.badge:hover { text-decoration: underline; cursor: pointer; } display: flex; align-items: stretch; justify-content: center; - height: auto; min-height: 64px; } @@ -115,26 +113,6 @@ a.badge:hover { text-decoration: underline; cursor: pointer; } .itemBody { flex: 1; min-width: 0; - display: flex; - gap: 14px; - align-items: stretch; -} - -.itemMain { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - justify-content: center; - gap: 8px; -} - -.itemFacts { - display: flex; - flex-direction: column; - align-items: flex-end; - justify-content: center; - gap: 8px; } .itemTop { @@ -147,6 +125,7 @@ a.badge:hover { text-decoration: underline; cursor: pointer; } .itemName { font-weight: 700; font-size: 14px; + min-width: 0; } .badge { @@ -161,22 +140,23 @@ a.badge:hover { text-decoration: underline; cursor: pointer; } gap: 6px; } -.meta { +.metaRow { + margin-top: 8px; display: flex; gap: 10px; flex-wrap: wrap; + align-items: center; color: var(--muted); - font-size: 12px; + font-size: 13px; } -.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } - -.priceBig { - font-size: 14px; +.price { font-weight: 700; color: var(--text); } +.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } + .topbar { display: flex; align-items: center; @@ -261,16 +241,6 @@ a.badge:hover { text-decoration: underline; cursor: pointer; } .thumbBox { width: 56px; flex: 0 0 56px; min-height: 56px; } .detailThumbBox { width: 84px; height: 84px; flex: 0 0 84px; } - /* on mobile, keep the "facts" inline so cards don't get super tall */ - .itemBody { flex-direction: column; gap: 10px; } - .itemFacts { - flex-direction: row; - flex-wrap: wrap; - justify-content: flex-start; - align-items: center; - gap: 10px; - } - .chartBox { height: 58vh; min-height: 260px;