Skip to content

Commit bb3d56a

Browse files
ericyangpanclaude
andcommitted
feat: split MegaMenu into specialized components
Replace the generic MegaMenu component with two specialized menu components to improve maintainability and separation of concerns. - Delete generic MegaMenu.tsx - Add StackMegaMenu.tsx for AI coding stack navigation - Add RankingMegaMenu.tsx for ranking-related navigation - Update Header to support multiple mega menu types - Add megaMenuType configuration to menu items 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1659da2 commit bb3d56a

File tree

3 files changed

+82
-35
lines changed

3 files changed

+82
-35
lines changed

src/components/Header.tsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { useTranslations } from 'next-intl'
66
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
77
import { Link } from '@/i18n/navigation'
88
import SearchDialog from './controls/SearchDialog'
9-
import { MegaMenu } from './MegaMenu'
9+
import { RankingMegaMenu } from './RankingMegaMenu'
10+
import { StackMegaMenu } from './StackMegaMenu'
1011

1112
// Menu item configuration type
1213
interface MenuItem {
@@ -15,6 +16,7 @@ interface MenuItem {
1516
namespace?: 'header' | 'community'
1617
isExternal?: boolean
1718
hasMegaMenu?: boolean
19+
megaMenuType?: 'aiCodingStack' | 'ranking'
1820
}
1921

2022
// Common CSS class names - extracted to constants for DRY
@@ -27,7 +29,7 @@ function Header() {
2729
const params = useParams()
2830
const locale = params?.locale as string | undefined
2931
const [isMenuOpen, setIsMenuOpen] = useState(false)
30-
const [isMegaMenuOpen, setIsMegaMenuOpen] = useState(false)
32+
const [activeMegaMenu, setActiveMegaMenu] = useState<'aiCodingStack' | 'ranking' | null>(null)
3133
const [isSearchDialogOpen, setIsSearchDialogOpen] = useState(false)
3234
const tHeader = useTranslations('header')
3335
const tCommunity = useTranslations('community')
@@ -41,8 +43,16 @@ function Header() {
4143
translationKey: 'aiCodingStack',
4244
namespace: 'header',
4345
hasMegaMenu: true,
46+
megaMenuType: 'aiCodingStack',
4447
},
4548
{ href: '/ai-coding-landscape', translationKey: 'landscape', namespace: 'header' },
49+
{
50+
href: '#',
51+
translationKey: 'ranking',
52+
namespace: 'header',
53+
hasMegaMenu: true,
54+
megaMenuType: 'ranking',
55+
},
4656
{ href: '/curated-collections', translationKey: 'collections', namespace: 'header' },
4757
],
4858
[]
@@ -57,12 +67,12 @@ function Header() {
5767
setIsMenuOpen(false)
5868
}, [])
5969

