← back to Corvette Dashboard Viewer

public/new.html

130 lines

<!DOCTYPE html>
<html lang="en">
<head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
  var _fetch = window.fetch.bind(window);
  var cleanBase = function () { return location.origin + location.pathname; };
  window.fetch = function (input, init) {
    try {
      if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
        input = new URL(input, cleanBase()).href;
      } else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
        input = new Request(new URL(input.url, cleanBase()).href, input);
      }
    } catch (_) { /* fall through to native */ }
    return _fetch(input, init);
  };
})();
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Arrivals — Live · TK-135</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Racing+Sans+One&family=Oswald:wght@300;500;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
:root{--red:#C8102E;--red-lit:#ff2d4d;--amber:#FFB300;--cream:#e9e4d6;--green:#39d353}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Oswald',sans-serif;color:var(--cream);
  background:radial-gradient(120% 80% at 50% -10%,#2a2b2f,#141416 45%,#050506);min-height:100vh;padding:20px 14px 60px;-webkit-font-smoothing:antialiased}
.wrap{max-width:1080px;margin:0 auto}
header{display:flex;align-items:center;justify-content:space-between;gap:14px;
  background:linear-gradient(180deg,#1c1c1e,#0c0c0d);border:1px solid #2a2a2d;border-radius:16px;
  padding:16px 22px;box-shadow:inset 0 2px 0 rgba(255,255,255,.05),0 20px 50px rgba(0,0,0,.7);position:sticky;top:10px;z-index:5}
.brand{display:flex;align-items:center;gap:12px}
.brand .h1{font-family:'Racing Sans One',cursive;font-size:22px;letter-spacing:1px;
  background:linear-gradient(180deg,#f7f7f9,#b8bcc4 50%,#6e7178);-webkit-background-clip:text;background-clip:text;color:transparent}
.live{display:flex;align-items:center;gap:7px;font-family:'Share Tech Mono',monospace;font-size:12px;letter-spacing:2px;color:var(--green)}
.dot{width:9px;height:9px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:pulse 1.4s infinite}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.7)}}
.stats{text-align:right;font-family:'Share Tech Mono',monospace}
.stats .big{font-size:26px;color:var(--amber);text-shadow:0 0 12px rgba(255,179,0,.5);line-height:1}
.stats .lbl{font-size:9px;letter-spacing:3px;color:#8b8f98}
.sub{text-align:center;font-size:10px;letter-spacing:4px;color:#7d818b;margin:12px 0 16px}
.list{display:flex;flex-direction:column;gap:9px}
.row{display:grid;grid-template-columns:30px 56px 1fr 200px;gap:13px;align-items:center;
  background:linear-gradient(180deg,#1a1a1c,#101011);border:1px solid #2c2c31;border-radius:12px;
  padding:11px 14px;box-shadow:0 5px 14px rgba(0,0,0,.45);transition:box-shadow .15s}
.thumb{width:56px;height:56px;border-radius:8px;object-fit:cover;background:#222;border:1px solid #333;box-shadow:0 2px 6px rgba(0,0,0,.5)}
.row:hover{box-shadow:0 8px 20px rgba(0,0,0,.7),0 0 0 1px rgba(255,179,0,.3)}
.row.fresh{animation:land .8s ease}
@keyframes land{0%{opacity:0;transform:translateY(-12px);box-shadow:0 0 0 2px var(--amber)}100%{opacity:1;transform:none}}
.idx{font-family:'Share Tech Mono',monospace;font-size:15px;color:#5f636c;text-align:center}
.idx.top{color:var(--red-lit);text-shadow:0 0 8px rgba(255,45,77,.6)}
.main .t{font-size:15px;font-weight:500;line-height:1.15}
.main .t a{color:var(--cream);text-decoration:none;border-bottom:1px dotted #4a4a50}
.main .t a:hover{color:var(--amber);border-color:var(--amber)}
.main .meta{font-size:10px;letter-spacing:1px;color:#8b8f98;margin:3px 0 7px;text-transform:uppercase}
.chips{display:flex;flex-wrap:wrap;gap:4px}
.chip{font-size:9px;letter-spacing:.5px;padding:2px 8px;border-radius:20px;background:#26262b;border:1px solid #37373d;color:#c3c7cf}
.right{display:flex;flex-direction:column;gap:5px;align-items:flex-end}
.pal{display:flex;width:100%;height:30px;border-radius:6px;overflow:hidden;border:1px solid #000;box-shadow:inset 0 0 0 1px rgba(255,255,255,.05)}
.pal i{position:relative;display:flex;align-items:flex-end;justify-content:center}
.pal i span{font-family:'Share Tech Mono',monospace;font-size:7px;color:rgba(255,255,255,.9);text-shadow:0 1px 2px #000;opacity:0;padding-bottom:1px}
.pal:hover i span{opacity:1}
.gidl{font-family:'Share Tech Mono',monospace;font-size:9px;color:#5f636c;letter-spacing:1px}
.hint{text-align:center;color:#5f636c;font-size:11px;margin-top:20px;letter-spacing:2px}
a.open{font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--amber);text-decoration:none;letter-spacing:1px}
a.open:hover{text-shadow:0 0 8px rgba(255,179,0,.6)}
</style>
</head>
<body>
<div class="wrap">
  <header>
    <div class="brand">
      <span style="font-size:22px">🏁</span>
      <div><div class="h1">NEW ARRIVALS · LIVE</div>
        <div class="live"><span class="dot"></span><span id="clock">— streaming TK-135 —</span></div></div>
    </div>
    <div class="stats"><div class="big" id="processed">0</div><div class="lbl">SKUS UPDATED</div></div>
  </header>
  <div class="sub">N E W E S T &nbsp; C O L O R - N O R M A L I Z E D &nbsp; I T E M S &nbsp; · &nbsp; T O P &nbsp; 5 0 &nbsp; · &nbsp; A U T O - R E F R E S H</div>
  <div class="list" id="list"></div>
  <div class="hint" id="hint">connecting…</div>
</div>
<script>
let seen=new Set(), first=true;
function esc(s){return (s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
async function tick(){
  try{
    const r=await fetch('/api/new-items?limit=50',{cache:'no-store'});
    const d=await r.json();
    document.getElementById('processed').textContent=(d.processed||0).toLocaleString();
    document.getElementById('clock').textContent='updated '+new Date().toLocaleTimeString();
    document.getElementById('hint').textContent=d.records.length?'▲ newest at top · live feed':'waiting for items to land…';
    const list=document.getElementById('list'); list.innerHTML='';
    d.records.forEach((p,i)=>{
      const isNew=!seen.has(p.gid)&&!first;
      const total=p.palette.reduce((a,c)=>a+(+c.pct||0),0)||1;
      const pal=p.palette.length?p.palette.map(c=>`<i style="flex:${c.pct};background:${c.hex}"><span>${c.hex}</span></i>`).join(''):'<i style="flex:1;background:#222"></i>';
      const title=p.href?`<a href="${p.href}" target="_blank">${esc(p.title)}</a>`:esc(p.title);
      const chips=(p.tags||[]).map(t=>`<span class="chip">${esc(t)}</span>`).join('');
      const row=document.createElement('div');
      row.className='row'+(isNew?' fresh':'');
      const thumb=p.image?`<img class="thumb" src="${p.image}&width=112" loading="lazy" alt="">`:`<div class="thumb"></div>`;
      row.innerHTML=`<div class="idx ${i<3?'top':''}">${i+1}</div>
        ${p.href?`<a href="${p.href}" target="_blank">${thumb}</a>`:thumb}
        <div class="main"><div class="t">${title}</div>
          <div class="meta">${esc(p.vendor)||'—'} · ${esc(p.sku)||'no sku'} · ${p.palette.length} colors</div>
          <div class="chips">${chips}</div></div>
        <div class="right"><div class="pal">${pal}</div>
          <div class="gidl">#${p.gid}</div>
          ${p.href?`<a class="open" href="${p.href}" target="_blank">OPEN PAGE ▸</a>`:''}</div>`;
      list.appendChild(row);
    });
    seen=new Set(d.records.map(p=>p.gid)); first=false;
  }catch(e){document.getElementById('hint').textContent='feed error: '+e.message;}
}
tick(); setInterval(tick,4000);
</script>
</body>
</html>