← back to Wallco Ai

public/new.html

270 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>What's New — wallco.ai</title>
<style>
  :root {
    --ink:#1f1808; --line:#d8d0c0; --gold:#c9a14b;
    --card-min:220px;
    --bg:#fbf8f1; --card:#fff; --faint:#7a6e5a;
    --green:#1f6a2c; --blue:#3b78d8; --purple:#7a4ab8; --orange:#c0660b; --red:#b3261e;
    --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
    --serif:'Playfair Display','Didot',Georgia,serif;
  }
  * { box-sizing:border-box; }
  body { margin:0; background:var(--bg); color:var(--ink); font:14px var(--sans); }
  header {
    padding:18px 28px; border-bottom:1px solid var(--line); background:#fff;
    display:flex; gap:16px; align-items:baseline; position:sticky; top:0; z-index:5;
  }
  header h1 { font:400 26px/1.2 var(--serif); margin:0; }
  header .meta { color:var(--faint); font-size:13px; letter-spacing:.04em; }
  header .stats { margin-left:auto; display:flex; gap:18px; font:600 12px ui-monospace,Menlo,monospace; }
  header .stats b { color:var(--ink); }
  header .live {
    font:600 10.5px var(--sans); color:var(--green); letter-spacing:.1em;
    margin-left:10px;
  }
  header .live::before { content:'●'; margin-right:5px; animation:pulse 1.4s infinite; }
  @keyframes pulse { 0%,100% {opacity:1;} 50% {opacity:.3;} }

  main { max-width:1500px; margin:0 auto; padding:22px 18px 80px; }

  .controls {
    display:flex; gap:14px; align-items:center; padding:10px 14px;
    background:#fff; border:1px solid var(--line); border-radius:8px;
    margin-bottom:14px;
  }
  .controls label { font:600 10.5px var(--sans); letter-spacing:.16em;
                    text-transform:uppercase; color:var(--faint); }
  .controls select { padding:6px 10px; border:1px solid var(--line);
                     border-radius:4px; background:#fff; font:13px var(--sans); }
  .controls .right { margin-left:auto; font:600 11px ui-monospace,Menlo,monospace; color:var(--faint); }
  .controls a {
    color:var(--ink); text-decoration:none; font:600 11px var(--sans);
    letter-spacing:.08em; padding:6px 12px; border:1px solid var(--line);
    border-radius:14px; text-transform:uppercase;
  }

  .grid {
    display:grid; grid-template-columns:repeat(auto-fill,minmax(var(--card-min),1fr)); gap:14px;
  }
  .controls input[type=range] { accent-color:var(--gold); vertical-align:middle; }
  .card {
    background:var(--card); border:1px solid var(--line); border-radius:8px;
    overflow:hidden; cursor:pointer; transition:transform .12s, box-shadow .12s;
    text-decoration:none; color:inherit; display:block;
  }
  .card:hover { transform:translateY(-2px); box-shadow:0 6px 22px rgba(0,0,0,.08); }
  .thumb {
    aspect-ratio:1; background:#f3eee2; position:relative; overflow:hidden;
  }
  .thumb img { width:100%; height:100%; object-fit:cover; display:block; }
  .thumb .id {
    position:absolute; top:6px; right:6px;
    background:rgba(31,24,8,.78); color:#fff;
    font:600 9.5px ui-monospace,Menlo,monospace;
    padding:3px 7px; border-radius:3px;
  }
  .thumb .gen-pill {
    position:absolute; left:6px; bottom:6px;
    font:700 9px var(--sans); letter-spacing:.06em; text-transform:uppercase;
    padding:3px 7px; border-radius:11px; color:#fff;
  }
  .gen-smart-fix { background:#c79a3a; color:#3a2a00; }
  .gen-crop-fix { background:var(--blue); }
  .gen-regen   { background:var(--purple); }
  .gen-wand    { background:var(--green); }
  .gen-other   { background:rgba(31,24,8,.78); }

  .meta-row {
    padding:8px 10px; display:flex; flex-direction:column; gap:3px;
    font:11.5px var(--sans);
  }
  .meta-row .cat { color:var(--ink); font-weight:600; letter-spacing:.02em; }
  .meta-row .ts  { color:var(--faint); font:11px ui-monospace,Menlo,monospace; }

  .empty {
    text-align:center; padding:80px 20px; color:var(--faint);
    font:14px var(--sans);
  }
  .empty h2 { font:400 28px var(--serif); color:var(--ink); margin-bottom:10px; }
</style>
</head>
<body>

<header>
  <h1>What's New on wallco.ai</h1>
  <span class="meta">designs added or refreshed in the window</span>
  <span class="live">LIVE</span>
  <div class="stats" id="stats">
    <span>NEW <b id="stat-total">0</b></span>
    <span>SMART <b id="stat-smart">0</b></span>
    <span>FIX <b id="stat-fix">0</b></span>
    <span>WAND <b id="stat-wand">0</b></span>
    <span>REGEN <b id="stat-regen">0</b></span>
  </div>
</header>

<main>
  <div class="controls">
    <label>Window</label>
    <select id="hours">
      <option value="1">last 1h</option>
      <option value="6">last 6h</option>
      <option value="12">last 12h</option>
      <option value="24" selected>last 24h</option>
      <option value="48">last 48h</option>
      <option value="168">last 7d</option>
    </select>
    <label>Sort</label>
    <select id="sort">
      <option value="newest">Newest</option>
      <option value="oldest">Oldest</option>
      <option value="category">Category A→Z</option>
      <option value="color">Color (by hue)</option>
      <option value="id">ID ↑</option>
      <option value="id_desc">ID ↓</option>
    </select>
    <label>Density</label>
    <input type="range" id="density" min="140" max="340" step="10" title="card size">
    <span class="right" id="meta">—</span>
    <a href="/admin/ghost-review">🔍 Ghost Review →</a>
  </div>

  <div id="grid" class="grid"></div>
  <div id="empty" class="empty" style="display:none">
    <h2>No new designs in this window yet</h2>
    <p>The Seamless Agent runs continuously. Check back in a few minutes.</p>
  </div>
</main>

<script>
(() => {
  const $ = (id) => document.getElementById(id);
  let HOURS = 24;
  let LAST_TOTAL = 0;
  let ALL = [];
  $('hours').onchange = (e) => { HOURS = parseInt(e.target.value, 10); refresh(); };

  // sort + density (Steve standing rule: every product grid; both persist)
  $('sort').value = localStorage.getItem('new_sort') || 'newest';
  $('density').value = localStorage.getItem('new_density') || '220';
  document.documentElement.style.setProperty('--card-min', $('density').value + 'px');
  $('sort').onchange = () => { localStorage.setItem('new_sort', $('sort').value); render(); };
  $('density').oninput = () => {
    localStorage.setItem('new_density', $('density').value);
    document.documentElement.style.setProperty('--card-min', $('density').value + 'px');
  };

  function hexHue(hex) {
    const m = /^#?([0-9a-fA-F]{6})$/.exec(hex || ''); if (!m) return 1e6;
    const n = parseInt(m[1], 16), r = (n >> 16 & 255) / 255, g = (n >> 8 & 255) / 255, b = (n & 255) / 255;
    const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min; let h = 0;
    if (d === 0) h = 0;
    else if (max === r) h = ((g - b) / d) % 6;
    else if (max === g) h = (b - r) / d + 2;
    else h = (r - g) / d + 4;
    h *= 60; if (h < 0) h += 360;
    const s = max === 0 ? 0 : d / max;
    if (s < 0.12) return 1e5 + (1 - max);
    return h;
  }
  function sortItems(rows) {
    const r = rows.slice(), m = $('sort').value;
    if (m === 'newest') r.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
    else if (m === 'oldest') r.sort((a, b) => String(a.created_at || '').localeCompare(String(b.created_at || '')));
    else if (m === 'category') r.sort((a, b) => String(a.category || '').localeCompare(String(b.category || '')) || (b.id - a.id));
    else if (m === 'color') r.sort((a, b) => hexHue(a.dominant_hex) - hexHue(b.dominant_hex) || (b.id - a.id));
    else if (m === 'id') r.sort((a, b) => a.id - b.id);
    else if (m === 'id_desc') r.sort((a, b) => b.id - a.id);
    return r;
  }

  function genKind(gen) {
    const g = (gen || '').toLowerCase();
    if (g.includes('smart-fix') || g.includes('smart_fix')) return 'smart-fix';
    if (g.includes('wand') || g.includes('browser-edit'))   return 'wand';
    if (g.includes('regen-reverse') || g.includes('regenerate-reverse')) return 'regen';
    if (g.includes('crop-fix') || g.includes('image-edit')) return 'crop-fix';
    return 'other';
  }
  function pillLabel(k) {
    return ({ 'smart-fix':'🎯 SMART', 'crop-fix':'🪄 FIX', 'regen':'↻ REGEN', 'wand':'🪄 WAND', 'other':'NEW' })[k] || 'NEW';
  }

  async function refresh() {
    try {
      const r = await fetch(`/api/new-designs?hours=${HOURS}`);
      const j = await r.json();
      const items = j.items || [];
      if (items.length > LAST_TOTAL && LAST_TOTAL > 0) {
        document.title = `(${items.length - LAST_TOTAL} new) What's New`;
        setTimeout(() => { document.title = "What's New — wallco.ai"; }, 4000);
      }
      LAST_TOTAL = items.length;
      ALL = items;

      const counts = { 'smart-fix':0, 'crop-fix':0, 'regen':0, 'wand':0, 'other':0 };
      items.forEach(it => { counts[genKind(it.generator)]++; });
      $('stat-total').textContent = items.length;
      $('stat-smart').textContent = counts['smart-fix'];
      $('stat-fix').textContent   = counts['crop-fix'];
      $('stat-wand').textContent  = counts['wand'];
      $('stat-regen').textContent = counts['regen'];
      $('meta').textContent = `${items.length} shown · window=${HOURS}h · auto-refresh 10s`;

      render();
    } catch (e) {
      console.warn('refresh failed:', e.message);
    }
  }

  function render() {
    const grid = $('grid');
    grid.innerHTML = '';
    if (!ALL.length) { $('empty').style.display = ''; return; }
    $('empty').style.display = 'none';
    const frag = document.createDocumentFragment();
    for (const it of sortItems(ALL)) {
      const k = genKind(it.generator);
      const card = document.createElement('a');
      card.className = 'card';
      card.href = `/design/${it.id}`;
      card.target = '_blank';
      const ts = new Date(it.created_at);
      const ago = humanAgo(ts);
      card.innerHTML = `
        <div class="thumb">
          <img loading="lazy" src="/designs/img/by-id/${it.id}" alt="${it.id}">
          <div class="id">#${it.id}</div>
          <div class="gen-pill gen-${k}">${pillLabel(k)}</div>
        </div>
        <div class="meta-row">
          <span class="cat">${it.category || '—'}</span>
          <span class="ts">${ago} · ${it.dominant_hex || ''}</span>
        </div>
      `;
      frag.appendChild(card);
    }
    grid.appendChild(frag);
  }

  function humanAgo(ts) {
    const sec = Math.max(0, Math.round((Date.now() - ts.getTime()) / 1000));
    if (sec < 60)    return `${sec}s ago`;
    if (sec < 3600)  return `${Math.round(sec/60)}m ago`;
    if (sec < 86400) return `${Math.round(sec/3600)}h ago`;
    return `${Math.round(sec/86400)}d ago`;
  }

  refresh();
  setInterval(refresh, 10000);
})();
</script>
</body>
</html>