60-
const handleMegaMenuOpen = useCallback(() => {
61-
setIsMegaMenuOpen(true)
70+
const handleMegaMenuOpen = useCallback((type: 'aiCodingStack' | 'ranking') => {
71+
setActiveMegaMenu(type)
6272
}, [])
6373

6474
const handleMegaMenuClose = useCallback(() => {
65-
setIsMegaMenuOpen(false)
75+
setActiveMegaMenu(null)
6676
}, [])
6777

6878
// Handle keyboard shortcuts for search
@@ -92,23 +102,29 @@ function Header() {
92102
// Render desktop menu item
93103
const renderDesktopMenuItem = useCallback(
94104
(item: MenuItem) => {
95-
if (item.hasMegaMenu) {
105+
if (item.hasMegaMenu && item.megaMenuType) {
106+
const isActive = activeMegaMenu === item.megaMenuType
96107
return (
97108
<li
98109
key={item.href}
99110
className="relative"
100-
onMouseEnter={handleMegaMenuOpen}
111+
onMouseEnter={() => handleMegaMenuOpen(item.megaMenuType!)}
101112
onMouseLeave={handleMegaMenuClose}
102113
>
103114
<Link
104115
href={item.href}
105116
className={DESKTOP_LINK_CLASSES}
106-
aria-expanded={isMegaMenuOpen}
117+
aria-expanded={isActive}
107118
aria-haspopup="true"
108119
>
109120
{getMenuText(item)}
110121
</Link>
111-
<MegaMenu isOpen={isMegaMenuOpen} onClose={handleMegaMenuClose} />
122+
{item.megaMenuType === 'aiCodingStack' && (
123+
<StackMegaMenu isOpen={isActive} onClose={handleMegaMenuClose} />
124+
)}
125+
{item.megaMenuType === 'ranking' && (
126+
<RankingMegaMenu isOpen={isActive} onClose={handleMegaMenuClose} />
127+
)}
112128
</li>
113129
)
114130
}
@@ -127,7 +143,7 @@ function Header() {
127143
</li>
128144
)
129145
},
130-
[isMegaMenuOpen, handleMegaMenuOpen, handleMegaMenuClose, getMenuText]
146+
[activeMegaMenu, handleMegaMenuOpen, handleMegaMenuClose, getMenuText]
131147
)
132148

133149
// Render mobile menu item

src/components/RankingMegaMenu.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use client'
2+
3+
import { useTranslations } from 'next-intl'
4+
import { memo } from 'react'
5+
import { Link } from '@/i18n/navigation'
6+
7+
interface RankingMegaMenuProps {
8+
isOpen: boolean
9+
onClose: () => void
10+
}
11+
12+
// Shared CSS classes for reusability
13+
const featuredLinkClass =
14+
'block p-[var(--spacing-sm)] border border-[var(--color-border)] hover:border-[var(--color-border-strong)] hover:bg-[var(--color-hover)] transition-all'
15+
16+
export const RankingMegaMenu = memo(function RankingMegaMenu({
17+
isOpen,
18+
onClose,
19+
}: RankingMegaMenuProps) {
20+
const tNav = useTranslations('header')
21+
22+
if (!isOpen) return null
23+
24+
return (
25+
<div className="absolute top-full left-[-2rem] pt-[var(--spacing-xs)] w-[400px] z-50">
26+
{/* Invisible bridge area to prevent menu from closing */}
27+
<div className="h-[var(--spacing-xs)]" />
28+
29+
<div className="bg-[var(--color-bg)] border border-[var(--color-border)] shadow-lg animate-fadeIn">
30+
<div className="p-[var(--spacing-md)]">
31+
{/* Open Source Ranking Link */}
32+
<Link href="/open-source-rank" onClick={onClose} className={featuredLinkClass}>
33+
<div className="font-medium mb-[var(--spacing-xs)]">{tNav('openSourceRank')}</div>
34+
<div className="text-xs text-[var(--color-text-secondary)]">
35+
{tNav('openSourceRankDesc')}
36+
</div>
37+
</Link>
38+
</div>
39+
</div>
40+
</div>
41+
)
42+
})
Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useTranslations } from 'next-intl'
44
import { memo, useMemo } from 'react'
55
import { Link } from '@/i18n/navigation'
66

7-
interface MegaMenuProps {
7+
interface StackMegaMenuProps {
88
isOpen: boolean
99
onClose: () => void
1010
}
@@ -80,7 +80,7 @@ const MenuColumn = memo(function MenuColumn({
8080
)
8181
})
8282

83-
export const MegaMenu = memo(function MegaMenu({ isOpen, onClose }: MegaMenuProps) {
83+
export const StackMegaMenu = memo(function StackMegaMenu({ isOpen, onClose }: StackMegaMenuProps) {
8484
const tStacks = useTranslations('stacks')
8585
const tNav = useTranslations('header')
8686

@@ -101,17 +101,7 @@ export const MegaMenu = memo(function MegaMenu({ isOpen, onClose }: MegaMenuProp
101101
)
102102

103103
// Memoize featured links configuration
104-
const featuredLinks = useMemo<FeaturedLink[]>(
105-
() => [
106-
{
107-
href: '/open-source-rank',
108-
titleKey: 'openSourceRank',
109-
descKey: 'openSourceRankDesc',
110-
marginBottom: 'md',
111-
},
112-
],
113-
[]
114-
)
104+
const featuredLinks = useMemo<FeaturedLink[]>(() => [], [])
115105

116106
// Memoize menu columns configuration
117107
const menuColumns = useMemo<MenuColumn[]>(
@@ -137,23 +127,22 @@ export const MegaMenu = memo(function MegaMenu({ isOpen, onClose }: MegaMenuProp
137127
))}
138128

139129
{/* Two Column Grid */}
140-
<div className="grid grid-cols-2 gap-[var(--spacing-md)] mb-[var(--spacing-sm)]">
130+
<div className="grid grid-cols-2 gap-[var(--spacing-md)]">
141131
{menuColumns.map(column => (
142132
<MenuColumn key={column.titleKey} {...column} onClose={onClose} tNav={tNav} />
143133
))}
144134
</div>
145135

146-
{/* All Vendors */}
147-
<Link
148-
href="/vendors"
149-
onClick={onClose}
150-
className="flex items-center justify-between px-[var(--spacing-sm)] py-[var(--spacing-xs)] border border-[var(--color-border)] hover:border-[var(--color-border-strong)] hover:bg-[var(--color-hover)] transition-all group"
151-
>
152-
<span className="text-sm font-medium">{tStacks('vendors')}</span>
153-
<span className="text-[var(--color-text-muted)] group-hover:text-[var(--color-text)] group-hover:translate-x-1 transition-all">
154-
155-
</span>
156-
</Link>
136+
{/* All Vendors - separated by top border */}
137+
<div className="mt-[var(--spacing-md)] pt-[var(--spacing-sm)] border-t border-[var(--color-border)]">
138+
<Link
139+
href="/vendors"
140+
onClick={onClose}
141+
className="block px-[var(--spacing-xs)] py-1 text-sm hover:bg-[var(--color-hover)] transition-colors"
142+
>
143+
{tStacks('vendors')}
144+
</Link>
145+
</div>
157146
</div>
158147
</div>
159148
</div>

0 commit comments

Comments
 (0)