11import { NavLink , Link } from "react-router-dom" ;
2- import { useState , useContext } from "react" ;
2+ import { useEffect , useMemo , useRef , useState , useContext } from "react" ;
33import { ThemeContext } from "../context/ThemeContext" ;
4- import { Moon , Sun , Menu , X , User } from "lucide-react" ;
4+ import { Moon , Sun , Menu , X , ChevronDown , BadgeInfo , LogOut , User } from "lucide-react" ;
5+
6+ type NavbarUser = {
7+ id ?: string ;
8+ username ?: string ;
9+ email ?: string ;
10+ } ;
11+
12+ const AUTH_STORAGE_KEY = "github_tracker_auth_user" ;
13+
14+ const readStoredUser = ( ) : NavbarUser | null => {
15+ if ( typeof window === "undefined" ) {
16+ return null ;
17+ }
18+
19+ const storedUser = window . localStorage . getItem ( AUTH_STORAGE_KEY ) ;
20+
21+ if ( ! storedUser ) {
22+ return null ;
23+ }
24+
25+ try {
26+ const parsedUser = JSON . parse ( storedUser ) as NavbarUser ;
27+ return parsedUser ?. username ? parsedUser : null ;
28+ } catch {
29+ return null ;
30+ }
31+ } ;
32+
33+ type ProfileDropdownProps = {
34+ user : NavbarUser ;
35+ onLogout : ( ) => void ;
36+ onCloseMenu ?: ( ) => void ;
37+ mobile ?: boolean ;
38+ } ;
39+
40+ const ProfileDropdown : React . FC < ProfileDropdownProps > = ( { user, onLogout, onCloseMenu, mobile = false } ) => {
41+ const [ isOpen , setIsOpen ] = useState ( false ) ;
42+ const profileMenuRef = useRef < HTMLDivElement | null > ( null ) ;
43+ const displayName = useMemo ( ( ) => user . username ?? "Profile" , [ user . username ] ) ;
44+
45+ useEffect ( ( ) => {
46+ const handleOutsideClick = ( event : MouseEvent ) => {
47+ if ( profileMenuRef . current && ! profileMenuRef . current . contains ( event . target as Node ) ) {
48+ setIsOpen ( false ) ;
49+ }
50+ } ;
51+
52+ document . addEventListener ( "mousedown" , handleOutsideClick ) ;
53+ return ( ) => document . removeEventListener ( "mousedown" , handleOutsideClick ) ;
54+ } , [ ] ) ;
55+
56+ const closeMenu = ( ) => setIsOpen ( false ) ;
57+
58+ if ( mobile ) {
59+ return (
60+ < div className = "mt-2 rounded-3xl border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800/60 p-4" >
61+ < div className = "flex items-center gap-3" >
62+ < div className = "flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-600 to-cyan-500 text-white font-semibold" >
63+ { displayName . charAt ( 0 ) . toUpperCase ( ) }
64+ </ div >
65+ < div >
66+ < p className = "font-semibold text-slate-900 dark:text-white" > { displayName } </ p >
67+ < p className = "text-sm text-slate-500 dark:text-slate-400" > { user . email ?? "Signed in" } </ p >
68+ </ div >
69+ </ div >
70+
71+ < div className = "mt-4 flex flex-col gap-2" >
72+ < Link
73+ to = "/profile"
74+ onClick = { onCloseMenu }
75+ className = "flex items-center gap-2 rounded-2xl px-4 py-3 text-sm font-medium text-slate-700 transition hover:bg-white dark:text-slate-200 dark:hover:bg-white/5"
76+ >
77+ < User className = "h-4 w-4" />
78+ View Profile
79+ </ Link >
80+ < Link
81+ to = { user . username ? `/contributor/${ user . username } ` : "/contributors" }
82+ onClick = { onCloseMenu }
83+ className = "flex items-center gap-2 rounded-2xl px-4 py-3 text-sm font-medium text-slate-700 transition hover:bg-white dark:text-slate-200 dark:hover:bg-white/5"
84+ >
85+ < BadgeInfo className = "h-4 w-4" />
86+ Account Details
87+ </ Link >
88+ < button
89+ onClick = { onLogout }
90+ className = "flex items-center gap-2 rounded-2xl px-4 py-3 text-sm font-medium text-red-600 transition hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-500/10"
91+ >
92+ < LogOut className = "h-4 w-4" />
93+ Logout
94+ </ button >
95+ </ div >
96+ </ div >
97+ ) ;
98+ }
99+
100+ return (
101+ < div className = "relative" ref = { profileMenuRef } >
102+ < button
103+ onClick = { ( ) => setIsOpen ( ( prev ) => ! prev ) }
104+ className = "flex items-center gap-3 rounded-2xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 px-3 py-2 text-left transition hover:border-blue-300 hover:bg-blue-50 dark:hover:bg-gray-700"
105+ aria-haspopup = "menu"
106+ aria-expanded = { isOpen }
107+ >
108+ < span className = "flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-600 to-cyan-500 text-white shadow-md" >
109+ { displayName . charAt ( 0 ) . toUpperCase ( ) }
110+ </ span >
111+ < span className = "hidden xl:block" >
112+ < span className = "block text-sm font-semibold text-slate-900 dark:text-white" > { displayName } </ span >
113+ < span className = "block text-xs text-slate-500 dark:text-slate-400" > Signed in</ span >
114+ </ span >
115+ < ChevronDown className = { `h-4 w-4 text-slate-500 transition-transform ${ isOpen ? "rotate-180" : "" } ` } />
116+ </ button >
117+
118+ { isOpen && (
119+ < div className = "absolute right-0 mt-3 w-72 overflow-hidden rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200" >
120+ < div className = "px-5 py-4 border-b border-gray-100 dark:border-gray-800" >
121+ < p className = "text-xs uppercase tracking-[0.2em] text-slate-400" > Account</ p >
122+ < div className = "mt-2 flex items-center gap-3" >
123+ < div className = "flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-600 to-cyan-500 text-white font-semibold" >
124+ { displayName . charAt ( 0 ) . toUpperCase ( ) }
125+ </ div >
126+ < div >
127+ < p className = "font-semibold text-slate-900 dark:text-white" > { displayName } </ p >
128+ < p className = "text-sm text-slate-500 dark:text-slate-400" > { user . email ?? "No email available" } </ p >
129+ </ div >
130+ </ div >
131+ </ div >
132+
133+ < div className = "p-2" >
134+ < Link
135+ to = "/profile"
136+ onClick = { closeMenu }
137+ className = "flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium text-slate-700 transition hover:bg-blue-50 hover:text-blue-700 dark:text-slate-300 dark:hover:bg-white/5 dark:hover:text-cyan-300"
138+ >
139+ < User className = "h-4 w-4" />
140+ View Profile
141+ </ Link >
142+ < Link
143+ to = { user . username ? `/contributor/${ user . username } ` : "/contributors" }
144+ onClick = { closeMenu }
145+ className = "flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium text-slate-700 transition hover:bg-blue-50 hover:text-blue-700 dark:text-slate-300 dark:hover:bg-white/5 dark:hover:text-cyan-300"
146+ >
147+ < BadgeInfo className = "h-4 w-4" />
148+ Account Details
149+ </ Link >
150+ < button
151+ onClick = { onLogout }
152+ className = "flex w-full items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium text-red-600 transition hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-500/10"
153+ >
154+ < LogOut className = "h-4 w-4" />
155+ Logout
156+ </ button >
157+ </ div >
158+ </ div >
159+ ) }
160+ </ div >
161+ ) ;
162+ } ;
5163
6164const Navbar : React . FC = ( ) => {
7165 const [ isOpen , setIsOpen ] = useState ( false ) ;
166+ const [ user , setUser ] = useState < NavbarUser | null > ( ( ) => readStoredUser ( ) ) ;
8167
9168 const themeContext = useContext ( ThemeContext ) ;
10169
@@ -20,6 +179,13 @@ const Navbar: React.FC = () => {
20179 } `;
21180
22181 const closeMenu = ( ) => setIsOpen ( false ) ;
182+ const handleLogout = ( ) => {
183+ if ( typeof window !== "undefined" ) {
184+ window . localStorage . removeItem ( AUTH_STORAGE_KEY ) ;
185+ }
186+ setUser ( null ) ;
187+ closeMenu ( ) ;
188+ } ;
23189
24190 return (
25191 < nav className = "sticky top-0 z-50 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 transition-colors duration-300 backdrop-blur" >
@@ -42,12 +208,15 @@ const Navbar: React.FC = () => {
42208 < NavLink to = "/contributors" className = { navLinkStyles } >
43209 Contributors
44210 </ NavLink >
45- < NavLink to = "/profile" className = { navLinkStyles } >
46- Profile
47- </ NavLink >
48- < NavLink to = "/login" className = { navLinkStyles } >
49- Login
50- </ NavLink >
211+
212+ { user ? (
213+ < ProfileDropdown user = { user } onLogout = { handleLogout } />
214+ ) : (
215+ < NavLink to = "/login" className = { navLinkStyles } >
216+ Login
217+ </ NavLink >
218+ ) }
219+
51220 < button
52221 onClick = { toggleTheme }
53222 className = "ml-2 p-2 rounded-xl border border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
@@ -99,12 +268,14 @@ const Navbar: React.FC = () => {
99268 < NavLink to = "/contributors" className = { navLinkStyles } onClick = { closeMenu } >
100269 Contributors
101270 </ NavLink >
102- < NavLink to = "/profile" className = { navLinkStyles } onClick = { closeMenu } >
103- < span className = "inline-flex items-center gap-2" > < User className = "h-4 w-4" /> Profile</ span >
104- </ NavLink >
105- < NavLink to = "/login" className = { navLinkStyles } onClick = { closeMenu } >
106- Login
107- </ NavLink >
271+
272+ { user ? (
273+ < ProfileDropdown user = { user } onLogout = { handleLogout } onCloseMenu = { closeMenu } mobile />
274+ ) : (
275+ < NavLink to = "/login" className = { navLinkStyles } onClick = { closeMenu } >
276+ Login
277+ </ NavLink >
278+ ) }
108279 </ div >
109280 </ div >
110281 ) }
0 commit comments