| 66 | className="flex items-center gap-3 px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors" |
| 67 | > |
| 68 | {icon} |
| 69 | {label} |
| 70 | </Link> |
| 71 | ); |
| 72 | } |
| 73 | |
| 74 | export function UserMenu() { |
| 75 | const { user, profile, isLoading, signOut } = useAuth(); |
| 76 | const [isOpen, setIsOpen] = useState(false); |
| 77 | const menuRef = useRef<HTMLDivElement>(null); |
| 78 | const triggerRef = useRef<HTMLButtonElement>(null); |
| 79 | |
| 80 | // Close on outside click / Escape, returning focus to the trigger |
| 81 | useDismiss(menuRef, isOpen, () => setIsOpen(false), { |
| 82 | returnFocusRef: triggerRef, |
| 83 | }); |
| 84 | |
| 85 | const handleSignOut = async () => { |
| 86 | try { |
| 87 | await signOut(); |
| 88 | setIsOpen(false); |
| 89 | } catch (error) { |
| 90 | console.error("Sign out error:", error); |
| 91 | toast.error("Failed to sign out. Please try again."); |
| 92 | } |
| 93 | }; |
| 94 | |
| 95 | const closeMenu = () => setIsOpen(false); |
| 96 | |
| 97 | if (isLoading) { |
| 98 | // Placeholder avoids the signed-in state flashing to nothing/"Sign In" |
| 99 | return ( |
| 100 | <div |
| 101 | className="w-8 h-8 rounded-full bg-gray-200 dark:bg-zinc-800 animate-pulse" |
| 102 | aria-hidden="true" |
| 103 | /> |
| 104 | ); |
| 105 | } |
| 106 | |
| 107 | if (!user) { |
| 108 | return null; |
| 109 | } |
| 110 | |
| 111 | // Get avatar URL from profile OR from user metadata (Google provides it in user_metadata) |
| 112 | const avatarUrl = |
| 113 | profile?.avatar_url || |
| 114 | (user.user_metadata?.avatar_url as string | undefined) || |
| 115 | (user.user_metadata?.picture as string | undefined); |
| 116 | const fullName = |
| 117 | profile?.full_name || |
| 118 | (user.user_metadata?.full_name as string | undefined) || |
| 119 | (user.user_metadata?.name as string | undefined); |
| 120 | |
| 121 | return ( |
| 122 | <div ref={menuRef} className="relative"> |
| 123 | <button |
| 124 | ref={triggerRef} |
| 125 | onClick={() => setIsOpen(!isOpen)} |