S10 (a1): public ledger profiles v1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user