import React, { useState, useRef, useEffect, useContext } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { readUser, updateProfile, buildCSRFHeaders } from "../ash_rpc"; import { uploadAvatar } from "../upload"; import { AuthCtx } from "../context"; import { getAssetHost } from "../utils"; import { Spinner } from "./ui"; import type { User } from "../types"; export function ProfileEditor({ userId }: { userId: string }) { const assetHost = getAssetHost(); const qc = useQueryClient(); const { data: user, isLoading } = useQuery({ queryKey: ["user", userId], queryFn: async () => { const res = await readUser({ fields: ["id", "email", "username", "displayName", "avatarUrl", "followerCount", "followingCount", "amIFollowing"], filter: { id: { eq: userId } }, headers: buildCSRFHeaders(), }); if (!res.success) throw new Error("Failed to load user"); const results = Array.isArray(res.data) ? res.data : (res.data as any)?.results ?? []; return (results[0] as User) ?? null; }, }); const [username, setUsername] = useState(""); const [displayName, setDisplayName] = useState(""); const [saveError, setSaveError] = useState(null); const [saveSuccess, setSaveSuccess] = useState(false); const [avatarUploading, setAvatarUploading] = useState(false); const [avatarError, setAvatarError] = useState(null); const [previewAvatarUrl, setPreviewAvatarUrl] = useState(null); const avatarInputRef = useRef(null); useEffect(() => { if (user) { setUsername(user.username ?? ""); setDisplayName(user.displayName ?? ""); } }, [user?.id]); const saveMutation = useMutation({ mutationFn: async () => { const res = await updateProfile({ identity: userId, input: { username: username.trim() || null, displayName: displayName.trim() || null, }, fields: ["id", "username", "displayName", "avatarUrl"], headers: buildCSRFHeaders(), }); if (!res.success) throw new Error((res.errors?.[0] as any)?.message ?? "Save failed"); return res.data; }, onSuccess: () => { qc.invalidateQueries({ queryKey: ["user", userId] }); setSaveSuccess(true); setSaveError(null); setTimeout(() => setSaveSuccess(false), 3000); }, onError: (e: Error) => setSaveError(e.message), }); async function handleAvatarChange(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; e.target.value = ""; if (previewAvatarUrl) URL.revokeObjectURL(previewAvatarUrl); setPreviewAvatarUrl(URL.createObjectURL(file)); setAvatarError(null); setAvatarUploading(true); const csrfToken = buildCSRFHeaders()["X-CSRF-Token"] as string; const result = await uploadAvatar(file, csrfToken); setAvatarUploading(false); if ("error" in result) { setAvatarError(result.error); if (previewAvatarUrl) URL.revokeObjectURL(previewAvatarUrl); setPreviewAvatarUrl(null); } else { qc.invalidateQueries({ queryKey: ["user", userId] }); } } if (isLoading || !user) return ; const currentAvatarUrl = previewAvatarUrl ? previewAvatarUrl : user.avatarUrl ? `${assetHost}/${user.avatarUrl}` : null; return (
{currentAvatarUrl ? ( Your avatar ) : (
{(user.displayName || user.username || user.email || "M")[0].toUpperCase()}
)}
{avatarError &&

{avatarError}

}
{user.followerCount ?? 0} followers {user.followingCount ?? 0} following
setDisplayName(e.target.value)} maxLength={50} />
@ setUsername(e.target.value.replace(/[^a-zA-Z0-9_]/g, ""))} maxLength={30} />

3–30 characters. Letters, numbers, underscores only.

{saveError &&

{saveError}

} {saveSuccess &&

✓ Saved!

}
Sign out
); } export function MyProfile() { const { userId } = useContext(AuthCtx); if (!userId) { return (

Your profile

Sign in {" "}to view your profile.

); } return ; }