"use client"; /** * UserMenu — the right-most item in SiteHeader. Three states: * * 1. Loading (auth context hasn't hydrated) — render a quiet placeholder * so the row width doesn't jump when the user data arrives. * 2. Logged out — single "Log in" icon/button. * 3. Logged in — avatar / initials chip that opens a popover with * "Profile" and "Log out". * * Why hand-rolled instead of `@base-ui/react/popover`: outside-click * dismissal was unreliable for the avatar trigger here. The pattern below * mirrors `ReadingSettings`: mousedown listener for outside clicks, keyup * for Escape. That's already proven across the site and stays simple. */ import { useEffect, useRef, useState } from "react"; import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { UserRound } from "lucide-react"; import { useAuth } from "./auth-provider"; import { cn } from "@/lib/utils"; function initials(name: string): string { return ( name .split(/\s+/) .map((p) => p[0]) .slice(0, 2) .join("") .toUpperCase() || "?" ); } export function UserMenu() { const { user, loading, logOut } = useAuth(); const router = useRouter(); const [open, setOpen] = useState(false); const panelRef = useRef(null); const triggerRef = useRef(null); // Close on outside click + Escape, mirroring ReadingSettings. useEffect(() => { if (!open) return; function onMouseDown(e: MouseEvent) { if ( panelRef.current && !panelRef.current.contains(e.target as Node) && triggerRef.current && !triggerRef.current.contains(e.target as Node) ) { setOpen(false); } } function onKey(e: KeyboardEvent) { if (e.key === "Escape") setOpen(false); } document.addEventListener("mousedown", onMouseDown); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onMouseDown); document.removeEventListener("keydown", onKey); }; }, [open]); if (loading) { // Placeholder keeps the action row width stable until auth hydrates. return ; } if (!user) { // Mobile: compact icon button so the header row fits on 375px viewports. // Desktop: full "Log in" text — there's plenty of space. return ( Log in ); } async function onLogOut() { setOpen(false); await logOut(); router.push("/"); router.refresh(); } return (
{open ? (

{user.name}

{user.email}

setOpen(false)} className="block rounded-lg px-3 py-2 text-sm text-foreground hover:bg-muted transition-colors" > Profile
) : null}
); }