+
+
-
-
-
-
-
-
+
+
+
+
Exclusives / Last stock
+
-
+
Exclusives
+
+
+
Last stock
+
-
-
`;
- document.getElementById("back").addEventListener("click", () => {
- sessionStorage.setItem("viz:lastRoute", location.hash);
- location.hash = "#/";
- });
-
const $q = document.getElementById("q");
- const $status = document.getElementById("status");
- const $resultsExclusive = document.getElementById("resultsExclusive");
- const $resultsCompare = document.getElementById("resultsCompare");
- const $sentinel = document.getElementById("sentinel");
- const $resultsWrap = document.getElementById("results");
-
- const $maxPrice = document.getElementById("maxPrice");
- const $maxPriceLabel = document.getElementById("maxPriceLabel");
- const $priceWrap = document.getElementById("priceWrap");
-
+ const $storeSub = document.getElementById("storeSub");
+ const $storeResults = document.getElementById("storeResults");
+ const $exclusiveResults = document.getElementById("exclusiveResults");
+ const $lastStockResults = document.getElementById("lastStockResults");
const $clearSearch = document.getElementById("clearSearch");
- const $exSort = document.getElementById("exSort");
- const $cmpMode = document.getElementById("cmpMode");
+ const $rightSort = document.getElementById("rightSort");
- // Persist query per store
- const storeNorm = normStoreLabel(storeLabel);
- const LS_KEY = `viz:storeQuery:${storeNorm}`;
- const savedQ = String(localStorage.getItem(LS_KEY) || "");
- if (savedQ) $q.value = savedQ;
+ // Keep store-page filter consistent with the app's saved query (optional).
+ $q.value = loadSavedQuery() || "";
- // Persist max price per store (clamped later once bounds known)
- const LS_MAX_PRICE = `viz:storeMaxPrice:${storeNorm}`;
- const savedMaxPriceRaw = localStorage.getItem(LS_MAX_PRICE);
- let savedMaxPrice =
- savedMaxPriceRaw !== null ? Number(savedMaxPriceRaw) : null;
- if (!Number.isFinite(savedMaxPrice)) savedMaxPrice = null;
+ const SORT_KEY = "viz:storeRightSort";
+ $rightSort.value = sessionStorage.getItem(SORT_KEY) || "time";
- // Persist exclusives sort per store
- const LS_EX_SORT = `viz:storeExclusiveSort:${storeNorm}`;
- const savedExSort = String(localStorage.getItem(LS_EX_SORT) || "");
- if (savedExSort) $exSort.value = savedExSort;
+ let indexReady = false;
- // Persist comparison technique per store
- const LS_CMP_MODE = `viz:storeCompareMode:${storeNorm}`;
- const savedCmpMode = String(localStorage.getItem(LS_CMP_MODE) || "");
- if (savedCmpMode) $cmpMode.value = savedCmpMode;
+ let STORE_LABEL = String(storeLabelParam || "").trim();
+ let CANON = (x) => x;
- $resultsExclusive.innerHTML = `
Loading…
`;
- $resultsCompare.innerHTML = ``;
+ // canonicalSku -> agg
+ let aggBySku = new Map();
+ // canonicalSku -> storeLabel -> { priceNum, priceStr, url }
+ let PRICE_BY_SKU_STORE = new Map();
+ // canonicalSku -> storeLabel -> url (for badge href)
+ let URL_BY_SKU_STORE = new Map();
- const idx = await loadIndex();
- rulesCache = await loadSkuRules();
- const rules = rulesCache;
+ // canonicalSku -> storeLabel -> most recent recent-row (within 7d)
+ let RECENT_BY_SKU_STORE = new Map();
- const listingsAll = Array.isArray(idx.items) ? idx.items : [];
- const liveAll = listingsAll.filter((r) => r && !r.removed);
+ // For left list: canonicalSku -> best row for this store (cheapest priceNum; tie -> newest)
+ let STORE_ROW_BY_SKU = new Map();
- function dateMsFromRow(r) {
- const t = String(r?.firstSeenAt || "");
+ // Derived right-side lists
+ let exclusives = [];
+ let lastStock = [];
+ let storeItems = []; // left list items
+
+ function normStoreLabel(s) {
+ return String(s || "").trim().toLowerCase();
+ }
+
+ function resolveStoreLabel(listings, wanted) {
+ const w = normStoreLabel(wanted);
+ if (!w) return "";
+ for (const r of Array.isArray(listings) ? listings : []) {
+ const lab = String(r?.storeLabel || r?.store || "").trim();
+ if (lab && normStoreLabel(lab) === w) return lab;
+ }
+ return wanted;
+ }
+
+ function tsValue(r) {
+ const t = String(r?.ts || "");
const ms = t ? Date.parse(t) : NaN;
- return Number.isFinite(ms) ? ms : null;
+ if (Number.isFinite(ms)) return ms;
+ const d = String(r?.date || "");
+ const ms2 = d ? Date.parse(d) : NaN;
+ return Number.isFinite(ms2) ? ms2 : 0;
}
- // Build earliest "first in DB (for this store)" timestamp per canonical SKU (includes removed rows)
- const firstSeenBySkuInStore = new Map(); // sku -> ms
- for (const r of listingsAll) {
- if (!r) continue;
- const store = normStoreLabel(r.storeLabel || r.store || "");
- if (store !== storeNorm) continue;
+ function eventMs(r) {
+ const t = String(r?.ts || "");
+ const ms = t ? Date.parse(t) : NaN;
+ if (Number.isFinite(ms)) return ms;
- const skuKey = keySkuForRow(r);
- const sku = String(rules.canonicalSku(skuKey) || skuKey);
-
- const ms = dateMsFromRow(r);
- if (ms === null) continue;
-
- const prev = firstSeenBySkuInStore.get(sku);
- if (prev === undefined || ms < prev) firstSeenBySkuInStore.set(sku, ms);
+ const d = String(r?.date || "");
+ const ms2 = d ? Date.parse(d + "T00:00:00Z") : NaN;
+ return Number.isFinite(ms2) ? ms2 : 0;
}
- // 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;
+ function buildUrlMap(listings, canonicalSkuFn) {
+ const out = new Map();
+ for (const r of Array.isArray(listings) ? listings : []) {
+ if (!r || r.removed) continue;
- const skuKey = keySkuForRow(r);
- const sku = String(rules.canonicalSku(skuKey) || skuKey);
+ const skuKey = String(keySkuForRow(r) || "").trim();
+ if (!skuKey) continue;
- let ss = everStoresBySku.get(sku);
- if (!ss) everStoresBySku.set(sku, (ss = new Set()));
- ss.add(store);
- }
+ const sku = String(canonicalSkuFn ? canonicalSkuFn(skuKey) : skuKey).trim();
+ if (!sku) continue;
- // 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)
+ const storeLabel = String(r.storeLabel || r.store || "").trim();
+ const url = String(r.url || "").trim();
+ if (!storeLabel || !url) continue;
- for (const r of liveAll) {
- const store = normStoreLabel(r.storeLabel || r.store || "");
- if (!store) continue;
-
- const skuKey = keySkuForRow(r);
- const sku = String(rules.canonicalSku(skuKey) || skuKey);
-
- let ss = storesBySku.get(sku);
- if (!ss) storesBySku.set(sku, (ss = new Set()));
- ss.add(store);
-
- const p = parsePriceToNumber(r.price);
- if (p !== null) {
- let m = minPriceBySkuStore.get(sku);
- if (!m) minPriceBySkuStore.set(sku, (m = new Map()));
- const prev = m.get(store);
- if (prev === undefined || p < prev) m.set(store, p);
+ let m = out.get(sku);
+ if (!m) out.set(sku, (m = new Map()));
+ if (!m.has(storeLabel)) m.set(storeLabel, url);
}
+ return out;
}
- function bestAllPrice(sku) {
- const m = minPriceBySkuStore.get(sku);
- if (!m) return null;
- let best = null;
- for (const v of m.values()) best = best === null ? v : Math.min(best, v);
- return best;
+ function urlForSkuStore(sku, storeLabel) {
+ const s = String(sku || "");
+ const lab = String(storeLabel || "");
+ return URL_BY_SKU_STORE.get(s)?.get(lab) || "";
}
- function bestOtherPrice(sku, store) {
- const m = minPriceBySkuStore.get(sku);
- if (!m) return null;
- let best = null;
- for (const [k, v] of m.entries()) {
- if (k === store) continue;
- best = best === null ? v : Math.min(best, v);
+ function buildPriceMap(listings, canonicalSkuFn) {
+ const out = new Map();
+ for (const r of Array.isArray(listings) ? listings : []) {
+ if (!r || r.removed) continue;
+
+ const rawSku = String(keySkuForRow(r) || "").trim();
+ if (!rawSku) continue;
+
+ const sku = String(canonicalSkuFn ? canonicalSkuFn(rawSku) : rawSku).trim();
+ if (!sku) continue;
+
+ const storeLabel = String(r.storeLabel || r.store || "").trim();
+ if (!storeLabel) continue;
+
+ const priceStr = String(r.price || "").trim();
+ const priceNum = parsePriceToNumber(priceStr);
+ if (!Number.isFinite(priceNum)) continue;
+
+ const url = String(r.url || "").trim();
+
+ let m = out.get(sku);
+ if (!m) out.set(sku, (m = new Map()));
+
+ const prev = m.get(storeLabel);
+ if (!prev || priceNum < prev.priceNum) {
+ m.set(storeLabel, { priceNum, priceStr, url });
+ }
}
- return best;
+ return out;
}
- // Store-specific live rows only (in-stock for that store)
- const rowsStoreLive = liveAll.filter(
- (r) => normStoreLabel(r.storeLabel || r.store || "") === storeNorm
- );
+ function pickBestStoreRowForSku(listings, canonicalSkuFn, storeLabel) {
+ const out = new Map();
+ const want = normStoreLabel(storeLabel);
- // Aggregate in this store, grouped by canonical SKU (so mappings count as same bottle)
- let items = aggregateBySku(rowsStoreLive, rules.canonicalSku);
+ for (const r of Array.isArray(listings) ? listings : []) {
+ if (!r || r.removed) continue;
- // Decorate each item with pricing comparisons + exclusivity
- const EPS = 0.01;
+ const lab = String(r.storeLabel || r.store || "").trim();
+ if (!lab || normStoreLabel(lab) !== want) continue;
- items = items.map((it) => {
- const sku = String(it.sku || "");
- const liveStoreSet = storesBySku.get(sku) || new Set([storeNorm]);
- const everStoreSet = everStoresBySku.get(sku) || liveStoreSet;
+ const rawSku = String(keySkuForRow(r) || "").trim();
+ if (!rawSku) continue;
- const soloLiveHere =
- liveStoreSet.size === 1 && liveStoreSet.has(storeNorm);
- const lastStock = soloLiveHere && everStoreSet.size > 1;
- const exclusive = soloLiveHere && !lastStock;
+ const sku = String(canonicalSkuFn ? canonicalSkuFn(rawSku) : rawSku).trim();
+ if (!sku) continue;
- const storePrice = Number.isFinite(it.cheapestPriceNum)
- ? it.cheapestPriceNum
- : null;
- const bestAll = bestAllPrice(sku);
- const other = bestOtherPrice(sku, storeNorm);
+ const priceStr = String(r.price || "").trim();
+ const priceNum = parsePriceToNumber(priceStr);
+ const ms = tsValue(r);
- const isBest =
- storePrice !== null && bestAll !== null
- ? storePrice <= bestAll + EPS
- : false;
+ const prev = out.get(sku);
+ if (!prev) {
+ out.set(sku, { r, priceNum, ms });
+ continue;
+ }
- const diffVsOtherDollar =
- storePrice !== null && other !== null ? storePrice - other : null;
- const diffVsOtherPct =
- storePrice !== null && other !== null && other > 0
- ? ((storePrice - other) / other) * 100
- : null;
+ const prevPrice = prev.priceNum;
+ const prevMs = prev.ms;
- const diffVsBestDollar =
- storePrice !== null && bestAll !== null ? storePrice - bestAll : null;
- const diffVsBestPct =
- storePrice !== null && bestAll !== null && bestAll > 0
- ? ((storePrice - bestAll) / bestAll) * 100
- : null;
+ const priceOk = Number.isFinite(priceNum);
+ const prevOk = Number.isFinite(prevPrice);
- const firstSeenMs = firstSeenBySkuInStore.get(sku);
- const firstSeen = firstSeenMs !== undefined ? firstSeenMs : null;
+ if (priceOk && !prevOk) out.set(sku, { r, priceNum, ms });
+ else if (priceOk && prevOk && priceNum < prevPrice) out.set(sku, { r, priceNum, ms });
+ else if (
+ (priceOk && prevOk && Math.abs(priceNum - prevPrice) <= 0.01 && ms > prevMs) ||
+ (!priceOk && !prevOk && ms > prevMs)
+ ) {
+ out.set(sku, { r, priceNum, ms });
+ }
+ }
+
+ return out;
+ }
+
+ function buildRecentBySkuStore(recentItems, canonicalSkuFn, days) {
+ const nowMs = Date.now();
+ const cutoffMs = nowMs - days * 24 * 60 * 60 * 1000;
+
+ // sku -> storeLabel -> row
+ const out = new Map();
+
+ for (const r of Array.isArray(recentItems) ? recentItems : []) {
+ const ms = eventMs(r);
+ if (!(ms >= cutoffMs && ms <= nowMs)) continue;
+
+ const rawSku = String(r?.sku || "").trim();
+ if (!rawSku) continue;
+
+ const sku = String(canonicalSkuFn ? canonicalSkuFn(rawSku) : rawSku).trim();
+ if (!sku) continue;
+
+ const storeLabel = String(r?.storeLabel || r?.store || "").trim();
+ if (!storeLabel) continue;
+
+ let storeMap = out.get(sku);
+ if (!storeMap) out.set(sku, (storeMap = new Map()));
+
+ const prev = storeMap.get(storeLabel);
+ if (!prev || eventMs(prev) < ms) storeMap.set(storeLabel, r);
+ }
+
+ return out;
+ }
+
+ function normalizeKindForPrice(r) {
+ let kind = String(r?.kind || "");
+ if (kind === "price_change") {
+ const o = parsePriceToNumber(r?.oldPrice || "");
+ const n = parsePriceToNumber(r?.newPrice || "");
+ if (Number.isFinite(o) && Number.isFinite(n)) {
+ if (n < o) kind = "price_down";
+ else if (n > o) kind = "price_up";
+ else kind = "price_change";
+ }
+ }
+ return kind;
+ }
+
+ function salePctOff(oldRaw, newRaw) {
+ const oldN = parsePriceToNumber(oldRaw);
+ const newN = parsePriceToNumber(newRaw);
+ if (!Number.isFinite(oldN) || !Number.isFinite(newN)) return null;
+ if (!(oldN > 0)) return null;
+ if (!(newN < oldN)) return null;
+ const pct = Math.round(((oldN - newN) / oldN) * 100);
+ return Number.isFinite(pct) && pct > 0 ? pct : null;
+ }
+
+ function pctChange(oldRaw, newRaw) {
+ const oldN = parsePriceToNumber(oldRaw);
+ const newN = parsePriceToNumber(newRaw);
+ if (!Number.isFinite(oldN) || !Number.isFinite(newN)) return null;
+ if (!(oldN > 0)) return null;
+ const pct = Math.round(((newN - oldN) / oldN) * 100);
+ return Number.isFinite(pct) ? pct : null;
+ }
+
+ function fmtUsd(n) {
+ const abs = Math.abs(Number(n) || 0);
+ if (!Number.isFinite(abs)) return "";
+ const s = abs.toFixed(2);
+ return s.endsWith(".00") ? s.slice(0, -3) : s;
+ }
+
+ function recentSaleMetaForSkuStore(sku, storeLabel) {
+ const r = RECENT_BY_SKU_STORE.get(String(sku || ""))?.get(String(storeLabel || ""));
+ if (!r) return null;
+
+ const kind = normalizeKindForPrice(r);
+ if (kind !== "price_down" && kind !== "price_up" && kind !== "price_change") return null;
+
+ const oldStr = String(r?.oldPrice || "").trim();
+ const newStr = String(r?.newPrice || "").trim();
+ const oldN = parsePriceToNumber(oldStr);
+ const newN = parsePriceToNumber(newStr);
+
+ if (!Number.isFinite(oldN) || !Number.isFinite(newN) || !(oldN > 0)) return null;
+
+ // signedPct: down => negative; up => positive; unchanged => 0
+ let signedPct = 0;
+ let signedDelta = newN - oldN; // down => negative
+
+ if (newN < oldN) {
+ const off = salePctOff(oldStr, newStr);
+ signedPct = off !== null ? -off : Math.round(((newN - oldN) / oldN) * 100);
+ } else if (newN > oldN) {
+ const up = pctChange(oldStr, newStr);
+ signedPct = up !== null ? up : Math.round(((newN - oldN) / oldN) * 100);
+ } else {
+ signedPct = 0;
+ signedDelta = 0;
+ }
+
+ const when = r.ts ? prettyTs(r.ts) : r.date || "";
return {
- ...it,
- _exclusive: exclusive,
- _lastStock: lastStock,
- _storePrice: storePrice,
- _bestAll: bestAll,
- _bestOther: other,
- _isBest: isBest,
- _diffVsOtherDollar: diffVsOtherDollar,
- _diffVsOtherPct: diffVsOtherPct,
- _diffVsBestDollar: diffVsBestDollar,
- _diffVsBestPct: diffVsBestPct,
- _firstSeenMs: firstSeen,
+ r,
+ kind,
+ oldStr,
+ newStr,
+ oldN,
+ newN,
+ signedPct,
+ signedDelta,
+ when,
};
- });
+ }
- // ---- Max price slider (exponential mapping + clicky rounding) ----
- const MIN_PRICE = 25;
+ function pctOffVsNextBest(sku, storeLabel) {
+ const m = PRICE_BY_SKU_STORE.get(String(sku || ""));
+ if (!m) return null;
- function maxStorePriceOnPage() {
- let mx = null;
- for (const it of items) {
- const p = it && Number.isFinite(it._storePrice) ? it._storePrice : null;
- if (p === null) continue;
- mx = mx === null ? p : Math.max(mx, p);
+ const here = m.get(String(storeLabel || ""));
+ if (!here || !Number.isFinite(here.priceNum)) return null;
+
+ const prices = [];
+ for (const v of m.values()) {
+ if (Number.isFinite(v?.priceNum)) prices.push(v.priceNum);
}
- return mx;
- }
+ prices.sort((a, b) => a - b);
- const pageMax = maxStorePriceOnPage();
- const boundMax = pageMax !== null ? Math.max(MIN_PRICE, pageMax) : MIN_PRICE;
+ if (!prices.length) return null;
- function stepForPrice(p) {
- const x = Number.isFinite(p) ? p : boundMax;
- if (x < 120) return 5;
- if (x < 250) return 10;
- if (x < 600) return 25;
- return 100;
- }
- function roundToStep(p) {
- const step = stepForPrice(p);
- return Math.round(p / step) * step;
- }
+ const EPS = 0.01;
+ const min = prices[0];
+ if (Math.abs(here.priceNum - min) > EPS) return null;
- function priceFromT(t) {
- t = Math.max(0, Math.min(1, t));
- if (boundMax <= MIN_PRICE) return MIN_PRICE;
- const ratio = boundMax / MIN_PRICE;
- return MIN_PRICE * Math.exp(Math.log(ratio) * t);
- }
- function tFromPrice(price) {
- if (!Number.isFinite(price)) return 1;
- if (boundMax <= MIN_PRICE) return 1;
- const p = Math.max(MIN_PRICE, Math.min(boundMax, price));
- const ratio = boundMax / MIN_PRICE;
- return Math.log(p / MIN_PRICE) / Math.log(ratio);
- }
-
- function clampPrice(p) {
- if (!Number.isFinite(p)) return boundMax;
- return Math.max(MIN_PRICE, Math.min(boundMax, p));
- }
-
- function clampAndRound(p) {
- const c = clampPrice(p);
- const r = roundToStep(c);
- return clampPrice(r);
- }
-
- function formatDollars(p) {
- if (!Number.isFinite(p)) return "";
- return `$${Math.round(p)}`;
- }
-
- let selectedMaxPrice = clampAndRound(
- savedMaxPrice !== null ? savedMaxPrice : boundMax
- );
-
- function setSliderFromPrice(p) {
- const t = tFromPrice(p);
- const v = Math.round(t * 1000);
- $maxPrice.value = String(v);
- }
-
- function getRawPriceFromSlider() {
- const v = Number($maxPrice.value);
- const t = Number.isFinite(v) ? v / 1000 : 1;
- return clampPrice(priceFromT(t));
- }
-
- function updateMaxPriceLabel() {
- if (pageMax === null) {
- $maxPriceLabel.textContent = "No prices";
- return;
- }
- $maxPriceLabel.textContent = `${formatDollars(selectedMaxPrice)}`;
- }
-
- if (pageMax === null) {
- $maxPrice.disabled = true;
- $priceWrap.title = "No priced items in this store.";
- selectedMaxPrice = boundMax;
- setSliderFromPrice(boundMax);
- localStorage.setItem(LS_MAX_PRICE, String(selectedMaxPrice));
- updateMaxPriceLabel();
- } else {
- selectedMaxPrice = clampAndRound(selectedMaxPrice);
- localStorage.setItem(LS_MAX_PRICE, String(selectedMaxPrice));
- setSliderFromPrice(selectedMaxPrice);
- updateMaxPriceLabel();
- }
-
- // ---- Listing display price: keep cents (no rounding) ----
- function listingPriceStr(it) {
- const p = it && Number.isFinite(it._storePrice) ? it._storePrice : null;
- if (p === null)
- return it.cheapestPriceStr ? it.cheapestPriceStr : "(no price)";
- return `$${p.toFixed(2)}`;
- }
-
- function compareMode() {
- return $cmpMode && $cmpMode.value === "percent" ? "percent" : "dollar";
- }
-
- function priceBadgeHtml(it) {
- if (it._exclusive || it._lastStock) return "";
-
- const mode = compareMode();
-
- if (mode === "percent") {
- const d = it._diffVsOtherPct;
- if (d === null || !Number.isFinite(d)) return "";
- const abs = Math.abs(d);
- if (abs <= 5) {
- return `
within 5%`;
+ // find second distinct
+ let second = null;
+ for (let i = 1; i < prices.length; i++) {
+ if (Math.abs(prices[i] - min) > EPS) {
+ second = prices[i];
+ break;
}
- const pct = Math.round(abs);
- if (d < 0) return `
${esc(pct)}% lower`;
- return `
${esc(pct)}% higher`;
}
+ if (!Number.isFinite(second) || !(second > 0) || !(min < second)) return null;
- const d = it._diffVsOtherDollar;
- if (d === null || !Number.isFinite(d)) return "";
-
- const abs = Math.abs(d);
- if (abs <= 5) {
- return `
within $5`;
- }
-
- const dollars = Math.round(abs);
- if (d < 0) {
- return `
$${esc(dollars)} lower`;
- }
- return `
$${esc(dollars)} higher`;
+ const pct = Math.round(((second - min) / second) * 100);
+ return Number.isFinite(pct) && pct > 0 ? pct : null;
}
- function renderCard(it) {
- const price = listingPriceStr(it);
- const href = String(it.sampleUrl || "").trim();
+ function badgeHtmlForSale(sortMode, saleMeta) {
+ if (!saleMeta) return "";
+ if (sortMode === "sale_pct") {
+ const v = saleMeta.signedPct;
+ if (!Number.isFinite(v) || v === 0) return "";
+ const isDown = v < 0;
+ const abs = Math.abs(v);
+ const style = isDown
+ ? ` style="margin-left:6px; color:rgba(20,110,40,0.95); background:rgba(20,110,40,0.10); border:1px solid rgba(20,110,40,0.20);"`
+ : ` style="margin-left:6px; color:rgba(170,40,40,0.95); background:rgba(170,40,40,0.10); border:1px solid rgba(170,40,40,0.20);"`;
+ const txt = isDown ? `[${abs}% Off]` : `[+${abs}%]`;
+ return `
${esc(txt)}`;
+ }
- const specialBadge = it._lastStock
- ? `
Last Stock`
- : it._exclusive
- ? `
Exclusive`
- : "";
+ if (sortMode === "sale_abs") {
+ const d = saleMeta.signedDelta;
+ if (!Number.isFinite(d) || d === 0) return "";
+ const isDown = d < 0;
+ const abs = fmtUsd(d);
+ if (!abs) return "";
+ const style = isDown
+ ? ` style="margin-left:6px; color:rgba(20,110,40,0.95); background:rgba(20,110,40,0.10); border:1px solid rgba(20,110,40,0.20);"`
+ : ` style="margin-left:6px; color:rgba(170,40,40,0.95); background:rgba(170,40,40,0.10); border:1px solid rgba(170,40,40,0.20);"`;
+ const txt = isDown ? `[$${abs} Off]` : `[+$${abs}]`;
+ return `
${esc(txt)}`;
+ }
- const bestBadge =
- !it._exclusive && !it._lastStock && it._isBest
- ? `
Best Price`
- : "";
+ return "";
+ }
- const diffBadge = priceBadgeHtml(it);
+ function badgeHtmlForExclusivePctOff(sku) {
+ const pct = pctOffVsNextBest(sku, STORE_LABEL);
+ if (!Number.isFinite(pct) || pct <= 0) return "";
+ return `
[${esc(
+ pct
+ )}% Off]`;
+ }
+
+ function itemCardHtml(it, { annotateMode }) {
+ // annotateMode: "sale_pct" | "sale_abs" | "default"
+ const sku = String(it?.sku || "");
+ const name = String(it?.name || "(no name)");
+ const img = String(it?.img || "");
+ const priceStr = it.priceStr ? it.priceStr : "(no price)";
+
+ const href = urlForSkuStore(sku, STORE_LABEL) || String(it?.url || "").trim();
+ const storeBadge = href
+ ? `
${esc(
+ STORE_LABEL
+ )}`
+ : `
${esc(STORE_LABEL)}`;
+
+ const skuLink = `#/link/?left=${encodeURIComponent(sku)}`;
+
+ let annot = "";
+ if (annotateMode === "sale_pct" || annotateMode === "sale_abs") {
+ annot = badgeHtmlForSale(annotateMode, it.saleMeta);
+ } else {
+ // default annotation is % off for exclusives only (and only if >0)
+ if (it.isExclusive) annot = badgeHtmlForExclusivePctOff(sku);
+ }
- const skuLink = `#/link/?left=${encodeURIComponent(String(it.sku || ""))}`;
return `
-
+
-
${renderThumbHtml(it.img)}
+
+ ${renderThumbHtml(img)}
+
@@ -543,242 +471,300 @@ export async function renderStore($app, storeLabelRaw) {
`;
}
- // ---- Infinite scroll paging (shared across both columns) ----
- const PAGE_SIZE = 140;
- const PAGE_EACH = Math.max(1, Math.floor(PAGE_SIZE / 2));
-
- let filteredExclusive = [];
- let filteredCompare = [];
- let shownExclusive = 0;
- let shownCompare = 0;
-
- function totalFiltered() {
- return filteredExclusive.length + filteredCompare.length;
- }
- function totalShown() {
- return shownExclusive + shownCompare;
- }
-
- function setStatus() {
- const total = totalFiltered();
- if (!total) {
- $status.textContent = "No in-stock items for this store.";
+ function renderList($el, items, annotateMode) {
+ if (!items.length) {
+ $el.innerHTML = `
No matches.
`;
return;
}
- if (pageMax !== null) {
- $status.textContent = `In stock: ${total} item(s) (≤ ${formatDollars(
- selectedMaxPrice
- )}).`;
- return;
- }
+ const limited = items.slice(0, 80);
+ $el.innerHTML = limited.map((it) => itemCardHtml(it, { annotateMode })).join("");
- $status.textContent = `In stock: ${total} item(s).`;
- }
-
- function renderNext(reset) {
- if (reset) {
- $resultsExclusive.innerHTML = "";
- $resultsCompare.innerHTML = "";
- shownExclusive = 0;
- shownCompare = 0;
- }
-
- const sliceEx = filteredExclusive.slice(
- shownExclusive,
- shownExclusive + PAGE_EACH
- );
- const sliceCo = filteredCompare.slice(
- shownCompare,
- shownCompare + PAGE_EACH
- );
-
- shownExclusive += sliceEx.length;
- shownCompare += sliceCo.length;
-
- if (sliceEx.length)
- $resultsExclusive.insertAdjacentHTML(
- "beforeend",
- sliceEx.map(renderCard).join("")
- );
- if (sliceCo.length)
- $resultsCompare.insertAdjacentHTML(
- "beforeend",
- sliceCo.map(renderCard).join("")
- );
-
- const total = totalFiltered();
- const shown = totalShown();
-
- if (!total) {
- $sentinel.textContent = "";
- } else if (shown >= total) {
- $sentinel.textContent = `Showing ${shown} / ${total}`;
- } else {
- $sentinel.textContent = `Showing ${shown} / ${total}…`;
- }
- }
-
- $resultsWrap.addEventListener("click", (e) => {
- const el = e.target.closest(".item");
- if (!el) return;
- const sku = el.getAttribute("data-sku") || "";
- if (!sku) return;
- sessionStorage.setItem("viz:lastRoute", location.hash);
- location.hash = `#/item/${encodeURIComponent(sku)}`;
- });
-
- function sortExclusiveInPlace(arr) {
- const mode = String($exSort.value || "priceDesc");
- if (mode === "priceAsc" || mode === "priceDesc") {
- arr.sort((a, b) => {
- const ap = Number.isFinite(a._storePrice) ? a._storePrice : null;
- const bp = Number.isFinite(b._storePrice) ? b._storePrice : null;
- const aKey =
- ap === null ? (mode === "priceAsc" ? 9e15 : -9e15) : ap;
- const bKey =
- bp === null ? (mode === "priceAsc" ? 9e15 : -9e15) : bp;
- if (aKey !== bKey) return mode === "priceAsc" ? aKey - bKey : bKey - aKey;
- return (String(a.name) + a.sku).localeCompare(String(b.name) + b.sku);
- });
- return;
- }
-
- if (mode === "dateAsc" || mode === "dateDesc") {
- arr.sort((a, b) => {
- const ad = Number.isFinite(a._firstSeenMs) ? a._firstSeenMs : null;
- const bd = Number.isFinite(b._firstSeenMs) ? b._firstSeenMs : null;
- const aKey =
- ad === null ? (mode === "dateAsc" ? 9e15 : -9e15) : ad;
- const bKey =
- bd === null ? (mode === "dateAsc" ? 9e15 : -9e15) : bd;
- if (aKey !== bKey) return mode === "dateAsc" ? aKey - bKey : bKey - aKey;
- return (String(a.name) + a.sku).localeCompare(String(b.name) + b.sku);
+ for (const el of Array.from($el.querySelectorAll(".item"))) {
+ el.addEventListener("click", () => {
+ const sku = el.getAttribute("data-sku") || "";
+ if (!sku) return;
+ saveQuery($q.value);
+ sessionStorage.setItem("viz:lastRoute", location.hash);
+ location.hash = `#/item/${encodeURIComponent(sku)}`;
});
}
}
- function sortCompareInPlace(arr) {
- const mode = compareMode();
- arr.sort((a, b) => {
- const da = mode === "percent" ? a._diffVsOtherPct : a._diffVsOtherDollar;
- const db = mode === "percent" ? b._diffVsOtherPct : b._diffVsOtherDollar;
+ function renderStoreCatalog(items) {
+ if (!items.length) {
+ $storeResults.innerHTML = `
No matches.
`;
+ return;
+ }
- const sa = da === null || !Number.isFinite(da) ? 999999 : da;
- const sb = db === null || !Number.isFinite(db) ? 999999 : db;
- if (sa !== sb) return sa - sb;
+ const limited = items.slice(0, 160);
+ $storeResults.innerHTML = limited
+ .map((it) => {
+ const sku = String(it?.sku || "");
+ const href = urlForSkuStore(sku, STORE_LABEL) || String(it?.url || "").trim();
+ const storeBadge = href
+ ? `
${esc(
+ STORE_LABEL
+ )}`
+ : `
${esc(STORE_LABEL)}`;
- return (String(a.name) + a.sku).localeCompare(String(b.name) + b.sku);
+ const skuLink = `#/link/?left=${encodeURIComponent(sku)}`;
+
+ return `
+
+
+
+ ${renderThumbHtml(it.img)}
+
+
+
+
+ ${esc(it.priceStr || "(no price)")}
+ ${storeBadge}
+
+
+
+
+ `;
+ })
+ .join("");
+
+ for (const el of Array.from($storeResults.querySelectorAll(".item"))) {
+ el.addEventListener("click", () => {
+ const sku = el.getAttribute("data-sku") || "";
+ if (!sku) return;
+ saveQuery($q.value);
+ sessionStorage.setItem("viz:lastRoute", location.hash);
+ location.hash = `#/item/${encodeURIComponent(sku)}`;
+ });
+ }
+ }
+
+ function sortRightList(items, mode) {
+ const m = String(mode || "time");
+
+ const getTime = (it) => Number(it?.timeMs || 0);
+ const getPrice = (it) => (Number.isFinite(it?.priceNum) ? it.priceNum : Number.POSITIVE_INFINITY);
+
+ const getSignedPct = (it) => {
+ const v = it?.saleMeta?.signedPct;
+ return Number.isFinite(v) ? v : 0; // unchanged/no-event => 0 (middle)
+ };
+
+ const getSignedDelta = (it) => {
+ const v = it?.saleMeta?.signedDelta;
+ return Number.isFinite(v) ? v : 0; // unchanged/no-event => 0 (middle)
+ };
+
+ const out = items.slice();
+
+ if (m === "sale_pct") {
+ out.sort((a, b) => {
+ // deals (negative) first; unchanged (0) middle; increases (+) last
+ const av = getSignedPct(a);
+ const bv = getSignedPct(b);
+ if (av !== bv) return av - bv;
+ const ap = getPrice(a);
+ const bp = getPrice(b);
+ if (ap !== bp) return ap - bp;
+ return String(a.sku || "").localeCompare(String(b.sku || ""));
+ });
+ return out;
+ }
+
+ if (m === "sale_abs") {
+ out.sort((a, b) => {
+ const av = getSignedDelta(a);
+ const bv = getSignedDelta(b);
+ if (av !== bv) return av - bv; // negative (down) first, positive (up) last
+ const ap = getPrice(a);
+ const bp = getPrice(b);
+ if (ap !== bp) return ap - bp;
+ return String(a.sku || "").localeCompare(String(b.sku || ""));
+ });
+ return out;
+ }
+
+ if (m === "price") {
+ out.sort((a, b) => {
+ const ap = getPrice(a);
+ const bp = getPrice(b);
+ if (ap !== bp) return ap - bp;
+ return String(a.sku || "").localeCompare(String(b.sku || ""));
+ });
+ return out;
+ }
+
+ // time (default): newest first
+ out.sort((a, b) => {
+ const at = getTime(a);
+ const bt = getTime(b);
+ if (bt !== at) return bt - at;
+ return String(a.sku || "").localeCompare(String(b.sku || ""));
});
+ return out;
}
- function applyFilter() {
- const raw = String($q.value || "");
- localStorage.setItem(LS_KEY, raw);
+ function rebuildDerivedLists() {
+ const tokens = tokenizeQuery($q.value);
- const tokens = tokenizeQuery(raw);
+ const filteredStoreItems = !tokens.length
+ ? storeItems
+ : storeItems.filter((it) => matchesAllTokens(it.searchText, tokens));
- let base = items;
+ renderStoreCatalog(filteredStoreItems);
- if (tokens.length) {
- base = base.filter((it) => matchesAllTokens(it.searchText, tokens));
- }
+ const rightSortMode = String($rightSort.value || "time");
+ const annotMode =
+ rightSortMode === "sale_pct"
+ ? "sale_pct"
+ : rightSortMode === "sale_abs"
+ ? "sale_abs"
+ : "default";
- if (pageMax !== null && Number.isFinite(selectedMaxPrice)) {
- const cap = selectedMaxPrice + 0.0001;
- base = base.filter((it) => {
- const p = it && Number.isFinite(it._storePrice) ? it._storePrice : null;
- return p === null ? true : p <= cap;
- });
- }
+ const ex = !tokens.length
+ ? exclusives
+ : exclusives.filter((it) => matchesAllTokens(it.searchText, tokens));
- filteredExclusive = base.filter((it) => it._exclusive || it._lastStock);
- filteredCompare = base.filter((it) => !it._exclusive && !it._lastStock);
+ const ls = !tokens.length
+ ? lastStock
+ : lastStock.filter((it) => matchesAllTokens(it.searchText, tokens));
- sortExclusiveInPlace(filteredExclusive);
- sortCompareInPlace(filteredCompare);
-
- setStatus();
- renderNext(true);
+ renderList($exclusiveResults, sortRightList(ex, rightSortMode), annotMode);
+ renderList($lastStockResults, sortRightList(ls, rightSortMode), annotMode);
}
- applyFilter();
+ $storeResults.innerHTML = `
Loading…
`;
+ $exclusiveResults.innerHTML = `
Loading…
`;
+ $lastStockResults.innerHTML = `
Loading…
`;
- const io = new IntersectionObserver(
- (entries) => {
- const hit = entries.some((x) => x.isIntersecting);
- if (!hit) return;
- if (totalShown() >= totalFiltered()) return;
- renderNext(false);
- },
- { root: null, rootMargin: "600px 0px", threshold: 0.01 }
- );
- io.observe($sentinel);
+ Promise.all([loadIndex(), loadSkuRules(), loadRecent()])
+ .then(([idx, rules, recent]) => {
+ const listings = Array.isArray(idx?.items) ? idx.items : [];
- let t = null;
- $q.addEventListener("input", () => {
- if (t) clearTimeout(t);
- t = setTimeout(applyFilter, 60);
- });
+ CANON = typeof rules?.canonicalSku === "function" ? rules.canonicalSku : (x) => x;
+
+ STORE_LABEL = resolveStoreLabel(listings, STORE_LABEL);
+ $storeSub.textContent = STORE_LABEL ? `Browsing: ${STORE_LABEL}` : `Browsing store`;
+
+ // Global aggregates (for "exclusive" / "last stock" determination)
+ const allAgg = aggregateBySku(listings, CANON);
+ aggBySku = new Map(allAgg.map((x) => [String(x.sku || ""), x]));
+
+ URL_BY_SKU_STORE = buildUrlMap(listings, CANON);
+ PRICE_BY_SKU_STORE = buildPriceMap(listings, CANON);
+
+ // Store rows
+ STORE_ROW_BY_SKU = pickBestStoreRowForSku(listings, CANON, STORE_LABEL);
+
+ // Recent (7 days)
+ const recentItems = Array.isArray(recent?.items) ? recent.items : [];
+ RECENT_BY_SKU_STORE = buildRecentBySkuStore(recentItems, CANON, 7);
+
+ // Build store item objects
+ storeItems = [];
+ exclusives = [];
+ lastStock = [];
+
+ for (const [sku, best] of STORE_ROW_BY_SKU.entries()) {
+ const r = best?.r || null;
+ if (!r) continue;
+
+ const global = aggBySku.get(String(sku || "")) || null;
+ const globalStoreCount = global?.stores?.size || 0;
+
+ const storePriceStr = String(r.price || "").trim();
+ const storePriceNum = parsePriceToNumber(storePriceStr);
+
+ const saleMeta = recentSaleMetaForSkuStore(sku, STORE_LABEL);
+
+ const searchText = String(
+ [
+ r.name || global?.name || "",
+ r.url || "",
+ sku,
+ STORE_LABEL,
+ ].join(" ")
+ ).toLowerCase();
+
+ const it = {
+ sku: String(sku || ""),
+ name: r.name || global?.name || "",
+ img: global?.img || r.img || "",
+ url: r.url || "",
+ priceStr: storePriceStr,
+ priceNum: storePriceNum,
+ timeMs: tsValue(r),
+ searchText,
+ saleMeta,
+ isExclusive: false,
+ isLastStock: false,
+ globalStoreCount,
+ };
+
+ // Determine exclusives / last stock
+ const EPS = 0.01;
+ const bestGlobalNum = Number.isFinite(global?.cheapestPriceNum) ? global.cheapestPriceNum : null;
+ const storeIsCheapest =
+ Number.isFinite(storePriceNum) && Number.isFinite(bestGlobalNum)
+ ? Math.abs(storePriceNum - bestGlobalNum) <= EPS
+ : false;
+
+ if (globalStoreCount <= 1) {
+ it.isLastStock = true;
+ lastStock.push(it);
+ } else if (storeIsCheapest) {
+ it.isExclusive = true;
+ exclusives.push(it);
+ }
+
+ storeItems.push(it);
+ }
+
+ // Default sort for store catalog: by name
+ storeItems.sort((a, b) => String(a.name || "").localeCompare(String(b.name || "")));
+ indexReady = true;
+ $q.focus();
+ rebuildDerivedLists();
+ })
+ .catch((e) => {
+ const msg = `Failed to load: ${esc(e?.message || String(e))}`;
+ $storeResults.innerHTML = `
${msg}
`;
+ $exclusiveResults.innerHTML = `
${msg}
`;
+ $lastStockResults.innerHTML = `
${msg}
`;
+ });
$clearSearch.addEventListener("click", () => {
- let changed = false;
-
if ($q.value) {
$q.value = "";
- localStorage.setItem(LS_KEY, "");
- changed = true;
+ saveQuery("");
+ rebuildDerivedLists();
}
-
- // reset max price too (only if slider is active)
- if (pageMax !== null) {
- selectedMaxPrice = clampAndRound(boundMax);
- localStorage.setItem(LS_MAX_PRICE, String(selectedMaxPrice));
- setSliderFromPrice(selectedMaxPrice);
- updateMaxPriceLabel();
- changed = true;
- }
-
- if (changed) applyFilter();
$q.focus();
});
- $exSort.addEventListener("change", () => {
- localStorage.setItem(LS_EX_SORT, String($exSort.value || ""));
- applyFilter();
+ let t = null;
+ $q.addEventListener("input", () => {
+ saveQuery($q.value);
+ if (t) clearTimeout(t);
+ t = setTimeout(() => {
+ if (!indexReady) return;
+ rebuildDerivedLists();
+ }, 50);
});
- $cmpMode.addEventListener("change", () => {
- localStorage.setItem(LS_CMP_MODE, String($cmpMode.value || ""));
- applyFilter();
- });
-
- let tp = null;
- function setSelectedMaxPriceFromSlider() {
- const raw = getRawPriceFromSlider();
- const rounded = clampAndRound(raw);
- if (Math.abs(rounded - selectedMaxPrice) > 0.001) {
- selectedMaxPrice = rounded;
- localStorage.setItem(LS_MAX_PRICE, String(selectedMaxPrice));
- updateMaxPriceLabel();
- } else {
- updateMaxPriceLabel();
- }
- }
-
- $maxPrice.addEventListener("input", () => {
- if (pageMax === null) return;
- setSelectedMaxPriceFromSlider();
-
- if (tp) clearTimeout(tp);
- tp = setTimeout(applyFilter, 40);
- });
-
- $maxPrice.addEventListener("change", () => {
- if (pageMax === null) return;
- setSelectedMaxPriceFromSlider();
- setSliderFromPrice(selectedMaxPrice);
- updateMaxPriceLabel();
- applyFilter();
+ $rightSort.addEventListener("change", () => {
+ sessionStorage.setItem(SORT_KEY, String($rightSort.value || "time"));
+ if (!indexReady) return;
+ rebuildDerivedLists();
});
}