diff --git a/app/lib/profile.ts b/app/lib/profile.ts index 9b7588a..ac454d0 100644 --- a/app/lib/profile.ts +++ b/app/lib/profile.ts @@ -1,12 +1,13 @@ -// Local user preferences — title, bio, signature, avatar, default agent. -// Persisted in localStorage; reactive across tabs. Identity (name, email) -// is owned by the arcadia session, not this store — see ~/lib/session.ts. +// Local user preferences — title, signature, default agent, plus a cache +// mirror of the resolved avatar URL. Persisted in localStorage; reactive +// across tabs. Identity (name, email) is owned by the arcadia session +// (~/lib/session.ts); the public profile (bio, phone, location, timezone) +// is server-backed via /api/v1/profile (~/lib/arcadia/profiles.ts). import { useEffect, useSyncExternalStore } from "react" export type Profile = { title: string - bio: string signature: string avatarUrl: string defaultAgentId: string @@ -14,7 +15,6 @@ export type Profile = { export const DEFAULT_PROFILE: Profile = { title: "", - bio: "", signature: "", avatarUrl: "", defaultAgentId: "", @@ -32,7 +32,6 @@ function readFromStorage(): Profile { return { title: typeof parsed.title === "string" ? parsed.title : DEFAULT_PROFILE.title, - bio: typeof parsed.bio === "string" ? parsed.bio : DEFAULT_PROFILE.bio, signature: typeof parsed.signature === "string" ? parsed.signature diff --git a/app/routes/profile.tsx b/app/routes/profile.tsx index d12a620..c4e83f9 100644 --- a/app/routes/profile.tsx +++ b/app/routes/profile.tsx @@ -82,6 +82,24 @@ export default function ProfileRoute() { const [avatarUploading, setAvatarUploading] = useState(false) const [avatarError, setAvatarError] = useState(null) + // Public-profile editable fields, server-backed via PATCH /api/v1/profile. + const [profileDraft, setProfileDraft] = useState<{ + bio: string + phone: string + location: string + timezone: string + }>({ bio: "", phone: "", location: "", timezone: "" }) + const [profileSaving, setProfileSaving] = useState(false) + const [profileSavedAt, setProfileSavedAt] = useState(null) + const [profileError, setProfileError] = useState(null) + + const profileDirty = + !!arcadiaProfile && + (profileDraft.bio !== (arcadiaProfile.bio ?? "") || + profileDraft.phone !== (arcadiaProfile.phone ?? "") || + profileDraft.location !== (arcadiaProfile.location ?? "") || + profileDraft.timezone !== (arcadiaProfile.timezone ?? "")) + const loadAccount = useCallback(async () => { if (!session) return setAccountLoading(true) @@ -99,6 +117,12 @@ export default function ProfileRoute() { }) if (p) { setArcadiaProfile(p) + setProfileDraft({ + bio: p.bio ?? "", + phone: p.phone ?? "", + location: p.location ?? "", + timezone: p.timezone ?? "", + }) const url = pickAvatarUrl(p) if (url) setPrefs((d) => ({ ...d, avatarUrl: url })) } @@ -143,6 +167,37 @@ export default function ProfileRoute() { } } + const saveArcadiaProfile = async () => { + setProfileSaving(true) + setProfileError(null) + try { + const updated = await updateArcadiaProfile(arcadia, { + bio: profileDraft.bio || null, + phone: profileDraft.phone || null, + location: profileDraft.location || null, + timezone: profileDraft.timezone || null, + }) + setArcadiaProfile(updated) + setProfileDraft({ + bio: updated.bio ?? "", + phone: updated.phone ?? "", + location: updated.location ?? "", + timezone: updated.timezone ?? "", + }) + setProfileSavedAt(Date.now()) + } catch (err) { + setProfileError( + err instanceof ArcadiaError + ? err.message + : err instanceof Error + ? err.message + : "Save failed.", + ) + } finally { + setProfileSaving(false) + } + } + // Local prefs handlers. const initials = profileInitials( [accountDraft.first_name, accountDraft.last_name].filter(Boolean).join(" ") || @@ -366,12 +421,106 @@ export default function ProfileRoute() { + + + Profile + + Public profile fields stored on arcadia. Visible to other members + of this tenant. + + + + {profileError ? ( + {profileError} + ) : null} + +