import { createFileRoute } from "@tanstack/react-router"; import { useEffect, useRef, useState } from "react"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Loader2, Upload, Trash2, User as UserIcon, PenLine } from "lucide-react"; import { toast } from "sonner"; export const Route = createFileRoute("/settings/profile")({ component: ProfilePage, }); const EMPTY = { full_name: "", title: "", phone: "", bio: "", timezone: "", avatar_url: "" as string | null | "", signature_block: "", signature_typed: "", signature_image_path: "" as string | null | "", }; function initials(name: string, email: string) { const src = name?.trim() || email || ""; return src.split(/\s+/).map((p) => p[0]).filter(Boolean).slice(0, 2).join("").toUpperCase(); } function ProfilePage() { const { user } = useAuth(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [uploading, setUploading] = useState(false); const [uploadingSig, setUploadingSig] = useState(false); const [form, setForm] = useState({ ...EMPTY }); const [email, setEmail] = useState(""); const fileInput = useRef(null); const sigInput = useRef(null); // Derive a public preview URL for the saved signature image const signatureUrl = form.signature_image_path ? supabase.storage.from("avatars").getPublicUrl(form.signature_image_path).data.publicUrl : ""; const load = async () => { if (!user) return; setLoading(true); const { data } = await supabase .from("profiles") .select("*") .eq("id", user.id) .maybeSingle(); if (data) { setEmail(data.email || user.email || ""); const d = data as any; setForm({ full_name: data.full_name ?? "", title: d.title ?? "", phone: d.phone ?? "", bio: d.bio ?? "", timezone: d.timezone ?? "", avatar_url: d.avatar_url ?? "", signature_block: d.signature_block ?? "", signature_typed: d.signature_typed ?? "", signature_image_path: d.signature_image_path ?? "", }); } setLoading(false); }; useEffect(() => { load(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.id]); const update = (k: keyof typeof EMPTY, v: string) => setForm((f) => ({ ...f, [k]: v })); const onAvatarFile = async (file: File) => { if (!user) return; if (file.size > 5 * 1024 * 1024) { toast.error("Image must be under 5 MB"); return; } setUploading(true); const ext = (file.name.split(".").pop() || "png").toLowerCase(); // Path must start with the user's id for the storage policy to allow it. const path = `${user.id}/avatar-${Date.now()}.${ext}`; const { error } = await supabase.storage .from("avatars") .upload(path, file, { upsert: true, contentType: file.type, cacheControl: "3600" }); if (error) { setUploading(false); toast.error("Upload failed", { description: error.message }); return; } const { data: pub } = supabase.storage.from("avatars").getPublicUrl(path); const url = pub.publicUrl; // Persist immediately so it appears in the sidebar without a save click. const { error: upErr } = await supabase .from("profiles") .update({ avatar_url: url }) .eq("id", user.id); setUploading(false); if (upErr) { toast.error("Could not save avatar", { description: upErr.message }); return; } setForm((f) => ({ ...f, avatar_url: url })); toast.success("Profile photo updated"); }; const removeAvatar = async () => { if (!user) return; const { error } = await supabase .from("profiles") .update({ avatar_url: null }) .eq("id", user.id); if (error) { toast.error("Could not remove", { description: error.message }); return; } setForm((f) => ({ ...f, avatar_url: "" })); toast.success("Photo removed"); }; const onSignatureFile = async (file: File) => { if (!user) return; if (file.size > 2 * 1024 * 1024) { toast.error("Signature image must be under 2 MB"); return; } setUploadingSig(true); const ext = (file.name.split(".").pop() || "png").toLowerCase(); const path = `${user.id}/signature-${Date.now()}.${ext}`; const { error } = await supabase.storage .from("avatars") .upload(path, file, { upsert: true, contentType: file.type, cacheControl: "3600" }); if (error) { setUploadingSig(false); toast.error("Upload failed", { description: error.message }); return; } const { error: upErr } = await supabase .from("profiles") .update({ signature_image_path: path } as any) .eq("id", user.id); setUploadingSig(false); if (upErr) { toast.error("Could not save signature", { description: upErr.message }); return; } setForm((f) => ({ ...f, signature_image_path: path })); toast.success("Signature image saved"); }; const removeSignatureImage = async () => { if (!user) return; const { error } = await supabase .from("profiles") .update({ signature_image_path: null } as any) .eq("id", user.id); if (error) { toast.error("Could not remove", { description: error.message }); return; } setForm((f) => ({ ...f, signature_image_path: "" })); toast.success("Signature image removed"); }; const save = async () => { if (!user) return; setSaving(true); const { error } = await supabase .from("profiles") .update({ full_name: form.full_name || "", title: form.title || null, phone: form.phone || null, bio: form.bio || null, timezone: form.timezone || null, signature_block: form.signature_block || null, signature_typed: form.signature_typed || null, } as any) .eq("id", user.id); setSaving(false); if (error) { toast.error("Could not save", { description: error.message }); return; } toast.success("Profile saved"); load(); }; if (loading) { return (
); } return (
Profile photo {form.avatar_url ? : null} {initials(form.full_name, email)}
{ const f = e.target.files?.[0]; if (f) onAvatarFile(f); e.target.value = ""; }} />
{form.avatar_url && ( )}

PNG or JPG, up to 5 MB.

Personal information update("full_name", e.target.value)} /> update("title", e.target.value)} placeholder="Attorney, Paralegal, etc." /> update("phone", e.target.value)} /> update("timezone", e.target.value)} placeholder="America/New_York" />