11'use client' ;
22
3- import { Home , LogIn , Menu , ShoppingBag , X } from 'lucide-react' ;
3+ import { BookOpen , LogIn , Menu , ShoppingBag , X } from 'lucide-react' ;
44import { useSearchParams } from 'next/navigation' ;
55import { useTranslations } from 'next-intl' ;
6- import { useEffect , useMemo , useState } from 'react' ;
6+ import { useEffect , useMemo } from 'react' ;
77
88import { LogoutButton } from '@/components/auth/logoutButton' ;
9+ import { useMobileMenu } from '@/components/header/MobileMenuContext' ;
910import { HeaderButton } from '@/components/shared/HeaderButton' ;
1011import { Link , usePathname } from '@/i18n/routing' ;
1112import { SITE_LINKS } from '@/lib/navigation' ;
@@ -32,6 +33,31 @@ export function AppMobileMenu({
3233 const tProducts = useTranslations ( 'shop.products' ) ;
3334 const tBlog = useTranslations ( 'blog' ) ;
3435 const pathname = usePathname ( ) ;
36+ const searchParams = useSearchParams ( ) ;
37+
38+ const {
39+ isOpen : open ,
40+ isAnimating,
41+ close,
42+ toggle,
43+ startNavigation,
44+ } = useMobileMenu ( ) ;
45+
46+ const currentCategory = searchParams . get ( 'category' ) ;
47+
48+ const handleLinkClick = (
49+ e : React . MouseEvent < HTMLAnchorElement > ,
50+ href : string
51+ ) => {
52+ e . preventDefault ( ) ;
53+ startNavigation ( href ) ;
54+ } ;
55+
56+ const handleHeaderButtonLinkClick =
57+ ( href : string ) => ( e : React . MouseEvent < HTMLAnchorElement > ) => {
58+ e . preventDefault ( ) ;
59+ startNavigation ( href ) ;
60+ } ;
3561
3662 const getBlogCategoryLabel = ( categoryName : string ) : string => {
3763 const key = categoryName . toLowerCase ( ) as
@@ -40,45 +66,16 @@ export function AppMobileMenu({
4066 | 'insights'
4167 | 'news'
4268 | 'growth' ;
43- const categoryTranslations : Record < string , string > = {
69+ const translations : Record < string , string > = {
4470 tech : tBlog ( 'categories.tech' ) ,
4571 career : tBlog ( 'categories.career' ) ,
4672 insights : tBlog ( 'categories.insights' ) ,
4773 news : tBlog ( 'categories.news' ) ,
4874 growth : tBlog ( 'categories.growth' ) ,
4975 } ;
50- return categoryTranslations [ key ] || categoryName ;
51- } ;
52- const searchParams = useSearchParams ( ) ;
53- const currentCategory = searchParams . get ( 'category' ) ;
54- const [ open , setOpen ] = useState ( false ) ;
55- const [ isAnimating , setIsAnimating ] = useState ( false ) ;
56-
57- const close = ( ) => {
58- setIsAnimating ( false ) ;
59- setTimeout ( ( ) => setOpen ( false ) , 200 ) ;
60- } ;
61-
62- const toggle = ( ) => {
63- if ( open ) {
64- close ( ) ;
65- } else {
66- setOpen ( true ) ;
67- setTimeout ( ( ) => setIsAnimating ( true ) , 10 ) ;
68- }
76+ return translations [ key ] || categoryName ;
6977 } ;
7078
71- useEffect ( ( ) => {
72- if ( ! open ) return ;
73-
74- const onKeyDown = ( e : KeyboardEvent ) => {
75- if ( e . key === 'Escape' ) close ( ) ;
76- } ;
77-
78- window . addEventListener ( 'keydown' , onKeyDown ) ;
79- return ( ) => window . removeEventListener ( 'keydown' , onKeyDown ) ;
80- } , [ open ] ) ;
81-
8279 const shopLinks = useMemo (
8380 ( ) => [
8481 { href : '/shop/products' , label : tProducts ( 'title' ) , category : null } ,
@@ -107,40 +104,43 @@ export function AppMobileMenu({
107104 return [ ] ;
108105 } , [ variant , shopLinks ] ) ;
109106
107+ const slugify = ( value : string ) =>
108+ value
109+ . toLowerCase ( )
110+ . trim ( )
111+ . replace ( / [ ^ a - z 0 - 9 \s - ] / g, '' )
112+ . replace ( / \s + / g, '-' ) ;
113+
114+ const linkClass = ( isActive : boolean ) =>
115+ `rounded-md px-3 py-2 text-sm font-medium transition-colors ${
116+ isActive
117+ ? 'text-[var(--accent-primary)]'
118+ : 'text-muted-foreground active:text-[var(--accent-hover)]'
119+ } `;
120+
121+ // Lock body scroll when menu is open
110122 useEffect ( ( ) => {
111123 if ( open ) {
112124 const scrollY = window . scrollY ;
113-
114- document . body . style . position = 'fixed' ;
115- document . body . style . top = `-${ scrollY } px` ;
116- document . body . style . width = '100%' ;
117- document . body . style . overflow = 'hidden' ;
125+ Object . assign ( document . body . style , {
126+ position : 'fixed' ,
127+ top : `-${ scrollY } px` ,
128+ width : '100%' ,
129+ overflow : 'hidden' ,
130+ } ) ;
118131
119132 return ( ) => {
120- document . body . style . position = '' ;
121- document . body . style . top = '' ;
122- document . body . style . width = '' ;
123- document . body . style . overflow = '' ;
124-
133+ Object . assign ( document . body . style , {
134+ position : '' ,
135+ top : '' ,
136+ width : '' ,
137+ overflow : '' ,
138+ } ) ;
125139 window . scrollTo ( 0 , scrollY ) ;
126140 } ;
127141 }
128142 } , [ open ] ) ;
129143
130- const linkClass = ( isActive : boolean ) =>
131- `rounded-md px-3 py-2 text-sm font-medium transition-colors ${
132- isActive
133- ? 'text-[var(--accent-primary)]'
134- : 'text-muted-foreground active:text-[var(--accent-hover)]'
135- } `;
136-
137- const slugify = ( value : string ) =>
138- value
139- . toLowerCase ( )
140- . trim ( )
141- . replace ( / [ ^ a - z 0 - 9 \s - ] / g, '' )
142- . replace ( / \s + / g, '-' ) ;
143-
144144 return (
145145 < >
146146 < button
@@ -151,7 +151,7 @@ export function AppMobileMenu({
151151 color : open ? 'var(--accent-primary)' : 'var(--muted-foreground)' ,
152152 } }
153153 aria-label = { tAria ( 'toggleMenu' ) }
154- aria-expanded = { open ? 'true' : 'false' }
154+ aria-expanded = { open }
155155 aria-controls = { open ? 'app-mobile-nav' : undefined }
156156 >
157157 { open ? < X className = "h-5 w-5" /> : < Menu className = "h-5 w-5" /> }
@@ -178,65 +178,77 @@ export function AppMobileMenu({
178178 } }
179179 >
180180 < div className = "flex flex-col gap-1" >
181- { variant === 'shop' ? (
181+ { variant === 'shop' && (
182182 < >
183- < HeaderButton href = "/" icon = { Home } onClick = { close } >
184- { t ( 'home' ) }
183+ < HeaderButton
184+ href = "/shop"
185+ icon = { ShoppingBag }
186+ isActive = { pathname === '/shop' }
187+ onLinkClick = { handleHeaderButtonLinkClick ( '/shop' ) }
188+ >
189+ { t ( 'shop' ) }
185190 </ HeaderButton >
186191 { links . map ( link => {
187192 const isActive =
188193 pathname === '/shop/products' &&
189194 ( 'category' in link
190195 ? link . category === currentCategory
191196 : currentCategory === null ) ;
197+
192198 return (
193199 < Link
194200 key = { link . href }
195201 href = { link . href }
196- onClick = { close }
202+ onClick = { e => handleLinkClick ( e , link . href ) }
197203 className = { linkClass ( isActive ) }
198204 >
199205 { 'labelKey' in link ? t ( link . labelKey ) : link . label }
200206 </ Link >
201207 ) ;
202208 } ) }
203209 </ >
204- ) : null }
210+ ) }
205211
206- { variant === 'blog' ? (
212+ { variant === 'blog' && (
207213 < >
208- < HeaderButton href = "/" icon = { Home } onClick = { close } >
209- { t ( 'home' ) }
214+ < HeaderButton
215+ href = "/blog"
216+ icon = { BookOpen }
217+ isActive = { pathname === '/blog' }
218+ onLinkClick = { handleHeaderButtonLinkClick ( '/blog' ) }
219+ >
220+ { t ( 'blog' ) }
210221 </ HeaderButton >
211222 { blogCategories . map ( category => {
212223 const slug = slugify ( category . title || '' ) ;
213224 const href = `/blog/category/${ slug } ` ;
214225 const isActive = pathname === href ;
215226 const displayTitle =
216227 category . title === 'Growth' ? 'Career' : category . title ;
228+
217229 return (
218230 < Link
219231 key = { category . _id }
220232 href = { href }
221- onClick = { close }
233+ onClick = { e => handleLinkClick ( e , href ) }
222234 className = { linkClass ( isActive ) }
223235 >
224236 { getBlogCategoryLabel ( displayTitle ) }
225237 </ Link >
226238 ) ;
227239 } ) }
228240 </ >
229- ) : null }
241+ ) }
230242
231- { variant === 'platform' ? (
243+ { variant === 'platform' && (
232244 < >
233245 { links
234246 . filter ( link => link . href !== '/shop' )
235247 . map ( link => (
236248 < Link
237249 key = { link . href }
238250 href = { link . href }
239- onClick = { close }
251+ onClick = { e => handleLinkClick ( e , link . href ) }
240252 className = { linkClass ( pathname === link . href ) }
241253 >
242254 { 'labelKey' in link ? t ( link . labelKey ) : link . label }
@@ -247,46 +259,46 @@ export function AppMobileMenu({
247259 href = "/shop"
248260 icon = { ShoppingBag }
249261 showArrow
250- onClick = { close }
262+ onLinkClick = { handleHeaderButtonLinkClick ( '/shop' ) }
251263 >
252264 { t ( 'shop' ) }
253265 </ HeaderButton >
254266 </ >
255- ) : null }
267+ ) }
256268
257- { variant === 'shop' && showAdminLink ? (
269+ { variant === 'shop' && showAdminLink && (
258270 < Link
259271 href = "/shop/admin/products/new"
260- onClick = { close }
272+ onClick = { e => handleLinkClick ( e , '/shop/admin/products/new' ) }
261273 className = { linkClass ( pathname === '/shop/admin/products/new' ) }
262274 >
263275 { tMobileMenu ( 'newProduct' ) }
264276 </ Link >
265- ) : null }
277+ ) }
266278
267279 < div className = "bg-border my-2 h-px" />
268280
269281 { userExists ? (
270282 < >
271283 < Link
272284 href = "/dashboard"
273- onClick = { close }
285+ onClick = { e => handleLinkClick ( e , '/dashboard' ) }
274286 className = { linkClass ( pathname === '/dashboard' ) }
275287 >
276288 { t ( 'dashboard' ) }
277289 </ Link >
278290
279- { showAdminLink ? (
291+ { showAdminLink && (
280292 < Link
281293 href = "/shop/admin"
282294 aria-label = { tAria ( 'shopAdmin' ) }
283295 title = { tAria ( 'shopAdmin' ) }
284- onClick = { close }
296+ onClick = { e => handleLinkClick ( e , '/shop/admin' ) }
285297 className = { linkClass ( pathname === '/shop/admin' ) }
286298 >
287299 { tMobileMenu ( 'admin' ) }
288300 </ Link >
289- ) : null }
301+ ) }
290302
291303 < div onClick = { close } >
292304 < LogoutButton />
0 commit comments