← back to Small Business Builder
src/render/wall.js
282 lines
// /wall — public-facing brutalist gallery of every project that's been
// audited + mockuped. Lives at livesiteaudit.agentabrams.com/wall (host
// allowlisted in src/server/index.js). Filters out compliance_sensitive rows.
//
// Cards display: brutalist mockup mini-iframe, grade badge, name+vertical,
// qwen3 roast (if generated), and a link to /wa/:slug.
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({
'&':'&','<':'<','>':'>','"':'"',"'":'''
}[c]));
function gradeColor(score) {
if (!Number.isFinite(score)) return '#a3a3a3';
return score >= 80 ? '#22c55e' : score >= 65 ? '#84cc16' : score >= 45 ? '#facc15' : score >= 25 ? '#f97316' : '#dc2626';
}
const FONTS = `
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Space+Mono:wght@400;700&display=swap">
`;
const HEAD = (title, ogDesc) => `<!doctype html>
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(title)}</title>
<meta name="description" content="${esc(ogDesc)}">
<meta property="og:title" content="${esc(title)}">
<meta property="og:description" content="${esc(ogDesc)}">
<meta property="og:type" content="website">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><rect width='64' height='64' fill='%23000'/><text x='32' y='44' font-family='Archivo Black' font-size='40' fill='%23facc15' text-anchor='middle'>★</text></svg>">
${FONTS}
<style>
:root{--bg:#fafaf6;--ink:#111;--paper:#fff;--accent:#facc15;--muted:#666;--soft:#fef3c7;--green-bg:#f0fdf4;--red-bg:#fef2f2}
[data-theme="dark"]{--bg:#0a0a08;--ink:#fafaf6;--paper:#1a1a18;--accent:#facc15;--muted:#999;--soft:#1f1f1d;--green-bg:#0a2014;--red-bg:#1f0a0a}
*{box-sizing:border-box}
body{margin:0;font-family:'Space Mono',monospace;background:var(--bg);color:var(--ink);transition:background .15s,color .15s}
body .card{background:var(--paper);color:var(--ink)}
body .vert-row{background:var(--paper)}
body .filter,body .pager a,body .pager span{background:var(--paper);color:var(--ink)}
body .preview{background:var(--bg)}
body .body{background:var(--paper);color:var(--ink)}
.theme-toggle{background:var(--paper);color:var(--ink);border:3px solid var(--ink);padding:8px 12px;font-family:'Archivo Black',sans-serif;font-size:12px;cursor:pointer;text-transform:uppercase;letter-spacing:.04em}
.theme-toggle:hover{background:var(--accent);color:#000}
a{color:inherit}
h1,h2,h3{font-family:'Archivo Black',sans-serif;letter-spacing:-.02em;text-transform:uppercase;margin:0}
.container{max-width:1480px;margin:0 auto;padding:0 24px}
.marquee{overflow:hidden;background:#000;color:#facc15;border-bottom:4px solid #facc15;padding:12px 0;font-family:'Space Mono',monospace;font-size:14px}
.marquee-track{display:inline-block;white-space:nowrap;animation:scroll 90s linear infinite}
.marquee-track span{padding:0 32px;display:inline-block;border-right:2px solid #facc15;font-weight:700}
@keyframes scroll{from{transform:translateX(0)}to{transform:translateX(-50%)}}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:20px}
.card{border:4px solid #000;background:#fff;display:flex;flex-direction:column;text-decoration:none;color:inherit;transition:transform .08s}
.card:hover{transform:translate(-3px,-3px);box-shadow:6px 6px 0 0 #000}
.preview{aspect-ratio:4/3;position:relative;overflow:hidden;background:#fafaf6;border-bottom:4px solid #000}
.preview iframe{position:absolute;inset:0;width:200%;height:200%;border:0;transform:scale(.5);transform-origin:0 0;pointer-events:none}
.badge{position:absolute;top:8px;right:8px;width:48px;height:48px;display:flex;align-items:center;justify-content:center;font-family:'Archivo Black',sans-serif;font-size:24px;border:3px solid #000;background:#facc15}
.body{padding:14px 16px;flex:1;display:flex;flex-direction:column;gap:10px}
.meta{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#666}
.name{font-family:'Archivo Black',sans-serif;font-size:18px;text-transform:uppercase;line-height:1;letter-spacing:-.02em}
.roast{font-size:13px;line-height:1.4;color:#222;font-style:italic;border-top:2px solid #000;padding-top:10px;margin-top:auto}
.roast::before{content:"★ ";color:#facc15;-webkit-text-stroke:1px #000;font-weight:700;font-style:normal}
.pager{display:flex;gap:14px;align-items:center;justify-content:center;padding:48px 0;font-family:'Archivo Black',sans-serif;text-transform:uppercase;font-size:14px;letter-spacing:.04em}
.pager a, .pager span{padding:12px 22px;border:4px solid #000;text-decoration:none;color:#000;background:#fff}
.pager a:hover{background:#facc15}
.pager .cur{background:#000;color:#facc15}
.nav{display:flex;justify-content:space-between;align-items:center;padding:24px 0;border-bottom:4px solid #000}
.nav a{text-decoration:none;font-family:'Archivo Black',sans-serif;text-transform:uppercase;letter-spacing:.04em}
.filters{display:flex;gap:10px;flex-wrap:wrap;padding:18px 0;border-bottom:4px solid #000;font-family:'Space Mono',monospace;font-size:12px;font-weight:700}
.filter{padding:8px 14px;border:3px solid #000;background:#fff;text-decoration:none;color:#000;text-transform:uppercase;letter-spacing:.04em}
.filter.cur{background:#000;color:#facc15}
.filter:hover{background:#facc15}
.hero{padding:80px 0 48px;border-bottom:4px solid #000;position:relative;background:linear-gradient(135deg,#fafaf6 0%,#fafaf6 50%,#facc15 50%,#facc15 100%);background-size:24px 24px}
.hero h1{font-size:clamp(48px,9vw,128px);line-height:.85;color:#000}
.hero h1 mark{background:#facc15;padding:0 .15em;color:#000}
.hero p{font-size:18px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#444;margin-top:18px;max-width:60ch}
.cta{display:inline-block;background:#000;color:#facc15;padding:18px 32px;font-family:'Archivo Black',sans-serif;text-decoration:none;text-transform:uppercase;letter-spacing:.04em;border:4px solid #000;font-size:18px;margin-top:32px}
.cta:hover{background:#facc15;color:#000}
</style>
</head><body>`;
const FOOT = `
<footer style="border-top:4px solid #000;background:#000;color:#fafaf6;padding:48px 24px;margin-top:48px">
<div class="container">
<div style="display:flex;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;gap:24px">
<div>
<div style="font-family:'Archivo Black',sans-serif;font-size:32px;text-transform:uppercase;color:#facc15">★ LIVE SITE AUDIT</div>
<div style="font-size:12px;font-weight:700;color:#999;margin-top:4px;letter-spacing:.06em">EVERY SITE GETS THE BRUTAL TREATMENT.</div>
</div>
<div style="display:flex;gap:24px;flex-wrap:wrap">
<a href="/website-analysis" style="color:#facc15;text-decoration:none;font-family:'Archivo Black',sans-serif;font-size:14px;text-transform:uppercase;letter-spacing:.04em">★ AUDIT YOUR SITE →</a>
<a href="/wall" style="color:#fafaf6;text-decoration:none;font-family:'Archivo Black',sans-serif;font-size:14px;text-transform:uppercase;letter-spacing:.04em">★ THE WALL</a>
</div>
</div>
<div style="font-size:11px;color:#666;margin-top:24px;font-family:'Space Mono',monospace">© 2026 · made with cheerio + qwen3:14b · roasts may sting · contact info@agentabrams.com</div>
</div>
</footer>
</body></html>`;
export function renderWall({ rows, page = 1, perPage = 24, totalCount, vertical = 'all', verticals = [], roastsForMarquee = [], stats = null }) {
const pages = Math.max(1, Math.ceil(totalCount / perPage));
const cards = rows.map(r => {
const live = r.summary_json?.live_audit;
const liveLetter = live?.grade?.letter;
const liveScore = live?.grade?.score;
const cp = r.summary_json?.copy_pack;
const headline = cp?.headline || r.summary_json?.title || r.meta_json?.title || r.slug;
const vert = r.vertical && r.vertical !== 'generic' ? r.vertical : null;
const roast = r.summary_json?.roast?.text;
// Find a suitable mockup id — prefer brutalist if present, else first.
const mockupSrc = r.preview_mockup_id ? `/wa/${esc(r.slug)}/m/${esc(r.preview_mockup_id)}` : null;
const searchBlob = [r.slug, vert || '', liveLetter || '', headline, roast || ''].join(' ').toLowerCase();
return `<a class="card" data-search="${esc(searchBlob)}" href="/wa/${esc(r.slug)}" target="_blank" rel="noopener">
<div class="preview">
${mockupSrc ? `<iframe src="${mockupSrc}" loading="lazy" sandbox="allow-same-origin"></iframe>` : `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-family:'Archivo Black',sans-serif;color:#999;font-size:14px;text-transform:uppercase">★ NO PREVIEW</div>`}
${liveLetter ? `<div class="badge" style="background:${gradeColor(liveScore)};">${esc(liveLetter)}</div>` : ''}
</div>
<div class="body">
<div class="meta">${vert ? `★ ${esc(vert.toUpperCase())}` : '★ PROJECT'}${liveScore != null ? ` · LIVE ${esc(String(liveScore))}/100` : ''}</div>
<div class="name">${esc(headline.slice(0, 64))}</div>
${roast ? `<div class="roast">${esc(roast.slice(0, 200))}</div>` : ''}
</div>
</a>`;
}).join('');
const verticalChips = ['all', ...verticals].map(v => {
const cur = v === vertical;
const href = v === 'all' ? '/wall' : `/wall?v=${encodeURIComponent(v)}`;
return `<a class="filter${cur ? ' cur' : ''}" href="${esc(href)}">${v === 'all' ? '★ ALL' : esc(v.toUpperCase())}</a>`;
}).join('');
const pagerLinks = (() => {
if (pages <= 1) return '';
const link = (n, label) => n === page
? `<span class="cur">${esc(label || String(n))}</span>`
: `<a href="/wall?p=${n}${vertical !== 'all' ? `&v=${encodeURIComponent(vertical)}` : ''}">${esc(label || String(n))}</a>`;
const out = [];
if (page > 1) out.push(link(page - 1, '← PREV'));
const start = Math.max(1, page - 2);
const end = Math.min(pages, page + 2);
for (let n = start; n <= end; n += 1) out.push(link(n));
if (page < pages) out.push(link(page + 1, 'NEXT →'));
return `<div class="pager">${out.join('')}</div>`;
})();
const marquee = roastsForMarquee.length
? `<div class="marquee"><div class="marquee-track">${
[...roastsForMarquee, ...roastsForMarquee].map(r => `<span>★ ${esc((r.text || '').slice(0, 130))}</span>`).join('')
}</div></div>`
: '';
const title = `★ THE WALL · ${totalCount} BUILDS UNDER THE MICROSCOPE`;
const desc = `A brutalist gallery of every site we've audited and rebuilt. Live HTML grades, qwen3-generated roasts, and three new direction mockups for each.`;
// JSON-LD structured data — collection page with each card as ItemListElement.
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: title,
description: desc,
url: 'https://livesiteaudit.agentabrams.com/wall',
mainEntity: {
'@type': 'ItemList',
numberOfItems: totalCount,
itemListElement: rows.slice(0, 24).map((r, i) => ({
'@type': 'ListItem',
position: (page - 1) * perPage + i + 1,
url: `https://livesiteaudit.agentabrams.com/wa/${r.slug}`,
name: r.summary_json?.copy_pack?.headline || r.slug,
})),
},
};
return HEAD(title, desc) + `
<script type="application/ld+json">${JSON.stringify(jsonLd)}</script>
<script>(function(){try{var t=localStorage.getItem('lsa-theme');if(t==='dark')document.documentElement.setAttribute('data-theme','dark')}catch(e){}})()</script>
<nav class="nav container">
<a href="/" style="font-size:18px">★ LIVE SITE AUDIT</a>
<div style="display:flex;gap:14px;align-items:center;flex-wrap:wrap">
<a href="/grades" style="font-size:13px">★ LEADERBOARD</a>
<button class="theme-toggle" id="theme-toggle" title="toggle dark/light">★ <span id="theme-icon">☾</span></button>
<a href="/website-analysis" style="font-size:13px;background:#facc15;border:4px solid #000;padding:10px 18px">★ AUDIT YOUR SITE</a>
</div>
</nav>
${marquee}
<header class="hero">
<div class="container">
<h1>${totalCount}<br>BUILDS<br>UNDER THE<br><mark>MICROSCOPE.</mark></h1>
<p>Every project we've audited, scored, mocked-up, and roasted. Live HTML grades from a fresh fetch. Three rebuild mockups per project. No filters, no flattery.</p>
<a class="cta" href="/website-analysis">★ ROAST MY SITE →</a>
</div>
</header>
${stats ? `<section style="background:#000;color:#facc15;border-bottom:4px solid #000">
<div class="container" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:0;font-family:'Archivo Black',sans-serif">
<div style="padding:22px 18px;border-right:2px solid #facc15">
<div style="font-size:38px;line-height:1">${esc(String(stats.total))}</div>
<div style="font-size:11px;color:#fafaf6;letter-spacing:.06em;margin-top:4px">★ TOTAL BUILDS</div>
</div>
<div style="padding:22px 18px;border-right:2px solid #facc15">
<div style="font-size:38px;line-height:1">${esc(String(stats.graded))}</div>
<div style="font-size:11px;color:#fafaf6;letter-spacing:.06em;margin-top:4px">★ LIVE-AUDITED</div>
</div>
<div style="padding:22px 18px;border-right:2px solid #facc15">
<div style="font-size:38px;line-height:1">${esc(String(stats.roasted))}</div>
<div style="font-size:11px;color:#fafaf6;letter-spacing:.06em;margin-top:4px">★ ROASTED BY QWEN3</div>
</div>
<div style="padding:22px 18px;border-right:2px solid #facc15">
<div style="font-size:38px;line-height:1">${esc(String(stats.mockups))}</div>
<div style="font-size:11px;color:#fafaf6;letter-spacing:.06em;margin-top:4px">★ MOCKUPS RENDERED</div>
</div>
<div style="padding:22px 18px">
<div style="font-size:38px;line-height:1">${esc(stats.avgGrade)}</div>
<div style="font-size:11px;color:#fafaf6;letter-spacing:.06em;margin-top:4px">★ AVG LIVE GRADE</div>
</div>
</div>
</section>` : ''}
<section class="container" style="padding:32px 0 16px">
<div style="display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:18px">
<div style="flex:1;min-width:240px;display:flex;border:4px solid #000;background:#fff">
<span style="background:#000;color:#facc15;padding:14px 16px;font-family:'Archivo Black',sans-serif;font-size:14px;letter-spacing:.04em">★ FIND</span>
<input id="wall-search" type="text" placeholder="filter by name, vertical, grade…" autocomplete="off"
style="flex:1;padding:14px 16px;border:0;font-family:'Space Mono',monospace;font-size:14px;font-weight:700;background:#fff;outline:none">
<button id="wall-clear" type="button" title="clear" style="background:#fff;color:#666;border:0;border-left:3px solid #000;padding:0 16px;cursor:pointer;font-family:'Archivo Black',sans-serif;font-size:18px">×</button>
</div>
<div id="wall-count" style="font-family:'Space Mono',monospace;font-size:12px;font-weight:700;color:#666;letter-spacing:.04em;text-transform:uppercase">★ ${rows.length} OF ${totalCount} ON THIS PAGE</div>
</div>
<div class="filters">${verticalChips}</div>
</section>
<main class="container">
<div class="grid">${cards}</div>
${pagerLinks}
</main>
<script>
(function(){
const input = document.getElementById('wall-search');
const clear = document.getElementById('wall-clear');
const count = document.getElementById('wall-count');
const cards = Array.from(document.querySelectorAll('.card'));
const totalShown = cards.length;
function apply(q) {
q = (q || '').trim().toLowerCase();
if (!q) {
cards.forEach(c => { c.style.display = ''; });
count.textContent = '★ ' + totalShown + ' OF ${totalCount} ON THIS PAGE';
return;
}
let n = 0;
for (const c of cards) {
const ok = (c.dataset.search || '').includes(q);
c.style.display = ok ? '' : 'none';
if (ok) n += 1;
}
count.textContent = '★ ' + n + ' MATCH' + (n === 1 ? '' : 'ES') + ' OF ' + totalShown;
}
input?.addEventListener('input', e => apply(e.target.value));
clear?.addEventListener('click', () => { input.value = ''; apply(''); input.focus(); });
// Dark/light toggle (per memory: feedback_dark_light_toggle_standard.md)
const tt = document.getElementById('theme-toggle');
const icon = document.getElementById('theme-icon');
function reflectTheme() {
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
if (icon) icon.textContent = dark ? '☀' : '☾';
}
reflectTheme();
tt?.addEventListener('click', () => {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
if (isDark) { document.documentElement.removeAttribute('data-theme'); localStorage.setItem('lsa-theme','light'); }
else { document.documentElement.setAttribute('data-theme','dark'); localStorage.setItem('lsa-theme','dark'); }
reflectTheme();
});
})();
</script>
` + FOOT;
}