← back to Sku Check Skill
public/pills.html
231 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Live Check · Pills</title>
<link rel="icon" href="/favicon.svg">
<style>
:root{
--bg:#0f1115; --card:#171a21; --ink:#e8eaed; --muted:#9aa3af;
--lime:#84CC16; --lime-ink:#0f1115; --line:#242833;
--ok:#22c55e; --warn:#f59e0b; --bad:#ef4444; --info:#38bdf8;
}
*{box-sizing:border-box}
body{
margin:0; background:radial-gradient(1200px 600px at 50% -10%, #1b2130 0%, var(--bg) 60%);
color:var(--ink); font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
min-height:100vh; padding:32px 16px;
}
.wrap{max-width:620px; margin:0 auto}
h1{font-size:22px; font-weight:650; margin:0 0 4px; letter-spacing:-.01em}
.sub{color:var(--muted); margin:0 0 24px; font-size:13px}
.card{background:var(--card); border:1px solid var(--line); border-radius:22px; padding:22px}
/* ---- pill input ---- */
.field{display:flex; gap:10px; align-items:center}
input[type=text]{
flex:1; background:#0f131a; color:var(--ink); border:1px solid var(--line);
border-radius:999px; padding:14px 20px; font-size:16px; outline:none; transition:border .15s;
}
input[type=text]:focus{border-color:var(--lime)}
input::placeholder{color:#5b6472}
/* ---- pill checkboxes ---- */
.pills{display:flex; gap:10px; margin:16px 0 4px; flex-wrap:wrap}
.pill{
display:inline-flex; align-items:center; gap:8px; cursor:pointer; user-select:none;
background:#0f131a; border:1px solid var(--line); color:var(--muted);
padding:9px 16px; border-radius:999px; font-size:14px; font-weight:550; transition:all .15s;
}
.pill input{display:none}
.pill .dot{width:9px; height:9px; border-radius:50%; background:#3a4150; transition:background .15s}
.pill.on{background:rgba(132,204,22,.14); border-color:var(--lime); color:var(--lime)}
.pill.on .dot{background:var(--lime)}
/* ---- go button pill ---- */
button.go{
width:100%; margin-top:16px; background:var(--lime); color:var(--lime-ink);
border:none; border-radius:999px; padding:14px; font-size:16px; font-weight:700;
cursor:pointer; transition:filter .15s, opacity .15s;
}
button.go:hover{filter:brightness(1.06)}
button.go:disabled{opacity:.5; cursor:progress}
/* ---- results ---- */
.out{margin-top:20px; display:none}
.out.show{display:block}
.chip{
display:inline-flex; align-items:center; gap:6px; background:#0f131a; border:1px solid var(--line);
border-radius:999px; padding:6px 13px; font-size:13px; margin:0 6px 8px 0; color:var(--ink);
}
.chip b{color:var(--muted); font-weight:550}
.kv{display:flex; flex-wrap:wrap; margin-top:6px}
.sect{margin-top:18px; padding-top:16px; border-top:1px solid var(--line)}
.sect h3{margin:0 0 10px; font-size:12px; text-transform:uppercase; letter-spacing:.08em; color:var(--muted); font-weight:650}
.val{font-size:18px; font-weight:650}
.price-row{display:flex; gap:10px; flex-wrap:wrap}
.price-box{flex:1; min-width:120px; background:#0f131a; border:1px solid var(--line); border-radius:16px; padding:12px 14px}
.price-box .lbl{font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)}
.banner{border-radius:14px; padding:11px 15px; font-size:13.5px; margin-top:10px; border:1px solid}
.banner.info{background:rgba(56,189,248,.10); border-color:rgba(56,189,248,.4); color:#bae6fd}
.banner.warn{background:rgba(245,158,11,.10); border-color:rgba(245,158,11,.4); color:#fde68a}
.banner.bad{background:rgba(239,68,68,.10); border-color:rgba(239,68,68,.4); color:#fecaca}
.banner.ok{background:rgba(34,197,94,.10); border-color:rgba(34,197,94,.4); color:#bbf7d0}
.live-list{margin:8px 0 0; padding-left:18px}
.live-list li{margin:2px 0; font-size:13.5px}
.err{color:var(--bad); font-size:14px; margin-top:14px}
a{color:var(--lime)}
.spin{display:inline-block; width:14px; height:14px; border:2px solid rgba(15,17,21,.35); border-top-color:var(--lime-ink); border-radius:50%; animation:spin .7s linear infinite; vertical-align:-2px; margin-right:8px}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
</head>
<body>
<div class="wrap">
<h1>Live Check</h1>
<p class="sub">Enter a manufacturer number <em>or</em> a DW number, tick what to pull, check it live.</p>
<div class="card">
<div class="field">
<input id="q" type="text" placeholder="e.g. GGA-82668 or 437-RD336" autocomplete="off" autofocus>
</div>
<div class="pills">
<label class="pill on" id="pill-stock"><input type="checkbox" id="stock" checked><span class="dot"></span>Stock</label>
<label class="pill on" id="pill-price"><input type="checkbox" id="price" checked><span class="dot"></span>Price</label>
</div>
<button class="go" id="go">Check Live</button>
<div class="out" id="out"></div>
</div>
</div>
<script>
// pill toggle visuals
for (const key of ['stock','price']) {
const cb = document.getElementById(key);
const pill = document.getElementById('pill-'+key);
pill.addEventListener('click', () => setTimeout(() => pill.classList.toggle('on', cb.checked), 0));
}
const $ = id => document.getElementById(id);
const out = $('out');
const esc = s => String(s ?? '').replace(/[<>&]/g, c => ({'<':'<','>':'>','&':'&'}[c]));
const money = v => (v==null||v==='') ? '—' : (isNaN(+v) ? esc(v) : '$'+(+v).toFixed(2));
// Structured trade price + stock from the portal parse.
function parsedBoxes(P){
if(!P) return '';
let h='<div class="price-row">';
if(P.msrp!=null) h+='<div class="price-box"><div class="lbl">Retail (MSRP)</div><div class="val">'+money(P.msrp)+'</div><div class="lbl">'+esc(P.unit||'')+'</div></div>';
if(P.netPrice!=null) h+='<div class="price-box"><div class="lbl">Net / Trade'+(P.discountPct!=null?' ('+P.discountPct+'% off)':'')+'</div><div class="val">'+money(P.netPrice)+'</div><div class="lbl">'+esc(P.netUnit||'')+'</div></div>';
if(P.quantityAvailable!=null) h+='<div class="price-box"><div class="lbl">In Stock</div><div class="val">'+P.quantityAvailable+'</div><div class="lbl">'+esc(P.netUnit||P.unit||'rolls')+'</div></div>';
h+='</div>';
if(P.rollWidth||P.rollLength){
h+='<div class="kv" style="margin-top:8px">';
if(P.rollWidth) h+='<span class="chip"><b>Width</b> '+esc(P.rollWidth)+'</span>';
if(P.rollLength) h+='<span class="chip"><b>Length</b> '+esc(P.rollLength)+'</span>';
h+='</div>';
}
return h;
}
async function run() {
const query = $('q').value.trim();
if (!query) { $('q').focus(); return; }
const stock = $('stock').checked, price = $('price').checked;
$('go').disabled = true;
$('go').innerHTML = '<span class="spin"></span>Checking…';
out.className = 'out show';
out.innerHTML = '<p class="sub" style="margin:0">Resolving & pulling…</p>';
try {
const r = await fetch('/api/check/live', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ query, stock, price })
});
const d = await r.json();
render(d);
} catch (e) {
out.innerHTML = '<p class="err">Request failed: '+esc(e.message)+'</p>';
} finally {
$('go').disabled = false;
$('go').textContent = 'Check Live';
}
}
function render(d) {
if (d.error) { out.innerHTML = '<p class="err">'+esc(d.error)+'</p>'; return; }
const rz = d.resolved || {};
let h = '<div class="kv">';
h += '<span class="chip"><b>'+ (rz.matchedBy==='mfr'?'MFR':'DW') +' →</b> '+esc(rz.dw_sku||'')+'</span>';
h += '<span class="chip"><b>MFR</b> '+esc(rz.mfr_sku||'')+'</span>';
h += '<span class="chip"><b>Vendor</b> '+esc(rz.vendor||'')+'</span>';
h += '</div>';
// cached prices
if (d.cached) {
const c = d.cached;
h += '<div class="sect"><h3>Cached (catalog)</h3><div class="price-row">';
h += '<div class="price-box"><div class="lbl">Retail</div><div class="val">'+money(c.retailPrice)+'</div></div>';
h += '<div class="price-box"><div class="lbl">Trade</div><div class="val">'+money(c.tradePrice)+'</div></div>';
h += '<div class="price-box"><div class="lbl">Cost</div><div class="val">'+money(c.costPrice)+'</div></div>';
h += '</div>';
if (c.productUrl) h += '<div style="margin-top:10px"><a href="'+esc(c.productUrl)+'" target="_blank" rel="noopener">Vendor product page ↗</a></div>';
h += '</div>';
}
// last portal pull (from overnight job / prior pull) — skip if a fresh live succeeded
const freshLiveOk = d.live && d.live.loggedIn;
if (d.lastLive && !freshLiveOk) {
const L = d.lastLive;
const age = L.ageMin < 60 ? L.ageMin+'m ago' : Math.round(L.ageMin/60)+'h ago';
h += '<div class="sect"><h3>Last portal pull · '+age+'</h3>';
if (L.loggedIn && (L.parsed || (L.priceStock && L.priceStock.length))) {
h += '<div class="banner ok">Saved from the trade portal at '+esc(new Date(L.at).toLocaleString())+'</div>';
if (L.parsed) h += parsedBoxes(L.parsed);
else h += '<ul class="live-list">'+ L.priceStock.map(x=>'<li>'+esc(x)+'</li>').join('') +'</ul>';
} else if (L.locked) {
h += '<div class="banner bad">Last attempt hit a portal lockout ('+esc(new Date(L.at).toLocaleString())+').</div>';
} else {
h += '<div class="banner warn">Last saved pull had no price/stock text.</div>';
}
h += '</div>';
}
// live
if (d.live) {
h += '<div class="sect"><h3>Live (trade portal)</h3>';
const L = d.live;
if (L.attempted === false) {
h += '<div class="banner warn">'+esc(L.reason)+'</div>';
} else if (L.error) {
h += '<div class="banner bad">'+esc(L.message||L.error)+'</div>';
} else if (L.loggedIn) {
h += '<div class="banner ok">'+esc(L.message)+'</div>';
if (L.parsed) {
h += parsedBoxes(L.parsed);
} else if (L.priceStock && L.priceStock.length) {
h += '<ul class="live-list">'+ L.priceStock.map(x=>'<li>'+esc(x)+'</li>').join('') +'</ul>';
} else {
h += '<p class="sub" style="margin:8px 0 0">Logged in, but no price/stock text captured on the product page.</p>';
}
} else if (L.locked) {
h += '<div class="banner bad">'+esc(L.message)+'</div>';
} else {
h += '<div class="banner warn">'+esc(L.message||'No live data.')+'</div>';
}
h += '</div>';
}
out.innerHTML = h;
}
$('go').addEventListener('click', run);
$('q').addEventListener('keydown', e => { if (e.key==='Enter') run(); });
</script>
</body>
</html>