Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/
at main 113 lines 3.7 kB view raw
1'use client'; 2 3import Link from 'next/link'; 4import Image from 'next/image'; 5import { useTranslations } from 'next-intl'; 6import { ArrowSquareIn, SignOut, User } from '@phosphor-icons/react'; 7import { useAuth } from '@/components/auth-provider'; 8import { getLoginUrl, logout } from '@/lib/auth'; 9import { useState, useRef, useEffect } from 'react'; 10 11export function UserMenu() { 12 const { session, isLoading, refresh } = useAuth(); 13 const t = useTranslations('common'); 14 const [open, setOpen] = useState(false); 15 16 const handleLogout = async () => { 17 setOpen(false); 18 await logout(); 19 await refresh(); 20 }; 21 const menuRef = useRef<HTMLDivElement>(null); 22 23 useEffect(() => { 24 const handleClickOutside = (e: MouseEvent) => { 25 if (menuRef.current && !menuRef.current.contains(e.target as Node)) { 26 setOpen(false); 27 } 28 }; 29 if (open) { 30 document.addEventListener('mousedown', handleClickOutside); 31 } 32 return () => document.removeEventListener('mousedown', handleClickOutside); 33 }, [open]); 34 35 if (isLoading) { 36 return <div className="h-8 w-8 animate-pulse rounded-full bg-muted" />; 37 } 38 39 if (!session) { 40 return ( 41 <a 42 href={getLoginUrl()} 43 className="inline-flex h-8 items-center justify-center rounded-md border border-border bg-background px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-foreground" 44 > 45 {t('signIn')} 46 </a> 47 ); 48 } 49 50 return ( 51 <div className="relative" ref={menuRef}> 52 <button 53 type="button" 54 onClick={() => setOpen(!open)} 55 className="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" 56 aria-expanded={open} 57 aria-haspopup="true" 58 aria-label="User menu" 59 > 60 {session.avatar ? ( 61 <Image 62 src={session.avatar} 63 alt="" 64 width={32} 65 height={32} 66 className="h-8 w-8 rounded-full object-cover" 67 /> 68 ) : ( 69 <User className="h-4 w-4 text-muted-foreground" weight="bold" aria-hidden="true" /> 70 )} 71 </button> 72 73 {open && ( 74 <div 75 className="absolute right-0 top-10 z-50 w-48 rounded-md border border-border bg-card py-1 shadow-lg" 76 role="menu" 77 > 78 <div className="border-b border-border px-3 py-2"> 79 <p className="truncate text-sm font-medium">{session.displayName ?? session.handle}</p> 80 <p className="truncate text-xs text-muted-foreground">@{session.handle}</p> 81 </div> 82 <Link 83 href={`/p/${session.handle}`} 84 className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-accent" 85 role="menuitem" 86 onClick={() => setOpen(false)} 87 > 88 <User className="h-4 w-4" weight="bold" aria-hidden="true" /> 89 View profile 90 </Link> 91 <Link 92 href="/import" 93 className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-accent" 94 role="menuitem" 95 onClick={() => setOpen(false)} 96 > 97 <ArrowSquareIn className="h-4 w-4" weight="bold" aria-hidden="true" /> 98 {t('import')} 99 </Link> 100 <button 101 type="button" 102 onClick={handleLogout} 103 className="flex w-full items-center gap-2 px-3 py-2 text-sm text-destructive hover:bg-accent" 104 role="menuitem" 105 > 106 <SignOut className="h-4 w-4" weight="bold" aria-hidden="true" /> 107 {t('signOut')} 108 </button> 109 </div> 110 )} 111 </div> 112 ); 113}