Skip to content

Commit 8e9e03d

Browse files
committed
update nav for mobile
1 parent c99eeef commit 8e9e03d

File tree

2 files changed

+210
-55
lines changed

2 files changed

+210
-55
lines changed

src/components/Navigation.tsx

Lines changed: 198 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { useDB } from '@/context/DBContext'
22
import { signOut } from 'firebase/auth'
33
import { 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'
66
import {
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+
4191
const 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
}

src/index.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,15 @@ body {
7575
transform: rotate(1turn);
7676
}
7777
}
78+
79+
/* Mobile Navigation Popover */
80+
#mobile-nav-menu {
81+
&::backdrop {
82+
background: rgba(0, 0, 0, 0.4);
83+
opacity: 0;
84+
transition: opacity 300ms ease-out;
85+
}
86+
&:popover-open::backdrop {
87+
opacity: 1;
88+
}
89+
}

0 commit comments

Comments
 (0)