mirror of
https://github.com/samsonjs/spirit-tracker.git
synced 2026-03-25 09:25:51 +00:00
feat: Better URL rendering
This commit is contained in:
parent
2f708a9092
commit
f33ff16419
2 changed files with 35 additions and 71 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue