← back to Ventura Corridor
iter 127+128: /magazine/:id/print.html print-only view + sponsor-inquiry status flips — /magazine/:id/print.html renders a single-feature pure print layout (Letter @page, 0.75in margins, no toolbar, Cormorant 36pt italic headline, drop-cap lede, copper pull-quote, 2-col colophon w/ business/address/trade/issue, auto-fires window.print() on load); PATCH /api/sponsor/inquiries/:id flips status (new/responded/closed/spam); /today.html inquiry rows grow ↩ replied + ✓ done buttons + status pill (new=accent / responded=metal / closed=green / spam=mute)
5af0acfde182e48a4d0def859e0715218fa99fff · 2026-05-06 18:34:50 -0700 · SteveStudio2
Files touched
M public/today.htmlM src/server/index.ts
Diff
commit 5af0acfde182e48a4d0def859e0715218fa99fff
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Wed May 6 18:34:50 2026 -0700
iter 127+128: /magazine/:id/print.html print-only view + sponsor-inquiry status flips — /magazine/:id/print.html renders a single-feature pure print layout (Letter @page, 0.75in margins, no toolbar, Cormorant 36pt italic headline, drop-cap lede, copper pull-quote, 2-col colophon w/ business/address/trade/issue, auto-fires window.print() on load); PATCH /api/sponsor/inquiries/:id flips status (new/responded/closed/spam); /today.html inquiry rows grow ↩ replied + ✓ done buttons + status pill (new=accent / responded=metal / closed=green / spam=mute)
---
public/today.html | 19 ++++++++++++----
src/server/index.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 79 insertions(+), 4 deletions(-)
diff --git a/public/today.html b/public/today.html
index 2c777f0..bcc3451 100644
--- a/public/today.html
+++ b/public/today.html
@@ -672,15 +672,26 @@ async function loadInquiries() {
document.getElementById('inq-list').innerHTML = rows.map(r => {
const ts = new Date(r.received_at).toLocaleString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
const tierColor = ({ cover: 'var(--accent,#a8763a)', spread: 'var(--metal-glow)', ask: 'var(--ink-mute)' })[r.tier] || 'var(--ink-mute)';
- return `<a href="/magazine/${r.feature_id}" target="_blank" style="display:grid;grid-template-columns:auto 1fr auto auto;gap:14px;align-items:center;padding:7px 10px;background:var(--noir);border:1px solid var(--rule);text-decoration:none;color:var(--ink);font-size:12px">
+ const statusColor = ({ new: 'var(--accent,#a8763a)', responded: 'var(--metal)', closed: 'var(--green,#6a9b73)', spam: 'var(--ink-mute)' })[r.status] || 'var(--ink-mute)';
+ return `<div style="display:grid;grid-template-columns:auto 1fr auto auto auto;gap:14px;align-items:center;padding:7px 10px;background:var(--noir);border:1px solid var(--rule);font-size:12px">
<span style="color:${tierColor};font-family:var(--mono);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:500">${escHtml(r.tier || 'ask')}</span>
- <span><b style="font-family:var(--serif);font-size:14px;color:var(--metal-glow)">${escHtml(r.biz_name || '?')}</b> <span style="color:var(--ink-mute);font-family:var(--mono);font-size:10px;margin-left:6px">${escHtml(r.contact_name || '')} · ${escHtml(r.contact_email || '')}</span></span>
+ <a href="/magazine/${r.feature_id}" target="_blank" style="color:var(--ink);text-decoration:none"><b style="font-family:var(--serif);font-size:14px;color:var(--metal-glow)">${escHtml(r.biz_name || '?')}</b> <span style="color:var(--ink-mute);font-family:var(--mono);font-size:10px;margin-left:6px">${escHtml(r.contact_name || '')} · ${escHtml(r.contact_email || '')}</span></a>
+ <span style="color:${statusColor};font-family:var(--mono);font-size:9px;letter-spacing:.18em;text-transform:uppercase">${escHtml(r.status || 'new')}</span>
<span style="color:var(--ink-mute);font-family:var(--mono);font-size:9px">${ts}</span>
- <span style="color:var(--metal);font-size:10px;letter-spacing:.18em;text-transform:uppercase">view →</span>
- </a>`;
+ <span>
+ <button onclick="patchInq(${r.id}, 'responded')" style="background:transparent;border:1px solid var(--rule);color:var(--metal);font-size:8px;letter-spacing:.18em;text-transform:uppercase;padding:3px 6px;cursor:pointer">↩ replied</button>
+ <button onclick="patchInq(${r.id}, 'closed')" style="background:transparent;border:1px solid var(--rule);color:var(--green,#6a9b73);font-size:8px;letter-spacing:.18em;text-transform:uppercase;padding:3px 6px;cursor:pointer">✓ done</button>
+ </span>
+ </div>`;
}).join('');
} catch {}
}
+async function patchInq(id, status) {
+ try {
+ await fetch(`/api/sponsor/inquiries/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) });
+ loadInquiries();
+ } catch (e) { alert('Failed: ' + e.message); }
+}
loadInquiries();
setInterval(loadInquiries, 60_000);
</script>
diff --git a/src/server/index.ts b/src/server/index.ts
index a9cf237..f043f1c 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -2590,6 +2590,58 @@ app.get('/api/find', async (req, res) => {
}
});
+// /magazine/:id/print.html — minimal print-only single-feature, no toolbar/carousels
+app.get('/magazine/:id/print.html', async (req, res) => {
+ const id = parseInt(req.params.id, 10);
+ if (!Number.isFinite(id)) return res.status(400).send('bad id');
+ const r = await query(
+ `SELECT mf.headline, mf.subhead, mf.editorial, mf.pull_quote, mf.category_tag,
+ mf.published_at, mf.generated_at, mf.model,
+ b.name AS biz_name, b.address AS biz_address, b.city, b.zip,
+ b.raw->>'primary_naics_description' AS naics
+ FROM magazine_features mf JOIN businesses b ON b.id = mf.business_id
+ WHERE mf.id = $1`,
+ [id]
+ );
+ if (r.rowCount === 0) return res.status(404).send('not found');
+ const f = r.rows[0];
+ const esc = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]!));
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
+ res.send(`<!doctype html><html><head><meta charset="utf-8">
+<title>${esc(f.headline || f.biz_name)} — The Corridor</title>
+<style>
+@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,400;1,500&family=Inter:wght@300;400;500&display=swap');
+@page { size: Letter; margin: 0.75in 0.75in 0.5in; }
+*{box-sizing:border-box;margin:0;padding:0}
+body{font-family:'Cormorant Garamond',serif;color:#1a1815;line-height:1.55;background:white;font-weight:400}
+.kicker{font-family:'Inter',sans-serif;font-size:9pt;letter-spacing:.4em;text-transform:uppercase;color:#8a6d3b;font-weight:500;margin-bottom:18px;border-bottom:0.5pt solid #d8cdb8;padding-bottom:8px}
+h1{font-style:italic;font-weight:500;font-size:36pt;line-height:1.0;margin-bottom:10pt;letter-spacing:-0.01em}
+.subhead{font-style:italic;font-weight:300;font-size:14pt;line-height:1.35;color:#6e6356;margin-bottom:18pt}
+.lede{font-weight:400;font-size:12pt;line-height:1.65;margin-bottom:12pt;text-align:justify;hyphens:auto}
+.lede::first-letter{font-weight:600;font-size:36pt;line-height:0.85;float:left;margin:6pt 8pt 0 0;color:#6a3a1a}
+.pull{font-style:italic;font-weight:400;font-size:14pt;line-height:1.3;color:#6a3a1a;border-left:2pt solid #8a6d3b;padding-left:14pt;margin:18pt 0}
+.colophon{margin-top:36pt;padding-top:14pt;border-top:0.5pt solid #d8cdb8;font-family:'Inter',sans-serif;font-size:8pt;color:#6e6356;letter-spacing:.04em;display:grid;grid-template-columns:1fr 1fr;gap:24pt}
+.colophon dt{color:#8a6d3b;font-size:7pt;letter-spacing:.32em;text-transform:uppercase;margin-bottom:2pt}
+.colophon dd{font-family:'Cormorant Garamond',serif;font-size:11pt;font-style:italic;color:#1a1815}
+.foot{margin-top:24pt;text-align:center;font-family:'Inter',sans-serif;font-size:8pt;letter-spacing:.32em;text-transform:uppercase;color:#6e6356}
+.foot em{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:11pt;color:#8a6d3b;letter-spacing:0;text-transform:none}
+</style></head><body>
+<div class="kicker">The Corridor · ${esc(f.category_tag || 'feature')}</div>
+<h1>${esc(f.headline || '(untitled)')}</h1>
+<p class="subhead">${esc(f.subhead || '')}</p>
+<p class="lede">${esc(f.editorial || '')}</p>
+${f.pull_quote ? `<p class="pull">"${esc(f.pull_quote)}"</p>` : ''}
+<dl class="colophon">
+ <div><dt>Business</dt><dd>${esc(f.biz_name)}</dd></div>
+ <div><dt>Address</dt><dd>${esc(f.biz_address || '')}, ${esc(f.city || '')} ${esc(f.zip || '')}</dd></div>
+ ${f.naics ? `<div><dt>Trade</dt><dd>${esc(f.naics)}</dd></div>` : ''}
+ <div><dt>Issue</dt><dd>${f.published_at ? new Date(f.published_at).toLocaleDateString('en-US',{month:'long',year:'numeric'}) : 'Volume I'}</dd></div>
+</dl>
+<div class="foot">The <em>Corridor</em> · Volume I · ${new Date().getFullYear()}</div>
+<script>setTimeout(() => window.print(), 250);</script>
+</body></html>`);
+});
+
// /buildings/:bldg/issue — mini-issue showing every feature for one building.
// Powers a future "this building's tenants" landing page; permalinkable.
app.get('/buildings/:bldg/issue', async (req, res) => {
@@ -2837,6 +2889,18 @@ ${b.notes ? `<div style="margin-top:24px;padding:14px 18px;background:rgba(184,1
}
});
+app.patch('/api/sponsor/inquiries/:id', express.json(), async (req, res) => {
+ try {
+ const id = parseInt(req.params.id, 10);
+ const status = String(req.body?.status || '').trim();
+ if (!['new','responded','closed','spam'].includes(status)) return res.status(400).json({ error: 'status must be new|responded|closed|spam' });
+ await query(`UPDATE sponsor_inquiries SET status = $1 WHERE id = $2`, [status, id]);
+ res.json({ ok: true, status });
+ } catch (e: any) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
app.get('/api/sponsor/inquiries', async (_req, res) => {
try {
const r = await query(
← 26b38e1 iter 125+126: /sponsor.html public landing + sponsor-inquiry
·
back to Ventura Corridor
·
iter 129+130: /about masthead page + /api/feedback POST + re 04c6a95 →