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 { 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
? `<a class="badge" href="${esc(
href
@ -94,18 +89,18 @@ export function renderSearch($app) {
return `
<div class="item" data-sku="${esc(it.sku)}">
<div class="itemRow">
<div class="thumbBox">${renderThumbHtml(it.img)}</div>
<div class="thumbBox">
${renderThumbHtml(it.img)}
</div>
<div class="itemBody">
<div class="itemMain">
<div class="itemTop">
<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>
</div>
<div class="metaRow">
<span class="mono price">${esc(price)}</span>
${storeBadge}
</div>
</div>
</div>
</div>
@ -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) {
)}</a>`
: `<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 `
<div class="item" data-sku="${esc(sku)}">
<div class="itemRow">
<div class="thumbBox">${renderThumbHtml(img)}</div>
<div class="thumbBox">
${renderThumbHtml(img)}
</div>
<div class="itemBody">
<div class="itemMain">
<div class="itemTop">
<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>
</div>
<div class="metaRow">
<span class="badge">${esc(kind)}</span>
<span class="mono price">${esc(priceLine)}</span>
${storeBadge}
${dateBadge}
</div>
</div>
</div>
</div>

View file

@ -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;