S10 (a1): public ledger profiles v1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-11 19:32:22 -04:00
parent aaafc3e0f2
commit b20145c215
19 changed files with 1226 additions and 6 deletions
+88
View File
@@ -117,6 +117,54 @@ export default function SettingsPage() {
} catch { /* best-effort */ } finally { setPrefSaving(false); }
};
// A1 Session 10 — public ledger profile: claim a handle + the ONE explicit
// publish toggle. PRIVATE BY DEFAULT — nothing is public until the user
// flips the toggle and saves.
const [pfHandle, setPfHandle] = useState('');
const [pfPublished, setPfPublished] = useState(false);
const [pfSaving, setPfSaving] = useState(false);
const [pfMsg, setPfMsg] = useState<{ ok: boolean; text: string } | null>(null);
const [pfLive, setPfLive] = useState<{ handle: string; published: boolean } | null>(null);
useEffect(() => {
if (!session?.access_token) return;
fetch('/api/profiles/me', { headers: { Authorization: `Bearer ${session.access_token}` } })
.then((r) => (r.ok ? r.json() : null))
.then((d) => {
const p = d?.profile;
if (!p) return;
setPfHandle(p.handle || '');
setPfPublished(Boolean(p.published));
setPfLive({ handle: p.handle || '', published: Boolean(p.published) });
})
.catch(() => {});
}, [session]);
const saveProfile = async () => {
setPfSaving(true);
setPfMsg(null);
try {
const res = await fetch('/api/profiles/me', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(session?.access_token ? { Authorization: `Bearer ${session.access_token}` } : {}) },
body: JSON.stringify({ handle: pfHandle, published: pfPublished }),
});
const data = await res.json().catch(() => ({}));
if (res.ok) {
setPfLive({ handle: pfHandle, published: pfPublished });
setPfMsg({ ok: true, text: pfPublished ? 'Published. Your settled record is live.' : 'Saved. Your record stays private.' });
} else if (res.status === 409) {
setPfMsg({ ok: false, text: 'That handle is taken.' });
} else {
setPfMsg({ ok: false, text: data?.error || 'Could not save profile.' });
}
} catch {
setPfMsg({ ok: false, text: 'Network error. Try again.' });
} finally {
setPfSaving(false);
}
};
const plan = tierLabel(tier || 'free');
const canDelete = deleteText === 'DELETE';
@@ -231,6 +279,46 @@ export default function SettingsPage() {
</div>
</Section>
{/* PUBLIC PROFILE (A1 Session 10) */}
<Section label="PUBLIC PROFILE">
<p style={{ margin: '0 0 14px', fontSize: 13, lineHeight: 1.55, color: 'var(--text-1)' }}>
Publishing puts your ENTIRE settled record on a public page wins and misses. Private by default.
</p>
<div style={{ fontSize: 13, color: 'var(--text-0)', marginBottom: 8 }}>Handle</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
<span className="mono" style={{ fontSize: 13, color: 'var(--text-1)' }}>vyndr.app/u/</span>
<input
value={pfHandle}
onChange={(e) => setPfHandle(e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '').slice(0, 20))}
placeholder="your_handle"
aria-label="Public profile handle"
className="mono"
style={{ width: 200, padding: '9px 12px', borderRadius: 8, background: 'var(--bg-2)', border: '1px solid var(--border-hi)', color: '#fff', fontSize: 13, outline: 'none' }}
/>
</div>
<Row>
<div>
<div style={{ fontSize: 13, color: 'var(--text-0)' }}>Publish my record</div>
<div style={{ fontSize: 11, color: 'var(--text-1)', marginTop: 2 }}>Every settled read, nothing curated</div>
</div>
<Toggle on={pfPublished} onClick={() => setPfPublished((v) => !v)} />
</Row>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 12, flexWrap: 'wrap' }}>
<button type="button" onClick={saveProfile} disabled={pfSaving || pfHandle.length < 3} className="mono"
style={{ cursor: pfHandle.length >= 3 ? 'pointer' : 'not-allowed', padding: '9px 16px', borderRadius: 8, fontWeight: 700, fontSize: 11, letterSpacing: '0.04em', border: '1px solid var(--g-a)', background: pfHandle.length >= 3 ? 'var(--g-a)' : 'transparent', color: pfHandle.length >= 3 ? '#06060B' : 'var(--text-1)' }}>
{pfSaving ? 'SAVING…' : 'SAVE PROFILE'}
</button>
{pfMsg && (
<span className="mono" style={{ fontSize: 12, color: pfMsg.ok ? 'var(--g-a)' : 'var(--miss)' }}>{pfMsg.text}</span>
)}
{pfLive?.published && pfLive.handle && (
<a href={`/u/${pfLive.handle}`} className="mono" style={{ fontSize: 12, color: 'var(--g-a)', textDecoration: 'none' }}>
VIEW PUBLIC PAGE
</a>
)}
</div>
</Section>
{/* NOTIFICATIONS */}
<Section label="NOTIFICATIONS">
<Row>