Tesseract / studio /src /components /UserMenu.jsx
rkbosamia's picture
Tesseract major version upgrade - v2
9c49532
Raw
History Blame Contribute Delete
3.96 kB
/**
* Compact account dropdown in TopBar — shows the signed-in email and offers
* Logout. Closes on outside click; menu state is local since nothing else
* needs to pop it open.
*/
import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import useAppStore from '../store/appStore'
import { logout as apiLogout } from '../api/auth'
export default function UserMenu() {
const navigate = useNavigate()
const currentUser = useAppStore((s) => s.currentUser)
const setCurrentUser = useAppStore((s) => s.setCurrentUser)
const [open, setOpen] = useState(false)
const panelRef = useRef(null)
const buttonRef = useRef(null)
useEffect(() => {
if (!open) return
function onDown(e) {
if (panelRef.current?.contains(e.target)) return
if (buttonRef.current?.contains(e.target)) return
setOpen(false)
}
document.addEventListener('mousedown', onDown)
return () => document.removeEventListener('mousedown', onDown)
}, [open])
if (!currentUser) return null
// Avatar: first letter of first name (post-Phase 4.6 we collect this on
// signup) — falls back to the email initial for any legacy account that
// somehow doesn't have one set.
const avatar = (currentUser.first_name?.[0] ?? currentUser.email[0] ?? '?').toUpperCase()
const displayName = currentUser.first_name
? `${currentUser.first_name} ${currentUser.last_name ?? ''}`.trim()
: currentUser.email
async function onLogout() {
setOpen(false)
await apiLogout()
// Tear down the active session/project state so the WS effect that
// streams /api/projects/{id}/logs sees project.id → null and unsubscribes,
// and so the next login starts on a clean slate instead of a stale tree.
const { resetProject, setSessions, setActiveSessionId, setMessages } = useAppStore.getState()
resetProject()
setSessions([])
setActiveSessionId(null)
setMessages([])
setCurrentUser(null)
navigate('/', { replace: true })
}
return (
<div className="relative">
<button
ref={buttonRef}
onClick={() => setOpen((v) => !v)}
className="w-7 h-7 rounded-full grid place-items-center text-[11px] font-semibold transition-transform"
style={{
background: 'linear-gradient(135deg, var(--accent), var(--accent-lo))',
color: '#fff',
border: open ? '2px solid var(--accent-hi)' : '2px solid transparent',
boxSizing: 'border-box',
}}
title={currentUser.email}
aria-label="Account menu"
>
{avatar}
</button>
{open && (
<div
ref={panelRef}
className="absolute right-0 mt-2 w-[220px] rounded-lg border shadow-2xl z-50 fade-in"
style={{
background: 'var(--surface)',
borderColor: 'var(--border-2)',
boxShadow: '0 12px 36px rgba(0,0,0,0.5)',
}}
>
<div className="px-3 py-2.5 border-b" style={{ borderColor: 'var(--border)' }}>
<div className="text-[10px] uppercase tracking-wider" style={{ color: 'var(--text-faint)' }}>
Signed in as
</div>
<div className="text-[13px] truncate mt-0.5" style={{ color: 'var(--text)', fontWeight: 500 }}>
{displayName}
</div>
{currentUser.username && (
<div className="text-[11px] mono truncate" style={{ color: 'var(--text-faint)', marginTop: 2 }}>
@{currentUser.username} · {currentUser.email}
</div>
)}
</div>
<button
onClick={onLogout}
className="w-full text-left px-3 py-2 text-[12.5px] hover:bg-[var(--surface-2)] transition-colors"
style={{ color: 'var(--text)' }}
>
Sign out
</button>
</div>
)}
</div>
)
}