← back to Ventura Corridor

public/feedback.html

126 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Reader feedback · The Corridor</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script>(function(){try{var t=localStorage.getItem('vc-theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(e){document.documentElement.dataset.theme='dark';}})();</script>
<link rel="stylesheet" href="/theme.css">
<script src="/theme.js" defer></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;1,400;1,500&family=Inter:wght@200;300;400;500&family=JetBrains+Mono:wght@200;300&display=swap');
:root{--noir:#0a0a0c;--noir-rise:#131316;--ink:#f0ece2;--ink-mute:#888475;--metal:#b89968;--metal-glow:#d4b683;--green:#6a9b73;--red:#b66565;--rule:#2a2622}
*{box-sizing:border-box}html,body{margin:0;background:var(--noir);color:var(--ink);font-family:'Inter',sans-serif;font-weight:300}
header{padding:30px 32px 16px;border-bottom:1px solid var(--rule)}
header h1{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:400;font-size:32px;margin:0;color:var(--ink)}
header h1 em{color:var(--metal)}
header .sub{font-size:11px;letter-spacing:.26em;text-transform:uppercase;color:var(--ink-mute);margin-top:4px}
nav{padding:10px 32px;border-bottom:1px solid var(--rule);display:flex;gap:12px;flex-wrap:wrap;font-size:9px;letter-spacing:.26em;text-transform:uppercase}
nav a{color:var(--ink-mute);text-decoration:none;padding:5px 10px;border:1px solid var(--rule)}
nav a:hover, nav a.active{color:var(--metal);border-color:var(--metal)}

.chips{padding:14px 32px;display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid var(--rule)}
.chips button{font-size:9px;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-mute);background:transparent;border:1px solid var(--rule);padding:5px 12px;cursor:pointer;font-family:'Inter',sans-serif}
.chips button.active{color:var(--metal);border-color:var(--metal)}
.chips button:hover{border-color:var(--metal)}

main{padding:24px 32px;max-width:1100px;margin:0 auto;display:flex;flex-direction:column;gap:14px}
.row{display:grid;grid-template-columns:auto 1fr auto auto;gap:16px;padding:14px 18px;background:var(--noir-rise);border:1px solid var(--rule);align-items:flex-start;font-size:13px}
.row .kind{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:.22em;text-transform:uppercase;padding:3px 8px;border:1px solid var(--rule);align-self:flex-start;min-width:110px;text-align:center}
.row .kind.typo,.row .kind.correction{color:var(--metal);border-color:var(--metal)}
.row .kind.tip{color:var(--metal-glow);border-color:var(--metal-glow)}
.row .kind.complaint{color:var(--red);border-color:var(--red)}
.row .kind.praise{color:var(--green);border-color:var(--green)}
.row .body{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:15px;line-height:1.5;color:var(--ink)}
.row .meta{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--ink-mute);margin-top:6px;letter-spacing:.04em}
.row .meta a{color:var(--metal);text-decoration:none}
.row .ts{font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--ink-mute);align-self:flex-start;text-align:right;min-width:100px}
.row .actions{display:flex;flex-direction:column;gap:4px}
.row .actions button{background:transparent;border:1px solid var(--rule);color:var(--ink-mute);font-size:8px;letter-spacing:.18em;text-transform:uppercase;padding:4px 8px;cursor:pointer;font-family:'Inter',sans-serif}
.row .actions button.done{color:var(--green);border-color:var(--green)}
.row.reviewed{opacity:0.45}
.empty{text-align:center;padding:60px;color:var(--ink-mute);font-style:italic;font-family:'Cormorant Garamond',serif;font-size:20px}
</style>
</head>
<body>
<header>
  <div><h1>Reader <em>feedback</em></h1></div>
  <div class="sub">typos · corrections · tips · praise · complaints</div>
</header>
<nav>
  <a href="/today.html">today</a>
  <a href="/magazine.html">magazine</a>
  <a href="/scoreboard.html">scoreboard</a>
  <a href="/feedback.html" class="active">feedback</a>
  <a href="/issue">issue</a>
</nav>
<div class="chips">
  <button class="active" data-kind="all">All</button>
  <button data-kind="typo">Typos</button>
  <button data-kind="correction">Corrections</button>
  <button data-kind="tip">Tips</button>
  <button data-kind="complaint">Complaints</button>
  <button data-kind="praise">Praise</button>
  <button data-kind="note">Other</button>
  <span style="flex:1"></span>
  <button id="hide-reviewed" data-on="0">Hide reviewed</button>
</div>
<main id="rows"></main>
<script>
function escHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); }
let kind = 'all', hideReviewed = false, all = [];
async function load() {
  const d = await fetch('/api/feedback').then(r => r.json());
  all = d.rows || [];
  render();
}
function render() {
  let rows = all;
  if (kind !== 'all') rows = rows.filter(r => r.kind === kind);
  if (hideReviewed) rows = rows.filter(r => !r.reviewed);
  const main = document.getElementById('rows');
  if (!rows.length) { main.innerHTML = '<div class="empty">No feedback yet.</div>'; return; }
  main.innerHTML = rows.map(r => {
    const ts = new Date(r.received_at).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
    return `
      <div class="row ${r.reviewed ? 'reviewed' : ''}">
        <span class="kind ${r.kind}">${escHtml(r.kind)}</span>
        <div>
          <div class="body">${escHtml(r.body).replace(/\n/g, '<br>')}</div>
          <div class="meta">
            ${r.feature_id ? `<a href="/magazine/${r.feature_id}" target="_blank">/magazine/${r.feature_id}</a> · ${escHtml(r.headline || r.biz_name || '')}` : '<i>no feature attached</i>'}
            ${r.contact ? ` · <a href="mailto:${escHtml(r.contact)}">${escHtml(r.contact)}</a>` : ''}
          </div>
        </div>
        <span class="ts">${ts}</span>
        <div class="actions">
          ${r.reviewed
            ? `<button onclick="patchFb(${r.id}, false)">↶ unread</button>`
            : `<button class="done" onclick="patchFb(${r.id}, true)">✓ reviewed</button>`}
        </div>
      </div>`;
  }).join('');
}
async function patchFb(id, reviewed) {
  await fetch(`/api/feedback/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reviewed }) });
  load();
}
document.querySelectorAll('.chips button[data-kind]').forEach(b => b.addEventListener('click', () => {
  document.querySelectorAll('.chips button[data-kind]').forEach(x => x.classList.remove('active'));
  b.classList.add('active');
  kind = b.dataset.kind;
  render();
}));
document.getElementById('hide-reviewed').addEventListener('click', e => {
  hideReviewed = !hideReviewed;
  e.target.dataset.on = hideReviewed ? '1' : '0';
  e.target.textContent = hideReviewed ? '✓ Hiding reviewed' : 'Hide reviewed';
  e.target.classList.toggle('active', hideReviewed);
  render();
});
load();
setInterval(load, 60_000);
</script>
</body>
</html>