// ─── API hooks ──────────────────────────────────────────────────────────────── function useMemos(token) { const [memos, setMemos] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); React.useEffect(() => { let ignore = false; const ctrl = new AbortController(); const headers = token ? { Authorization: 'Bearer ' + token } : {}; fetch('/api/memos', { headers, signal: ctrl.signal }) .then(r => r.json()) .then(data => { if (!ignore) setMemos(Array.isArray(data) ? data : []); }) .catch(e => { if (!ignore) setError(e.message); }) .finally(() => { if (!ignore) setLoading(false); }); return () => { ignore = true; ctrl.abort(); }; }, [token]); return { memos, loading, error }; } function useMemoItem(slug, token) { const [memo, setMemo] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); React.useEffect(() => { if (!slug) return; let ignore = false; const ctrl = new AbortController(); const headers = token ? { Authorization: 'Bearer ' + token } : {}; fetch('/api/memos/' + slug, { headers, signal: ctrl.signal }) .then(r => r.ok ? r.json() : null) .then(data => { if (!ignore) setMemo(data); }) .catch(e => { if (!ignore) setError(e.message); }) .finally(() => { if (!ignore) setLoading(false); }); return () => { ignore = true; ctrl.abort(); }; }, [slug, token]); return { memo, loading, error }; } // ─── Render helpers ─────────────────────────────────────────────────────────── function renderBody(blocks, wide) { return blocks.map((b, i) => { if (b.t === 'h') return (
{b.s}
); if (b.t === 'callout') return (
{b.s}
); if (b.t === 'bullets') return ( ); // default: paragraph return (

{b.s}

); }); } // ─── MemoReader ─────────────────────────────────────────────────────────────── function MemoReader({ id, go, user, plan, token }) { const wide = useWide(); const { memo, loading } = useMemoItem(id, token); if (loading) return
Loading…
; if (!memo) return (
Memo not found. go('memos')} style={{ color: TH.accent, marginLeft: 8, cursor: 'pointer' }}>← Back
); const locked = !memo.free && !plan && user?.role !== 'admin'; const cutoffBlock = 4; // show first N blocks before paywall return (
{/* Top nav */}
go(plan ? 'memos' : 'memos')} style={{ fontFamily: TH.mono, fontSize: 11, color: TH.accent, letterSpacing: 1.5, cursor: 'pointer' }}> ← MEMOS MEMO №{memo.id}
{/* Article */}
{/* Header */}
{memo.category} · {memo.date}

{memo.title}

{memo.summary}
{memo.readMin} MIN READ {memo.free && · FREE} {!memo.free && !plan && · MEMBERS ONLY}
{/* Body */} {locked ? (
{/* Show partial content */}
{renderBody(memo.body.slice(0, cutoffBlock), wide)}
{/* Fade overlay */}
{/* CTA */}
⚿ MEMBERS ONLY
Continue reading Memo №{memo.id}
This memo is part of the Walkforward membership. Join to read the full archive — {memo.readMin - 2} more minutes of this piece, plus 141 back issues.
go('pricing')}>See membership → {!user && go('signup')}>Free account}
) : ( renderBody(memo.body, wide) )}
); } // ─── MemoList ───────────────────────────────────────────────────────────────── function MemoList({ go, user, plan, token }) { const wide = useWide(); const { memos, loading } = useMemos(token); if (loading) return
Loading…
; return (
§ MEMO ARCHIVE
142 memos. No filler.
Every Sunday, a short research dispatch on regime, funding, and cross-asset signals. Written to be read, not sold.
{memos.map((m, i) => (
go(`memo/${m.slug}`)} style={{ padding: wide ? '28px 28px' : '20px 18px', borderBottom: i < memos.length - 1 ? `1px solid ${TH.rule}` : 'none', cursor: 'pointer', transition: 'background 0.1s', }} onMouseEnter={e => e.currentTarget.style.background = TH.panelHi} onMouseLeave={e => e.currentTarget.style.background = 'transparent'} >
MEMO №{m.id} {m.category} {m.free && FREE}
{m.title}
{m.summary}
{m.date.toUpperCase()}
{m.readMin} MIN
))}
{!plan && (
139 more memos in the archive
Members get the full archive dating back to 2021, plus a new memo every Sunday.
go('pricing')}>See membership →
)}
); } Object.assign(window, { MemoList, MemoReader });