[object Object]

← 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

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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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 →