Sifa professional network frontend (Next.js, React, TailwindCSS)
sifa.id/
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}