← 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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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>