← back to 4square Agentabrams

public/wall.html

236 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Hero Wall — 4Square</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;700&family=Cormorant+Garamond:wght@400;500&family=Inter:wght@400;500;600&display=swap">
<style>
  :root {
    --bg: #0a0908;
    --panel: #14130f;
    --rule: #2a2825;
    --rule-strong: #3a3631;
    --fg: #f3ecdd;
    --muted: #9d9586;
    --muted-dim: #6e665a;
    --accent: #d9b14e;
    --accent-dim: #8a6f30;
    --font-display: 'Cormorant Garamond', 'Playfair Display', Georgia, serif;
    --font-ui: 'Inter', system-ui, sans-serif;
  }
  * { box-sizing: border-box; }
  html, body { margin: 0; min-height: 100%; background: var(--bg); color: var(--fg); font-family: var(--font-ui); -webkit-font-smoothing: antialiased; }
  header.top {
    display: flex; align-items: center; justify-content: space-between;
    padding: 14px 24px; border-bottom: 1px solid var(--rule);
    background: linear-gradient(180deg, #1c1a17 0%, #14130f 100%);
    position: sticky; top: 0; z-index: 5;
  }
  header.top h1 { font-family: 'Playfair Display', serif; margin: 0; font-size: 1.4rem; color: var(--accent); }
  header.top .sub { color: var(--muted); font-size: 0.78rem; letter-spacing: 0.06em; text-transform: uppercase; }
  header .controls { display: flex; align-items: center; gap: 10px; }
  header .meta { color: var(--muted); font-size: 0.78rem; }
  header input[type=search] {
    background: #100f0d; color: var(--fg); border: 1px solid var(--rule);
    padding: 7px 12px; border-radius: 8px; font: 0.85rem var(--font-ui); width: 220px;
  }
  header input[type=search]:focus { outline: 0; border-color: var(--accent-dim); }
  header select {
    background: #100f0d; color: var(--fg); border: 1px solid var(--rule);
    padding: 7px 10px; border-radius: 8px; font: 0.82rem var(--font-ui);
  }
  header a, header button {
    background: transparent; color: var(--fg); border: 1px solid var(--rule);
    padding: 7px 12px; border-radius: 8px; font: 600 0.78rem var(--font-ui);
    letter-spacing: 0.04em; text-decoration: none; cursor: pointer;
  }
  header a:hover, header button:hover { border-color: var(--accent); color: var(--accent); }
  /* ─ Wall grid ─ */
  .wall { padding: 18px 22px 36px; display: grid; gap: 24px; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); }
  .card {
    background: #100f0d; border: 1px solid var(--rule); border-radius: 12px;
    overflow: hidden; cursor: pointer;
    transition: border-color .15s ease, transform .15s ease, box-shadow .2s ease;
  }
  .card:hover { border-color: var(--accent-dim); transform: translateY(-1px); box-shadow: 0 8px 26px rgba(0,0,0,0.6); }
  .card-canvas {
    width: 100%; aspect-ratio: 16/9;
    display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr;
    gap: 2px; background: #1a1815;
  }
  .card-cell {
    position: relative; background: #15140f;
    background-size: cover; background-position: center 12%;
  }
  .card-cell.empty {
    display: flex; align-items: center; justify-content: center;
    color: var(--muted-dim); font: 700 0.85rem var(--font-display);
  }
  .card-cell.empty::after { content: '—'; }
  .card-body {
    padding: 12px 14px 14px; display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
    border-top: 1px solid var(--rule);
  }
  .card-wordmark {
    font-family: var(--font-display); font-size: 1.4rem; font-weight: 500;
    letter-spacing: -0.005em; line-height: 1; color: var(--fg);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    flex: 1 1 auto;
  }
  .card-kind {
    font-size: 0.65rem; color: var(--muted-dim);
    letter-spacing: 0.12em; text-transform: uppercase;
  }
  .card-domain {
    padding: 0 14px 12px; font-size: 0.74rem; color: var(--muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    display: flex; align-items: center; gap: 8px;
  }
  .card-domain .dot {
    width: 6px; height: 6px; border-radius: 999px; background: var(--accent);
    flex: 0 0 6px;
  }
  .card.missing .dot { background: #c54; }
  .card.missing .card-cell.empty::after { content: 'no hero-4grid'; font-size: 0.72rem; }
  .card-status {
    font-size: 0.65rem; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--muted-dim); margin-left: auto;
  }
  .card.missing .card-status { color: #d97c7c; }
  .card.ok .card-status { color: var(--accent); }
  .card .live {
    font-size: 0.7rem; color: var(--muted); letter-spacing: 0.04em;
    border: 1px solid var(--rule); padding: 2px 8px; border-radius: 999px;
    text-decoration: none; transition: color .15s ease, border-color .15s ease;
  }
  .card .live:hover { color: var(--accent); border-color: var(--accent-dim); }
  .loading, .empty { padding: 60px 20px; text-align: center; color: var(--muted); }
  .loading h2, .empty h2 { font-family: 'Playfair Display', serif; color: var(--fg); font-weight: 500; }
  @media (max-width: 580px) {
    .wall { grid-template-columns: 1fr; gap: 14px; padding: 12px; }
    header.top { padding: 10px 14px; flex-wrap: wrap; gap: 10px; }
    header input[type=search] { width: 130px; }
  }
</style>
</head>
<body>
<header class="top">
  <div>
    <h1>Hero Wall</h1>
    <div class="sub">Every front page · contact-sheet view</div>
  </div>
  <div class="controls">
    <span class="meta" id="wall-meta">loading…</span>
    <input id="filter" type="search" placeholder="Filter domain or kind…" autocomplete="off">
    <select id="sort">
      <option value="kind">Sort: by kind</option>
      <option value="domain">Sort: A→Z</option>
      <option value="missing">Sort: missing first</option>
    </select>
    <select id="show">
      <option value="all">Show: all</option>
      <option value="ok">Show: ok only</option>
      <option value="missing">Show: missing only</option>
    </select>
    <a href="/">← editor</a>
  </div>
</header>
<div class="wall" id="wall">
  <div class="loading"><h2>Fetching every hero…</h2></div>
</div>

<script>
(function () {
  'use strict';
  let allSites = [];
  let filter = '';
  let sort = 'kind';
  let show = 'all';

  function esc(s) {
    return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]));
  }
  function stem(d) { return String(d || '').replace(/\.[a-z]+$/, ''); }

  async function load() {
    try {
      const r = await fetch('/api/all-hero-grids?cb=' + Date.now(), { cache: 'no-store' });
      const j = await r.json();
      allSites = j.sites || [];
      document.getElementById('wall-meta').textContent =
        allSites.length + ' sites · ' +
        allSites.filter(s => s.source === 'file').length + ' wired · ' +
        allSites.filter(s => s.source === 'missing').length + ' missing';
      render();
    } catch (e) {
      document.getElementById('wall').innerHTML = '<div class="empty"><h2>Failed to load</h2><p>' + esc(e.message) + '</p></div>';
    }
  }

  function filtered() {
    let arr = allSites.slice();
    if (filter) {
      const q = filter.toLowerCase();
      arr = arr.filter(s => s.domain.toLowerCase().includes(q) || (s.kind || '').toLowerCase().includes(q));
    }
    if (show === 'ok')       arr = arr.filter(s => s.source === 'file');
    if (show === 'missing')  arr = arr.filter(s => s.source === 'missing');
    if (sort === 'domain')   arr.sort((a, b) => a.domain.localeCompare(b.domain));
    else if (sort === 'missing') arr.sort((a, b) => (a.source === 'missing' ? -1 : 1) - (b.source === 'missing' ? -1 : 1));
    else /* kind */ arr.sort((a, b) => (a.kind || 'zzz').localeCompare(b.kind || 'zzz') || a.domain.localeCompare(b.domain));
    return arr;
  }

  function render() {
    const wall = document.getElementById('wall');
    const arr = filtered();
    if (!arr.length) {
      wall.innerHTML = '<div class="empty"><h2>No matches</h2><p>Try a different filter.</p></div>';
      return;
    }
    wall.innerHTML = arr.map(s => {
      const cells = (s.grid && s.grid.cells) || [];
      const isOK = s.source === 'file' && cells.some(c => c && c.image_url);
      const cellHtml = [0,1,2,3].map(i => {
        const c = cells[i];
        if (c && c.image_url) {
          return '<div class="card-cell" style="background-image:url(' + JSON.stringify(c.image_url) + ')" title="' + esc(c.title || '') + '"></div>';
        }
        return '<div class="card-cell empty"></div>';
      }).join('');
      return '<div class="card ' + (isOK ? 'ok' : 'missing') + '" data-domain="' + esc(s.domain) + '">' +
        '<div class="card-canvas">' + cellHtml + '</div>' +
        '<div class="card-body">' +
          '<span class="card-wordmark">' + esc(stem(s.domain)) + '</span>' +
          '<span class="card-kind">' + esc(s.kind || '') + '</span>' +
        '</div>' +
        '<div class="card-domain">' +
          '<span class="dot"></span>' +
          '<span>' + esc(s.domain) + '</span>' +
          '<a class="live" href="https://' + esc(s.domain) + '/?fsq=' + Date.now() + '" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">View live ↗</a>' +
          '<span class="card-status">' + (isOK ? '✓ hero set' : '· needs hero') + '</span>' +
        '</div>' +
      '</div>';
    }).join('');
    wall.querySelectorAll('.card').forEach(card => {
      card.addEventListener('click', () => {
        const d = card.dataset.domain;
        // Drill into the per-site editor
        location.href = '/?site=' + encodeURIComponent(d);
      });
    });
  }

  document.getElementById('filter').addEventListener('input', (e) => { filter = e.target.value; render(); });
  document.getElementById('sort').addEventListener('change', (e) => { sort = e.target.value; render(); });
  document.getElementById('show').addEventListener('change', (e) => { show = e.target.value; render(); });

  load();
  // Auto-refresh every 60s so changes from the per-site editor show up here.
  setInterval(load, 60000);
})();
</script>
</body>
</html>