11import { useDB } from '@/context/DBContext'
22import { signOut } from 'firebase/auth'
33import { Link , useLocation } from 'react-router'
4- import React , { useState } from 'react'
5- import { LucideBarChart3 , LucideExternalLink , LucideMap , LucideUser , LucideChevronDown } from 'lucide-react'
4+ import React , { useState , useRef } from 'react'
5+ import { LucideBarChart3 , LucideExternalLink , LucideMap , LucideUser , LucideChevronDown , LucideMenu , LucideX } from 'lucide-react'
66import {
77 useFloating ,
88 offset ,
@@ -16,32 +16,87 @@ import {
1616 useTransitionStyles ,
1717} from '@floating-ui/react'
1818
19- const ExternalLink = ( { href, text } : { href : string ; text : string } ) => (
19+ const ExternalLink = ( { href, text, onClick } : { href : string ; text : string ; onClick ?: ( ) => void } ) => (
2020 < a
2121 href = { href }
2222 target = "_blank"
2323 rel = "noopener noreferrer"
24+ onClick = { onClick }
2425 className = "flex items-center gap-1 rounded-full border px-3 py-1.5 text-black/80 transition hover:border-gray-400 hover:bg-gray-200 hover:text-black"
2526 >
2627 { text }
2728 < LucideExternalLink height = { 12 } width = { 12 } />
2829 </ a >
2930)
3031
31- const NavLink = ( { to, icon, text, isActive } : { to : string ; icon ?: React . ReactNode ; text : string ; isActive : boolean } ) => (
32+ const NavLink = ( {
33+ to,
34+ icon,
35+ text,
36+ isActive,
37+ onClick,
38+ } : {
39+ to : string
40+ icon ?: React . ReactNode
41+ text : string
42+ isActive : boolean
43+ onClick ?: ( ) => void
44+ } ) => (
3245 < Link
3346 to = { to }
47+ onClick = { onClick }
3448 className = { `flex items-center gap-1 rounded-full border px-3 py-1.5 transition ${ isActive ? 'border-blue-600 bg-blue-100 text-blue-600 shadow-md' : 'text-black/80 hover:border-gray-400 hover:bg-gray-200 hover:text-black' } ` }
3549 >
3650 { icon }
3751 { text }
3852 </ Link >
3953)
4054
55+ const MobileNavLink = ( {
56+ to,
57+ icon,
58+ text,
59+ isActive,
60+ onClick,
61+ } : {
62+ to : string
63+ icon ?: React . ReactNode
64+ text : string
65+ isActive : boolean
66+ onClick ?: ( ) => void
67+ } ) => (
68+ < Link
69+ to = { to }
70+ onClick = { onClick }
71+ className = { `flex items-center gap-3 px-4 py-3 text-sm transition ${ isActive ? 'bg-blue-50 font-medium text-blue-600' : 'text-gray-700 hover:bg-gray-100' } ` }
72+ >
73+ { icon }
74+ { text }
75+ </ Link >
76+ )
77+
78+ const MobileExternalLink = ( { href, text, onClick } : { href : string ; text : string ; onClick ?: ( ) => void } ) => (
79+ < a
80+ href = { href }
81+ target = "_blank"
82+ rel = "noopener noreferrer"
83+ onClick = { onClick }
84+ className = "flex items-center gap-3 px-4 py-3 text-sm text-gray-700 transition hover:bg-gray-100"
85+ >
86+ { text }
87+ < LucideExternalLink height = { 14 } width = { 14 } />
88+ </ a >
89+ )
90+
4191const Navigation = ( ) => {
4292 const { isLoggedIn, isAdmin, auth } = useDB ( )
4393 const { pathname } = useLocation ( )
4494 const [ isDropdownOpen , setIsDropdownOpen ] = useState ( false )
95+ const mobileMenuRef = useRef < HTMLDivElement > ( null )
96+
97+ const closeMobileMenu = ( ) => {
98+ mobileMenuRef . current ?. hidePopover ( )
99+ }
45100
46101 const { refs, floatingStyles, context } = useFloating ( {
47102 open : isDropdownOpen ,
@@ -70,63 +125,151 @@ const Navigation = () => {
70125 < Link to = "/" >
71126 < img src = "/wordmark.png" alt = "Red Coral" className = "h-6 w-auto pr-4" />
72127 </ Link >
73- < NavLink to = "/" text = "Mapa" icon = { < LucideMap height = { 15 } width = { 15 } /> } isActive = { pathname === '/' } />
74- < NavLink to = "/stats" text = "Estadísticas" icon = { < LucideBarChart3 height = { 15 } width = { 15 } /> } isActive = { pathname === '/stats' } />
75- { isAdmin && (
76- < >
77- < div className = "mx-3 h-3 border-l-2 border-gray-400" />
78- < NavLink to = "/admin/dash" text = "Administrar categorías" isActive = { pathname === '/admin/dash' } />
79- < NavLink to = "/admin/publish" text = "Publicar Datos" isActive = { pathname === '/admin/publish' } />
80- < NavLink to = "/admin/analytics" text = "Analítica web" isActive = { pathname === '/admin/analytics' } />
81- </ >
82- ) }
83- < div className = "flex-grow" />
84- < ExternalLink href = "https://redcoralmap.org/guia-metodologia" text = "Guía de Metodología" />
85- < NavLink to = "/about" text = "Acerca de" isActive = { pathname === '/about' } />
86- { isLoggedIn ? (
87- < >
88- < button
89- ref = { refs . setReference }
90- { ...getReferenceProps ( ) }
91- className = { `flex items-center gap-1 rounded-full border px-3 py-1.5 transition ${ isDropdownOpen ? 'border-blue-600 bg-blue-100 text-blue-600 shadow-md' : 'text-black/80 hover:border-gray-400 hover:bg-gray-200 hover:text-black' } ` }
92- >
93- < LucideUser height = { 16 } width = { 16 } />
94- Cuenta
95- < LucideChevronDown height = { 14 } width = { 14 } className = { `transition-transform ${ isDropdownOpen ? 'rotate-180' : '' } ` } />
96- </ button >
97128
98- { isMounted && (
99- < FloatingFocusManager context = { context } modal = { false } >
100- < div ref = { refs . setFloating } style = { floatingStyles } { ...getFloatingProps ( ) } >
101- < div style = { transitionStyles } >
102- < div className = "min-w-[200px] rounded-lg border border-gray-400 bg-white shadow-lg" >
103- < div className = "border-b border-gray-200 px-4 py-2" >
104- < p className = "truncate text-xs text-gray-600" > { auth . currentUser ?. email } </ p >
129+ { /* Desktop Navigation */ }
130+ < div className = "hidden md:flex md:flex-1 md:items-center md:gap-1" >
131+ < NavLink to = "/" text = "Mapa" icon = { < LucideMap height = { 15 } width = { 15 } /> } isActive = { pathname === '/' } />
132+ < NavLink to = "/stats" text = "Estadísticas" icon = { < LucideBarChart3 height = { 15 } width = { 15 } /> } isActive = { pathname === '/stats' } />
133+ { isAdmin && (
134+ < >
135+ < div className = "mx-3 h-3 border-l-2 border-gray-400" />
136+ < NavLink to = "/admin/dash" text = "Administrar categorías" isActive = { pathname === '/admin/dash' } />
137+ < NavLink to = "/admin/publish" text = "Publicar Datos" isActive = { pathname === '/admin/publish' } />
138+ < NavLink to = "/admin/analytics" text = "Analítica web" isActive = { pathname === '/admin/analytics' } />
139+ </ >
140+ ) }
141+ < div className = "flex-grow" />
142+ < ExternalLink href = "https://redcoralmap.org/guia-metodologia" text = "Guía de Metodología" />
143+ < NavLink to = "/about" text = "Acerca de" isActive = { pathname === '/about' } />
144+ { isLoggedIn ? (
145+ < >
146+ < button
147+ ref = { refs . setReference }
148+ { ...getReferenceProps ( ) }
149+ className = { `flex items-center gap-1 rounded-full border px-3 py-1.5 transition ${ isDropdownOpen ? 'border-blue-600 bg-blue-100 text-blue-600 shadow-md' : 'text-black/80 hover:border-gray-400 hover:bg-gray-200 hover:text-black' } ` }
150+ >
151+ < LucideUser height = { 16 } width = { 16 } />
152+ Cuenta
153+ < LucideChevronDown height = { 14 } width = { 14 } className = { `transition-transform ${ isDropdownOpen ? 'rotate-180' : '' } ` } />
154+ </ button >
155+
156+ { isMounted && (
157+ < FloatingFocusManager context = { context } modal = { false } >
158+ < div ref = { refs . setFloating } style = { floatingStyles } { ...getFloatingProps ( ) } >
159+ < div style = { transitionStyles } >
160+ < div className = "min-w-[200px] rounded-lg border border-gray-400 bg-white shadow-lg" >
161+ < div className = "border-b border-gray-200 px-4 py-2" >
162+ < p className = "truncate text-xs text-gray-600" > { auth . currentUser ?. email } </ p >
163+ </ div >
164+ < ul className = "py-1" >
165+ < li >
166+ < button
167+ onClick = { ( ) => {
168+ signOut ( auth )
169+ setIsDropdownOpen ( false )
170+ } }
171+ className = "w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50"
172+ >
173+ Cerrar sesión
174+ </ button >
175+ </ li >
176+ </ ul >
105177 </ div >
106- < ul className = "py-1" >
107- < li >
108- < button
109- onClick = { ( ) => {
110- signOut ( auth )
111- setIsDropdownOpen ( false )
112- } }
113- className = "w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50"
114- >
115- Cerrar sesión
116- </ button >
117- </ li >
118- </ ul >
119178 </ div >
120179 </ div >
180+ </ FloatingFocusManager >
181+ ) }
182+ </ >
183+ ) : (
184+ < NavLink to = "/login" text = "Registrarse" icon = { < LucideUser height = { 16 } width = { 16 } /> } isActive = { pathname === '/login' } />
185+ ) }
186+ </ div >
187+
188+ { /* Mobile Menu Button */ }
189+ < div className = "flex flex-1 justify-end md:hidden" >
190+ < button
191+ // @ts -expect-error - popoverTarget is valid HTML but not yet in React types
192+ popovertarget = "mobile-nav-menu"
193+ className = "flex items-center justify-center rounded-lg p-2 text-gray-700 transition hover:bg-gray-100"
194+ aria-label = "Abrir menú"
195+ >
196+ < LucideMenu height = { 24 } width = { 24 } />
197+ </ button >
198+ </ div >
199+
200+ { /* Mobile Slide-over Menu */ }
201+ < div
202+ ref = { mobileMenuRef }
203+ popover = "auto"
204+ id = "mobile-nav-menu"
205+ className = "fixed inset-0 m-0 h-screen w-[280px] max-w-[80vw] translate-x-[-100vw] border-r border-gray-300 bg-white p-0 shadow-xl transition-transform duration-300 ease-out [&:popover-open]:translate-x-0"
206+ >
207+ < div className = "flex h-12 items-center justify-between border-b border-gray-200 px-4" >
208+ < Link to = "/" onClick = { closeMobileMenu } >
209+ < img src = "/wordmark.png" alt = "Red Coral" className = "h-6 w-auto" />
210+ </ Link >
211+ < button
212+ // @ts -expect-error - popoverTarget is valid HTML but not yet in React types
213+ popovertarget = "mobile-nav-menu"
214+ popovertargetaction = "hide"
215+ className = "flex items-center justify-center rounded-lg p-2 text-gray-700 transition hover:bg-gray-100"
216+ aria-label = "Cerrar menú"
217+ >
218+ < LucideX height = { 20 } width = { 20 } />
219+ </ button >
220+ </ div >
221+
222+ < nav className = "flex flex-col py-2" >
223+ < MobileNavLink to = "/" text = "Mapa" icon = { < LucideMap height = { 18 } width = { 18 } /> } isActive = { pathname === '/' } onClick = { closeMobileMenu } />
224+ < MobileNavLink
225+ to = "/stats"
226+ text = "Estadísticas"
227+ icon = { < LucideBarChart3 height = { 18 } width = { 18 } /> }
228+ isActive = { pathname === '/stats' }
229+ onClick = { closeMobileMenu }
230+ />
231+
232+ { isAdmin && (
233+ < >
234+ < div className = "my-2 border-t border-gray-200" />
235+ < p className = "px-4 py-2 text-xs font-semibold uppercase tracking-wider text-gray-500" > Admin</ p >
236+ < MobileNavLink to = "/admin/dash" text = "Administrar categorías" isActive = { pathname === '/admin/dash' } onClick = { closeMobileMenu } />
237+ < MobileNavLink to = "/admin/publish" text = "Publicar Datos" isActive = { pathname === '/admin/publish' } onClick = { closeMobileMenu } />
238+ < MobileNavLink to = "/admin/analytics" text = "Analítica web" isActive = { pathname === '/admin/analytics' } onClick = { closeMobileMenu } />
239+ </ >
240+ ) }
241+
242+ < div className = "my-2 border-t border-gray-200" />
243+ < MobileExternalLink href = "https://redcoralmap.org/guia-metodologia" text = "Guía de Metodología" onClick = { closeMobileMenu } />
244+ < MobileNavLink to = "/about" text = "Acerca de" isActive = { pathname === '/about' } onClick = { closeMobileMenu } />
245+
246+ < div className = "my-2 border-t border-gray-200" />
247+ { isLoggedIn ? (
248+ < >
249+ < div className = "px-4 py-2" >
250+ < p className = "truncate text-xs text-gray-500" > { auth . currentUser ?. email } </ p >
121251 </ div >
122- </ FloatingFocusManager >
252+ < button
253+ onClick = { ( ) => {
254+ signOut ( auth )
255+ closeMobileMenu ( )
256+ } }
257+ className = "flex items-center gap-3 px-4 py-3 text-sm text-red-600 transition hover:bg-red-50"
258+ >
259+ Cerrar sesión
260+ </ button >
261+ </ >
262+ ) : (
263+ < MobileNavLink
264+ to = "/login"
265+ text = "Registrarse"
266+ icon = { < LucideUser height = { 18 } width = { 18 } /> }
267+ isActive = { pathname === '/login' }
268+ onClick = { closeMobileMenu }
269+ />
123270 ) }
124- </ >
125- ) : (
126- < >
127- < NavLink to = "/login" text = "Registrarse" icon = { < LucideUser height = { 16 } width = { 16 } /> } isActive = { pathname === '/login' } />
128- </ >
129- ) }
271+ </ nav >
272+ </ div >
130273 </ div >
131274 )
132275}
0 commit comments