NeonClary's picture
Workshop nav, experiment editing, and saved chats
31091fd verified
Raw History Blame Contribute Delete
5.69 kB
import { useState, useRef, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import { useTutorialOptional } from '../context/TutorialContext'
import { SampleDataMenuItem } from './SampleDataMenuItem'
import { SpeechSettingsMenuItem } from './SpeechSettingsPanel'
type Props = {
onProfile: () => void
onAccount: () => void
onClearData: () => void
}
/**
* Account menu: hover opens transiently; click pins until click again,
* menu item action, Escape, or outside click.
*/
export function UserMenu({ onProfile, onAccount, onClearData }: Props) {
const { user, signOut } = useAuth()
const tutorial = useTutorialOptional()
const [hoverOpen, setHoverOpen] = useState(false)
const [pinned, setPinned] = useState(false)
const [speechOpen, setSpeechOpen] = useState(false)
const [sampleDataOpen, setSampleDataOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const open = pinned || hoverOpen
const closeAll = () => {
setPinned(false)
setHoverOpen(false)
setSpeechOpen(false)
setSampleDataOpen(false)
}
useEffect(() => {
if (!open) return
function onPointerDown(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
closeAll()
}
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') closeAll()
}
document.addEventListener('mousedown', onPointerDown)
document.addEventListener('keydown', onKeyDown)
return () => {
document.removeEventListener('mousedown', onPointerDown)
document.removeEventListener('keydown', onKeyDown)
}
}, [open])
const label = user ? user.firstName || user.email : 'Guest'
return (
<div
className="user-menu-wrap"
ref={ref}
onMouseEnter={() => setHoverOpen(true)}
onMouseLeave={() => {
if (!pinned) {
setHoverOpen(false)
setSpeechOpen(false)
setSampleDataOpen(false)
}
}}
>
<button
type="button"
className={`user-menu-trigger${open ? ' user-menu-trigger--open' : ''}`}
onClick={() => {
const next = !pinned
setPinned(next)
setHoverOpen(next)
if (!next) {
setSpeechOpen(false)
setSampleDataOpen(false)
}
}}
aria-expanded={open}
aria-haspopup="menu"
aria-label="Account menu"
>
<span className="user-menu-name">{label}</span>
<span className="user-menu-kebab" aria-hidden title="Menu">
⋮
</span>
</button>
{open && (
<div className="user-dropdown" role="menu">
<button
type="button"
role="menuitem"
onClick={() => {
closeAll()
onProfile()
}}
>
Profile
</button>
<button
type="button"
role="menuitem"
onClick={() => {
closeAll()
if (user) onAccount()
}}
disabled={!user}
title={!user ? 'Sign in to change account settings' : undefined}
style={!user ? { opacity: 0.55, cursor: 'not-allowed' } : undefined}
>
Account
</button>
<button
type="button"
role="menuitem"
onClick={() => {
closeAll()
onClearData()
}}
>
Clear User Data
</button>
<SampleDataMenuItem
open={sampleDataOpen}
onToggle={() => {
setSampleDataOpen((s) => !s)
setSpeechOpen(false)
}}
onActionComplete={closeAll}
/>
<div className="user-dropdown-divider" role="presentation" />
{tutorial && (
<>
<button
type="button"
role="menuitem"
onClick={() => {
closeAll()
tutorial.turnOnTutorial()
}}
>
Turn on Tutorial
</button>
<button
type="button"
role="menuitem"
onClick={() => {
closeAll()
tutorial.removeTutorialButton()
}}
>
Remove Tutorial Button
</button>
<div className="user-dropdown-divider" role="presentation" />
</>
)}
<SpeechSettingsMenuItem
open={speechOpen}
onToggle={() => {
setSpeechOpen((s) => !s)
setSampleDataOpen(false)
}}
/>
{user ? (
<button
type="button"
role="menuitem"
className="danger"
onClick={() => {
closeAll()
signOut()
}}
>
Sign out
</button>
) : (
<Link
to="/auth"
role="menuitem"
className="user-dropdown-link"
onClick={closeAll}
style={{ display: 'block', padding: '0.5rem 1rem', textDecoration: 'none', color: 'inherit' }}
>
Sign In / Create Account
</Link>
)}
</div>
)}
</div>
)
}