// ─── 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 (
{b.s.map((item, j) => (
- {item}
))}
);
// 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 });