import { useEffect, useId, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Camera, LoaderCircle, Save, Trash2, UserRoundPen } from "lucide-react"; import type { AuthSession, CurrentUserProfile, UpdateCurrentUserProfile } from "@paperclipai/shared"; import { authApi } from "@/api/auth"; import { assetsApi } from "@/api/assets"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useCompany } from "../context/CompanyContext"; import { queryKeys } from "../lib/queryKeys"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; function deriveInitials(name: string) { const parts = name.trim().split(/\s+/).filter(Boolean); if (parts.length >= 2) return `${parts[0]?.[0] ?? ""}${parts[parts.length - 1]?.[0] ?? ""}`.toUpperCase(); return name.slice(0, 2).toUpperCase(); } export function ProfileSettings() { const { setBreadcrumbs } = useBreadcrumbs(); const { selectedCompanyId, selectedCompany } = useCompany(); const queryClient = useQueryClient(); const avatarInputId = useId(); const avatarInputRef = useRef(null); const [name, setName] = useState(""); const [image, setImage] = useState(""); const [actionError, setActionError] = useState(null); const sessionQuery = useQuery({ queryKey: queryKeys.auth.session, queryFn: () => authApi.getSession(), retry: false, }); useEffect(() => { setBreadcrumbs([ { label: "Instance Settings" }, { label: "Profile" }, ]); }, [setBreadcrumbs]); useEffect(() => { const session = sessionQuery.data; if (!session) return; setName(session.user.name ?? ""); setImage(session.user.image ?? ""); }, [sessionQuery.data]); function syncSessionProfile(profile: CurrentUserProfile) { queryClient.setQueryData(queryKeys.auth.session, (current) => { if (!current) return current; return { ...current, user: { ...current.user, ...profile, }, }; }); } async function persistProfile(input: UpdateCurrentUserProfile) { const profile = await authApi.updateProfile(input); syncSessionProfile(profile); return profile; } function resolveProfileName() { return name.trim() || sessionQuery.data?.user.name || "Board"; } const updateMutation = useMutation({ mutationFn: (input: UpdateCurrentUserProfile) => persistProfile(input), onSuccess: (profile) => { setActionError(null); setName(profile.name ?? ""); setImage(profile.image ?? ""); }, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to update profile."); }, }); const uploadAvatarMutation = useMutation({ mutationFn: async (file: File) => { if (!selectedCompanyId) { throw new Error("Select a company before uploading a profile avatar."); } const asset = await assetsApi.uploadImage( selectedCompanyId, file, `profiles/${sessionQuery.data?.user.id ?? "board-user"}`, ); return persistProfile({ name: resolveProfileName(), image: asset.contentPath }); }, onSuccess: (profile) => { setActionError(null); setName(profile.name ?? ""); setImage(profile.image ?? ""); }, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to upload avatar."); }, }); const removeAvatarMutation = useMutation({ mutationFn: () => persistProfile({ name: resolveProfileName(), image: null }), onSuccess: (profile) => { setActionError(null); setName(profile.name ?? ""); setImage(profile.image ?? ""); }, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to remove avatar."); }, }); if (sessionQuery.isLoading) { return
Loading profile...
; } if (sessionQuery.error || !sessionQuery.data) { return (
{sessionQuery.error instanceof Error ? sessionQuery.error.message : "Failed to load profile."}
); } const currentName = name.trim() || sessionQuery.data.user.name || "Board"; const currentImage = image.trim() || null; const initials = deriveInitials(currentName); const isSavingProfile = updateMutation.isPending || uploadAvatarMutation.isPending || removeAvatarMutation.isPending; const uploadHint = selectedCompany ? `Stored in Paperclip file storage for ${selectedCompany.name}.` : "Select a company to upload an avatar into Paperclip storage."; return (

Profile

Control how your account appears in the sidebar and other board surfaces.

{actionError ? (
{actionError}
) : null}
{currentImage ? ( ) : null}

{currentName}

{sessionQuery.data.user.email ?? "No email"}

Click the avatar to upload a new image. {uploadHint}

{ event.preventDefault(); updateMutation.mutate({ name: resolveProfileName(), image: image.trim() || null }); }} >
setName(event.target.value)} maxLength={120} placeholder="Board" />

Shown in the sidebar account footer and comment author surfaces.

Email is managed by your auth session and is read-only here.

); }