← 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>