/* global React, Icon, StatusPill */ const { useState, useEffect, useRef } = React; /* ============================================================ CASE STUDIES — Phase 1: List + Brief editor Phase 2 (image library + drag-drop) and Phase 3 (renderer + PDF) land on the Images / Preview / Render tabs. ============================================================ */ function CaseStudies({ goto }) { const [items, setItems] = useState(null); const [creating, setCreating] = useState(false); const reload = () => fetch('/api/case-studies', { credentials: 'include' }) .then((r) => r.json()) .then((d) => setItems(d.items || [])) .catch(() => setItems([])); useEffect(() => { reload(); }, []); return (
Outreach assets

Case Studies

{items ? `${items.length} case ${items.length === 1 ? 'study' : 'studies'}` : '—'}
{items === null &&
Loading…
} {items && items.length === 0 && (
No case studies yet. Click + New case study to start one.
)} {items && items.length > 0 && (
{items.map((cs) => ( goto('case-study-editor', cs.id)} /> ))}
)}
{creating && ( setCreating(false)} onCreated={(cs) => { setCreating(false); goto('case-study-editor', cs.id); }} /> )}
); } function CaseStudyCard({ cs, onOpen }) { const headlineComplete = !!(cs.headline_before && cs.headline_after && cs.headline_time); const kpiCount = (cs.kpi_cards || []).length; const stackCount = (cs.stack_pillars || []).length; return (
{cs.client_name}
{cs.client_slug}
{headlineComplete && (
From {cs.headline_before} to {cs.headline_after} in {cs.headline_time}.
)}
{kpiCount} KPIs {stackCount} pillars {cs.last_rendered_at && rendered}
); } function NewCaseStudyModal({ onClose, onCreated }) { const [name, setName] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const submit = async () => { if (!name.trim()) { setError('Client name is required'); return; } setBusy(true); setError(null); try { const r = await fetch('/api/case-studies', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ client_name: name.trim() }), }); if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d.detail || 'create failed'); } const data = await r.json(); onCreated(data); } catch (e) { setError(e.message); } finally { setBusy(false); } }; return (
e.stopPropagation()} style={{ background: 'var(--surface)', border: '1px solid var(--divider)', width: 480, padding: 28 }}>
Outreach assets

New case study

setName(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && submit()} placeholder="e.g. Stride · Hintime · EnduraXcel" style={{ width: '100%', marginBottom: 16 }} /> {error &&
{error}
}
); } /* ============================================================ EDITOR ============================================================ */ function CaseStudyEditor({ caseStudyId, goto }) { const [cs, setCs] = useState(null); const [tab, setTab] = useState('brief'); const [savedAt, setSavedAt] = useState(null); const reload = async () => { const d = await fetch(`/api/case-studies/${caseStudyId}`, { credentials: 'include' }).then((r) => r.json()); setCs(d); }; useEffect(() => { reload(); }, [caseStudyId]); // Auto-save with a small debounce on field blur (handled per-field in BriefEditor) const patch = async (data) => { const r = await fetch(`/api/case-studies/${caseStudyId}`, { method: 'PATCH', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify(data), }); if (r.ok) { const updated = await r.json(); setCs(updated); setSavedAt(new Date()); } }; if (!cs) return
Loading…
; return (
setCs({ ...cs, client_name: e.target.value })} onBlur={() => patch({ client_name: cs.client_name })} style={{ fontFamily: 'var(--font-display)', fontStretch: '75%', fontWeight: 800, fontSize: 22, padding: '4px 8px', minWidth: 320 }} /> {savedAt && Saved {savedAt.toLocaleTimeString()}}
{[['brief','Brief'],['images','Images'],['preview','Preview'],['render','Render']].map(([k,l]) => (
setTab(k)}>{l}
))}
{tab === 'brief' &&
} {tab === 'images' && } {tab === 'preview' && } {tab === 'render' && }
); } function ComingSoon({ msg }) { return (
{msg}
); } /* ============================================================ BRIEF EDITOR ============================================================ */ function BriefEditor({ cs, setCs, patch }) { // Helpers — local set updates state, blur sends a PATCH const set = (field, value) => setCs({ ...cs, [field]: value }); const save = (field, value) => patch({ [field]: value }); return (
{/* Identity + dates */}
set('client_name', e.target.value)} onBlur={(e) => save('client_name', e.target.value)} style={{ width: '100%' }} /> set('client_slug', e.target.value)} onBlur={(e) => save('client_slug', e.target.value)} style={{ width: '100%' }} /> set('engagement_start_date', e.target.value)} onBlur={(e) => save('engagement_start_date', e.target.value)} style={{ width: '100%' }} />
{/* Headline */}
The cover headline always reads:{' '} From {cs.headline_before || '[BEFORE]'} to {cs.headline_after || '[AFTER]'} in just {cs.headline_time || '[TIME]'}.
set('headline_before', e.target.value)} onBlur={(e) => save('headline_before', e.target.value)} placeholder="e.g. manual reorders" style={{ width: '100%' }} /> set('headline_after', e.target.value)} onBlur={(e) => save('headline_after', e.target.value)} placeholder="e.g. 200+ new subscribers" style={{ width: '100%' }} /> set('headline_time', e.target.value)} onBlur={(e) => save('headline_time', e.target.value)} placeholder="e.g. three months" style={{ width: '100%' }} />