cropintel / components /AccountMenu.tsx
Jaithra Polavarapu
feat: add Firebase auth/farms backend alongside existing frontend
fb030a8
Raw
History Blame Contribute Delete
6.36 kB
'use client'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useEffect, useMemo, useRef, useState } from 'react'
import { ChevronDown, Loader2, LogOut, Settings, Sprout, Tractor, UserCircle } from 'lucide-react'
import type { User } from 'firebase/auth'
import { signOutUser } from '@/src/lib/auth'
type AccountMenuProps = {
user: User | null
loading?: boolean
}
function getAccountName(user: User | null) {
return user?.displayName?.trim() || user?.email?.split('@')[0]?.trim() || 'CropIntel user'
}
function getInitials(name: string, email: string) {
const source = name !== 'CropIntel user' ? name : email
const parts = source
.replace(/@.*/, '')
.split(/[\s._-]+/)
.filter(Boolean)
if (parts.length === 0) return 'CI'
return parts
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join('')
}
export default function AccountMenu({ user, loading = false }: AccountMenuProps) {
const router = useRouter()
const menuRef = useRef<HTMLDivElement | null>(null)
const [open, setOpen] = useState(false)
const [signingOut, setSigningOut] = useState(false)
const email = user?.email?.trim() || 'No email on file'
const name = getAccountName(user)
const initials = useMemo(() => getInitials(name, email), [name, email])
useEffect(() => {
if (!open) return
const handlePointerDown = (event: PointerEvent) => {
if (!menuRef.current?.contains(event.target as Node)) {
setOpen(false)
}
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') setOpen(false)
}
document.addEventListener('pointerdown', handlePointerDown)
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('pointerdown', handlePointerDown)
document.removeEventListener('keydown', handleKeyDown)
}
}, [open])
const handleSignOut = async () => {
setSigningOut(true)
try {
await signOutUser()
router.replace('/login')
} finally {
setSigningOut(false)
setOpen(false)
}
}
if (loading) {
return (
<div className="flex min-h-[44px] items-center gap-2 rounded-full border border-ink/10 bg-surface/70 px-2.5 py-1.5 shadow-sm">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-field-cream text-leaf-deep">
<Loader2 className="h-4 w-4 animate-spin" aria-label="Loading account" />
</div>
<div className="hidden min-w-0 sm:block">
<div className="h-3 w-24 rounded-full bg-ink/10" />
<div className="mt-1.5 h-2.5 w-32 rounded-full bg-ink/10" />
</div>
</div>
)
}
return (
<div ref={menuRef} className="relative z-50">
<button
type="button"
onClick={() => setOpen((current) => !current)}
aria-haspopup="menu"
aria-expanded={open}
aria-label={`Open account menu for ${name}`}
className="flex min-h-[44px] touch-manipulation items-center gap-1.5 rounded-full border border-ink/10 bg-surface/80 px-1.5 py-1.5 text-left shadow-sm backdrop-blur transition hover:border-leaf/30 hover:bg-white focus:outline-none focus:ring-4 focus:ring-primary-200/60"
>
<span
aria-hidden="true"
className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full bg-ink text-sm font-extrabold text-white"
>
{initials}
</span>
<ChevronDown className={`h-4 w-4 shrink-0 text-ink-soft transition ${open ? 'rotate-180' : ''}`} />
</button>
{open && (
<div
role="menu"
className="absolute right-0 mt-2 w-[min(20rem,calc(100vw-2rem))] overflow-hidden rounded-2xl border border-white/80 bg-surface shadow-[0_24px_60px_-28px_rgba(18,38,28,0.45)] ring-1 ring-ink/5"
>
<div className="border-b border-ink/10 p-4">
<div className="flex items-center gap-3">
<span
aria-hidden="true"
className="flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-full bg-ink text-sm font-extrabold text-white"
>
{initials}
</span>
<div className="min-w-0">
<p className="truncate text-sm font-extrabold text-ink">{name}</p>
<p className="truncate text-xs font-semibold text-ink-soft">{email}</p>
</div>
</div>
</div>
<div className="p-2">
<Link href="/profile" role="menuitem" className="flex min-h-[44px] items-center gap-3 rounded-xl px-3 py-2 text-sm font-semibold text-ink transition hover:bg-field-cream">
<UserCircle className="h-4 w-4 text-leaf-deep" />
Profile
</Link>
<Link href="/farms" role="menuitem" className="flex min-h-[44px] items-center gap-3 rounded-xl px-3 py-2 text-sm font-semibold text-ink transition hover:bg-field-cream">
<Tractor className="h-4 w-4 text-leaf-deep" />
My Farms
</Link>
<Link href="/settings" role="menuitem" className="flex min-h-[44px] items-center gap-3 rounded-xl px-3 py-2 text-sm font-semibold text-ink transition hover:bg-field-cream">
<Settings className="h-4 w-4 text-leaf-deep" />
Settings
</Link>
<button
type="button"
role="menuitem"
onClick={handleSignOut}
disabled={signingOut}
className="flex min-h-[44px] w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-sm font-semibold text-rose-900 transition hover:bg-rose-50 disabled:cursor-not-allowed disabled:opacity-70"
>
{signingOut ? <Loader2 className="h-4 w-4 animate-spin" /> : <LogOut className="h-4 w-4" />}
{signingOut ? 'Signing out...' : 'Sign Out'}
</button>
</div>
{!user && (
<div className="flex items-start gap-2 border-t border-amber/20 bg-field-cream px-4 py-3 text-xs font-semibold text-amber-deep">
<Sprout className="mt-0.5 h-4 w-4 shrink-0" />
Account details are not available yet.
</div>
)}
</div>
)}
</div>
)
}