diff --git a/viz/app/store_page.js b/viz/app/store_page.js index fa9466f..0b44d2b 100644 --- a/viz/app/store_page.js +++ b/viz/app/store_page.js @@ -67,7 +67,7 @@ export async function renderStore($app, storeLabelRaw) {
- Exclusive + Exclusive Only sold here
@@ -115,7 +115,22 @@ export async function renderStore($app, storeLabelRaw) { const listingsAll = Array.isArray(idx.items) ? idx.items : []; const liveAll = listingsAll.filter((r) => r && !r.removed); - // Build global per-canonical-SKU store presence + min prices + // Build "ever seen" store presence per canonical SKU (includes removed rows) + const everStoresBySku = new Map(); // sku -> Set(storeLabelNorm) + for (const r of listingsAll) { + if (!r) continue; + const store = normStoreLabel(r.storeLabel || r.store || ""); + if (!store) continue; + + const skuKey = keySkuForRow(r); + const sku = String(rules.canonicalSku(skuKey) || skuKey); + + let ss = everStoresBySku.get(sku); + if (!ss) everStoresBySku.set(sku, (ss = new Set())); + ss.add(store); + } + + // Build global per-canonical-SKU live store presence + min prices const storesBySku = new Map(); // sku -> Set(storeLabelNorm) const minPriceBySkuStore = new Map(); // sku -> Map(storeLabelNorm -> minPrice) @@ -172,8 +187,12 @@ export async function renderStore($app, storeLabelRaw) { items = items .map((it) => { const sku = String(it.sku || ""); - const storeSet = storesBySku.get(sku) || new Set([storeNorm]); - const exclusive = storeSet.size === 1 && storeSet.has(storeNorm); + const liveStoreSet = storesBySku.get(sku) || new Set([storeNorm]); + const everStoreSet = everStoresBySku.get(sku) || liveStoreSet; + + const soloLiveHere = liveStoreSet.size === 1 && liveStoreSet.has(storeNorm); + const lastStock = soloLiveHere && everStoreSet.size > 1; + const exclusive = soloLiveHere && !lastStock; const storePrice = Number.isFinite(it.cheapestPriceNum) ? it.cheapestPriceNum : null; const bestAll = bestAllPrice(sku); @@ -181,73 +200,72 @@ export async function renderStore($app, storeLabelRaw) { const isBest = storePrice !== null && bestAll !== null ? storePrice <= bestAll + EPS : false; - const pctVsOther = - storePrice !== null && other !== null && other > 0 - ? ((storePrice - other) / other) * 100 - : null; + const diffVsOther = + storePrice !== null && other !== null ? storePrice - other : null; - const pctVsBest = - storePrice !== null && bestAll !== null && bestAll > 0 - ? ((storePrice - bestAll) / bestAll) * 100 - : null; + const diffVsBest = + storePrice !== null && bestAll !== null ? storePrice - bestAll : null; return { ...it, _exclusive: exclusive, + _lastStock: lastStock, _storePrice: storePrice, _bestAll: bestAll, _bestOther: other, _isBest: isBest, - _pctVsOther: pctVsOther, - _pctVsBest: pctVsBest, + _diffVsOther: diffVsOther, + _diffVsBest: diffVsBest, }; }) .sort((a, b) => { - if (a._exclusive !== b._exclusive) return a._exclusive ? -1 : 1; + const aSpecial = !!(a._exclusive || a._lastStock); + const bSpecial = !!(b._exclusive || b._lastStock); + if (aSpecial !== bSpecial) return aSpecial ? -1 : 1; - const pa = a._pctVsOther; - const pb = b._pctVsOther; - const sa = pa === null ? 999999 : pa; - const sb = pb === null ? 999999 : pb; + const da = a._diffVsOther; + const db = b._diffVsOther; + const sa = da === null ? 999999 : da; + const sb = db === null ? 999999 : db; if (sa !== sb) return sa - sb; return (String(a.name) + a.sku).localeCompare(String(b.name) + b.sku); }); function priceBadgeHtml(it) { - if (it._exclusive) return ""; + if (it._exclusive || it._lastStock) return ""; - const pOther = it._pctVsOther; - const pBest = it._pctVsBest; + const d = it._diffVsOther; + if (d === null || !Number.isFinite(d)) return ""; - if (pOther === null || !Number.isFinite(pOther)) return ""; - - if (Math.abs(pOther) <= 5) { - return `same as next best price`; + const abs = Math.abs(d); + if (abs <= 5) { + return `within $5`; } - if (pOther < 0 && it._bestOther !== null && it._bestOther > 0 && it._storePrice !== null) { - const pct = Math.round(((it._bestOther - it._storePrice) / it._bestOther) * 100); - if (pct <= 0) return `same as next best price`; - return `${esc(pct)}% lower`; + const dollars = Math.round(abs); + if (d < 0) { + return `$${esc(dollars)} lower`; } - - if (pBest !== null && Number.isFinite(pBest) && pBest > 0) { - const pct = Math.round(pBest); - if (pct <= 5) return `competitive`; - return `${esc(pct)}% higher`; - } - - return ""; + return `$${esc(dollars)} higher`; } function renderCard(it) { const price = it.cheapestPriceStr ? it.cheapestPriceStr : "(no price)"; const href = String(it.sampleUrl || "").trim(); - const exclusiveBadge = it._exclusive ? `Exclusive` : ""; - const bestBadge = !it._exclusive && it._isBest ? `Best Price` : ""; - const pctBadge = priceBadgeHtml(it); + const specialBadge = it._lastStock + ? `Last Stock` + : it._exclusive + ? `Exclusive` + : ""; + + const bestBadge = + !it._exclusive && !it._lastStock && it._isBest + ? `Best Price` + : ""; + + const diffBadge = priceBadgeHtml(it); const skuLink = `#/link/?left=${encodeURIComponent(String(it.sku || ""))}`; console.log(it); @@ -262,9 +280,9 @@ export async function renderStore($app, storeLabelRaw) { href="${esc(skuLink)}" onclick="event.stopPropagation()">${esc(displaySku(it.sku))}
- ${exclusiveBadge} + ${specialBadge} ${bestBadge} - ${pctBadge} + ${diffBadge} ${esc(price)} ${ href @@ -353,8 +371,8 @@ export async function renderStore($app, storeLabelRaw) { base = items.filter((it) => matchesAllTokens(it.searchText, tokens)); } - filteredExclusive = base.filter((it) => it._exclusive); - filteredCompare = base.filter((it) => !it._exclusive); + filteredExclusive = base.filter((it) => it._exclusive || it._lastStock); + filteredCompare = base.filter((it) => !it._exclusive && !it._lastStock); setStatus(); renderNext(true); diff --git a/viz/style.css b/viz/style.css index dba7b21..2c44555 100644 --- a/viz/style.css +++ b/viz/style.css @@ -180,9 +180,9 @@ a.skuLink:hover { text-decoration: underline; cursor: pointer; } } .badgeBest { - color: rgba(160,120,20,0.95); - background: rgba(160,120,20,0.10); - border-color: rgba(160,120,20,0.22); + color: rgba(210, 170, 60, 0.95); + background: rgba(210, 170, 60, 0.12); + border-color: rgba(210, 170, 60, 0.26); } .metaRow { @@ -390,3 +390,15 @@ a.skuLink:hover { text-decoration: underline; cursor: pointer; } grid-template-columns: 1fr; /* stack columns */ } } + +.badgeExclusive { + color: rgba(20, 140, 140, 0.95); + background: rgba(20, 140, 140, 0.12); + border-color: rgba(20, 140, 140, 0.28); +} + +.badgeLastStock { + color: rgba(200, 120, 20, 0.95); + background: rgba(200, 120, 20, 0.12); + border-color: rgba(200, 120, 20, 0.28); +}