feat: Better URL rendering

This commit is contained in:
Brennan Wilkes (Text Groove) 2026-01-20 14:24:40 -08:00
parent 2f708a9092
commit f33ff16419
2 changed files with 35 additions and 71 deletions

View file

@ -1,4 +1,3 @@
/* viz/app/search_page.js */
import { esc, renderThumbHtml, prettyTs } from "./dom.js"; import { esc, renderThumbHtml, prettyTs } from "./dom.js";
import { tokenizeQuery, matchesAllTokens, displaySku, keySkuForRow } from "./sku.js"; import { tokenizeQuery, matchesAllTokens, displaySku, keySkuForRow } from "./sku.js";
import { loadIndex, loadRecent, loadSavedQuery, saveQuery } from "./state.js"; import { loadIndex, loadRecent, loadSavedQuery, saveQuery } from "./state.js";
@ -32,7 +31,7 @@ export function renderSearch($app) {
let allAgg = []; let allAgg = [];
let indexReady = false; let indexReady = false;
// sku(canonical) -> storeLabel -> url // canonicalSku -> storeLabel -> url
let URL_BY_SKU_STORE = new Map(); let URL_BY_SKU_STORE = new Map();
function buildUrlMap(listings, canonicalSkuFn) { function buildUrlMap(listings, canonicalSkuFn) {
@ -60,11 +59,7 @@ export function renderSearch($app) {
function urlForAgg(it, storeLabel) { function urlForAgg(it, storeLabel) {
const sku = String(it?.sku || ""); const sku = String(it?.sku || "");
const s = String(storeLabel || ""); const s = String(storeLabel || "");
return ( return URL_BY_SKU_STORE.get(sku)?.get(s) || "";
URL_BY_SKU_STORE.get(sku)?.get(s) ||
String(it?.sampleUrl || "").trim() ||
""
);
} }
function renderAggregates(items) { function renderAggregates(items) {
@ -81,8 +76,8 @@ export function renderSearch($app) {
const price = it.cheapestPriceStr ? it.cheapestPriceStr : "(no price)"; const price = it.cheapestPriceStr ? it.cheapestPriceStr : "(no price)";
const store = it.cheapestStoreLabel || ([...it.stores][0] || "Store"); const store = it.cheapestStoreLabel || ([...it.stores][0] || "Store");
// link must match displayed store label // link must match the displayed store label
const href = urlForAgg(it, store); const href = urlForAgg(it, store) || String(it.sampleUrl || "").trim();
const storeBadge = href const storeBadge = href
? `<a class="badge" href="${esc( ? `<a class="badge" href="${esc(
href href
@ -94,18 +89,18 @@ export function renderSearch($app) {
return ` return `
<div class="item" data-sku="${esc(it.sku)}"> <div class="item" data-sku="${esc(it.sku)}">
<div class="itemRow"> <div class="itemRow">
<div class="thumbBox">${renderThumbHtml(it.img)}</div> <div class="thumbBox">
${renderThumbHtml(it.img)}
</div>
<div class="itemBody"> <div class="itemBody">
<div class="itemMain"> <div class="itemTop">
<div class="itemName">${esc(it.name || "(no name)")}</div> <div class="itemName">${esc(it.name || "(no name)")}</div>
</div>
<div class="itemFacts">
<div class="mono priceBig">${esc(price)}</div>
${storeBadge}
<span class="badge mono">${esc(displaySku(it.sku))}</span> <span class="badge mono">${esc(displaySku(it.sku))}</span>
</div> </div>
<div class="metaRow">
<span class="mono price">${esc(price)}</span>
${storeBadge}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -160,10 +155,8 @@ export function renderSearch($app) {
: `${esc(r.oldPrice || "")}${esc(r.newPrice || "")}`; : `${esc(r.oldPrice || "")}${esc(r.newPrice || "")}`;
const when = r.ts ? prettyTs(r.ts) : r.date || ""; const when = r.ts ? prettyTs(r.ts) : r.date || "";
const rawSku = String(r.sku || ""); const rawSku = String(r.sku || "");
const sku = canon(rawSku); const sku = canon(rawSku);
const img = aggBySku.get(sku)?.img || ""; const img = aggBySku.get(sku)?.img || "";
const href = String(r.url || "").trim(); const href = String(r.url || "").trim();
@ -175,25 +168,26 @@ export function renderSearch($app) {
)}</a>` )}</a>`
: `<span class="badge">${esc(r.storeLabel || "")}</span>`; : `<span class="badge">${esc(r.storeLabel || "")}</span>`;
// date as a badge so it sits nicely in the single meta row
const dateBadge = when ? `<span class="badge mono">${esc(when)}</span>` : "";
return ` return `
<div class="item" data-sku="${esc(sku)}"> <div class="item" data-sku="${esc(sku)}">
<div class="itemRow"> <div class="itemRow">
<div class="thumbBox">${renderThumbHtml(img)}</div> <div class="thumbBox">
${renderThumbHtml(img)}
</div>
<div class="itemBody"> <div class="itemBody">
<div class="itemMain"> <div class="itemTop">
<div class="itemName">${esc(r.name || "(no name)")}</div> <div class="itemName">${esc(r.name || "(no name)")}</div>
<div class="meta">
<span class="badge">${esc(kind)}</span>
</div>
</div>
<div class="itemFacts">
<div class="mono priceBig">${esc(priceLine)}</div>
${storeBadge}
<div class="small mono">${esc(when)}</div>
<span class="badge mono">${esc(displaySku(sku))}</span> <span class="badge mono">${esc(displaySku(sku))}</span>
</div> </div>
<div class="metaRow">
<span class="badge">${esc(kind)}</span>
<span class="mono price">${esc(priceLine)}</span>
${storeBadge}
${dateBadge}
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,4 +1,3 @@
/* viz/style.css */
:root { :root {
--bg: #0b0d10; --bg: #0b0d10;
--panel: #12161b; --panel: #12161b;
@ -19,7 +18,7 @@ body {
a { color: var(--accent); text-decoration: none; } a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; } 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 { color: var(--muted); }
a.badge:hover { text-decoration: underline; cursor: pointer; } a.badge:hover { text-decoration: underline; cursor: pointer; }
@ -82,7 +81,7 @@ a.badge:hover { text-decoration: underline; cursor: pointer; }
.itemRow { .itemRow {
display: flex; display: flex;
gap: 12px; gap: 12px;
align-items: stretch; /* key: let thumb fill height */ align-items: stretch; /* let thumb fill card height */
} }
.thumbBox { .thumbBox {
@ -95,7 +94,6 @@ a.badge:hover { text-decoration: underline; cursor: pointer; }
display: flex; display: flex;
align-items: stretch; align-items: stretch;
justify-content: center; justify-content: center;
height: auto;
min-height: 64px; min-height: 64px;
} }
@ -115,26 +113,6 @@ a.badge:hover { text-decoration: underline; cursor: pointer; }
.itemBody { .itemBody {
flex: 1; flex: 1;
min-width: 0; 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 { .itemTop {
@ -147,6 +125,7 @@ a.badge:hover { text-decoration: underline; cursor: pointer; }
.itemName { .itemName {
font-weight: 700; font-weight: 700;
font-size: 14px; font-size: 14px;
min-width: 0;
} }
.badge { .badge {
@ -161,22 +140,23 @@ a.badge:hover { text-decoration: underline; cursor: pointer; }
gap: 6px; gap: 6px;
} }
.meta { .metaRow {
margin-top: 8px;
display: flex; display: flex;
gap: 10px; gap: 10px;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center;
color: var(--muted); color: var(--muted);
font-size: 12px; font-size: 13px;
} }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } .price {
.priceBig {
font-size: 14px;
font-weight: 700; font-weight: 700;
color: var(--text); color: var(--text);
} }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.topbar { .topbar {
display: flex; display: flex;
align-items: center; 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; } .thumbBox { width: 56px; flex: 0 0 56px; min-height: 56px; }
.detailThumbBox { width: 84px; height: 84px; flex: 0 0 84px; } .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 { .chartBox {
height: 58vh; height: 58vh;
min-height: 260px; min-height: 260px;