← back to Abrams

public/hub.html

111 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Abrams — Info Hub</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/panel.css" />
<style>
  .container { max-width: 1280px; margin: 0 auto; padding: 110px 24px 60px; }
  .source { background: var(--bg-2); border: 1px solid var(--hairline); padding: 18px 22px; margin-bottom: 10px; display: grid; grid-template-columns: 1fr 200px; gap: 20px; align-items: start; }
  .source .nm { font-family: var(--serif); font-size: 19px; font-weight: 500; }
  .source .nm a { color: var(--fg); }
  .source .nm a:hover { color: var(--accent); }
  .source .desc { color: var(--fg-2); font-size: 13px; line-height: 1.6; margin: 6px 0 8px; }
  .source .url { font-family: var(--mono); font-size: 11px; color: var(--accent); word-break: break-all; }
  .source .meta { font-family: var(--mono); font-size: 10px; letter-spacing: .15em; color: var(--fg-3); text-transform: uppercase; text-align: right; }
  .source .meta .row { padding: 3px 0; }
  .source .meta .mfa-yes { color: var(--warn); }
  .source .meta .mfa-no { color: var(--ok); }
  .cat-block { margin-bottom: 36px; }
  .cat-block h2 { font-family: var(--serif); font-size: 26px; font-weight: 400; margin: 0 0 4px; }
  .cat-block .sub { font-family: var(--mono); font-size: 10px; letter-spacing: .25em; color: var(--accent); text-transform: uppercase; margin: 0 0 16px; }
</style>
</head>
<body>

<div class="chrome">
  <a class="logo" href="/">ABRAMS<span class="dot">.</span></a>
  <div class="nav-burger" role="button" aria-label="open menu">
    <span class="bars"><span></span><span></span><span></span></span>
    <span class="label">MENU</span>
  </div>
</div>

<div class="container">
  <h1 style="font-family:var(--serif);font-size:48px;font-weight:400;margin:0 0 6px;">Info Hub</h1>
  <p style="color:var(--fg-2);max-width:760px;margin:0 0 16px;">Authoritative external sources every independent operator should bookmark — legal IP, email compliance, founder bookkeeping, tooling APIs, self-audit. Curated by the search-specialist subagent, refreshed on demand.</p>

  <div class="controls">
    <span class="lbl">Filter</span>
    <input id="filter" type="search" placeholder="search name + description + url" style="background:var(--bg-2);border:1px solid var(--hairline);color:var(--fg);padding:8px 12px;font-family:var(--mono);font-size:12px;min-width:300px;" />
    <span class="lbl">Cat</span>
    <select id="cat-filter"><option value="">all</option></select>
    <span class="lbl">Mobile</span>
    <select id="mobile-filter"><option value="">any</option><option value="3">★★★ only</option></select>
  </div>

  <div id="hub"></div>
  <p id="meta" style="color:var(--fg-3);font-family:var(--mono);font-size:11px;letter-spacing:.15em;margin-top:24px;"></p>
</div>

<footer>© 2026 Steven Abrams</footer>

<script src="/static/js/corner-nav.js"></script>
<script src="/static/js/gamify.js"></script>
<script>
function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m])); }
let allSources = [];
function render(){
  const f = (document.getElementById('filter').value||'').toLowerCase();
  const c = document.getElementById('cat-filter').value;
  const m = document.getElementById('mobile-filter').value;
  let sources = allSources.filter(s => {
    const blob = (s.name+' '+s.description+' '+s.url+' '+(s.subcategory||'')).toLowerCase();
    if (f && !blob.includes(f)) return false;
    if (c && s.category !== c) return false;
    if (m && (s.mobile||0) < parseInt(m, 10)) return false;
    return true;
  });
  const byCat = {};
  for (const s of sources) { (byCat[s.category] ||= []).push(s); }
  const order = Object.keys(byCat).sort();
  document.getElementById('hub').innerHTML = order.map(cat => `
    <div class="cat-block">
      <h2>${esc(cat)}</h2>
      <p class="sub">${byCat[cat].length} source${byCat[cat].length===1?'':'s'}</p>
      ${byCat[cat].map(s => `
        <div class="source">
          <div>
            <div class="nm"><a href="${esc(s.url)}" target="_blank" rel="noopener noreferrer">${esc(s.name)}</a></div>
            <div class="desc">${esc(s.description)}</div>
            <div class="url">${esc(s.url)}</div>
          </div>
          <div class="meta">
            <div class="row">${esc(s.subcategory||'')}</div>
            <div class="row mfa-${s.mfa?'yes':'no'}">${s.mfa?'MFA REQUIRED':'NO LOGIN'}</div>
            <div class="row">RET: ${esc(s.retention||'unknown')}</div>
            <div class="row">MOBILE ${'★'.repeat(s.mobile||1)}${'☆'.repeat(3-(s.mobile||1))}</div>
          </div>
        </div>
      `).join('')}
    </div>
  `).join('') || '<div style="color:var(--fg-3);padding:48px;text-align:center;font-family:var(--mono);">no sources match filters</div>';
  document.getElementById('meta').textContent = `${sources.length} of ${allSources.length} sources shown`;
}
fetch('/api/info-hub').then(r=>r.json()).then(d => {
  allSources = d.sources || [];
  // populate category dropdown
  const cats = [...new Set(allSources.map(s => s.category))].sort();
  document.getElementById('cat-filter').innerHTML = '<option value="">all categories</option>' + cats.map(c => `<option value="${esc(c)}">${esc(c)}</option>`).join('');
  render();
});
['filter','cat-filter','mobile-filter'].forEach(id => document.getElementById(id).addEventListener('input', render));
document.getElementById('cat-filter').addEventListener('change', render);
document.getElementById('mobile-filter').addEventListener('change', render);
</script>
</body>
</html>