Skip to content

Commit a90d1f4

Browse files
ericyangpanclaude
andcommitted
refactor: extract breadcrumb to shared component and remove duplicate code
- Replace inline breadcrumb implementations with shared Breadcrumb component across all comparison pages - Remove ~400 lines of duplicate breadcrumb scroll handling logic - Simplify comparison page components by delegating breadcrumb management to the shared component - Use standardized translation keys from common navigation section 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1d2ac0c commit a90d1f4

File tree

4 files changed

+43
-356
lines changed

4 files changed

+43
-356
lines changed

src/app/[locale]/clis/comparison/page.client.tsx

Lines changed: 11 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import { Download, FileText, Github, Home, Linkedin, Twitter, Youtube } from 'lucide-react'
44
import { useTranslations } from 'next-intl'
5-
import { useEffect, useRef, useState } from 'react'
65
import ComparisonTable, { type ComparisonColumn } from '@/components/ComparisonTable'
6+
import { Breadcrumb } from '@/components/controls/Breadcrumb'
77
import { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/controls/PlatformIcons'
88
import Footer from '@/components/Footer'
99
import Header from '@/components/Header'
@@ -19,38 +19,9 @@ type Props = {
1919

2020
export default function CLIComparisonPageClient({ locale }: Props) {
2121
const tComparison = useTranslations('comparison')
22+
const tStacks = useTranslations('stacks')
2223
const tCommunity = useTranslations('community')
2324
const t = useTranslations()
24-
const breadcrumbRef = useRef<HTMLElement>(null)
25-
const [isBreadcrumbFixed, setIsBreadcrumbFixed] = useState(false)
26-
const [breadcrumbHeight, setBreadcrumbHeight] = useState(0)
27-
28-
useEffect(() => {
29-
const handleScroll = () => {
30-
if (!breadcrumbRef.current) return
31-
32-
const headerHeight = 60 // Height of the site header
33-
const breadcrumbTop = breadcrumbRef.current.offsetTop
34-
35-
if (window.scrollY > breadcrumbTop - headerHeight) {
36-
if (!isBreadcrumbFixed) {
37-
setBreadcrumbHeight(breadcrumbRef.current.offsetHeight)
38-
}
39-
setIsBreadcrumbFixed(true)
40-
} else {
41-
setIsBreadcrumbFixed(false)
42-
}
43-
}
44-
45-
window.addEventListener('scroll', handleScroll)
46-
window.addEventListener('resize', handleScroll)
47-
handleScroll()
48-
49-
return () => {
50-
window.removeEventListener('scroll', handleScroll)
51-
window.removeEventListener('resize', handleScroll)
52-
}
53-
}, [isBreadcrumbFixed])
5425

5526
const columns: ComparisonColumn[] = [
5627
{
@@ -307,67 +278,19 @@ export default function CLIComparisonPageClient({ locale }: Props) {
307278
<>
308279
<Header />
309280

310-
{/* Fixed Breadcrumb (when scrolled) */}
311-
{isBreadcrumbFixed && (
312-
<section className="fixed top-[60px] left-0 right-0 z-30 py-[var(--spacing-sm)] bg-[var(--color-hover)] border-b border-[var(--color-border)] shadow-sm">
313-
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
314-
<nav className="flex items-center gap-[var(--spacing-xs)] text-[0.8125rem]">
315-
<Link
316-
href={`/${locale}/ai-coding-stack`}
317-
className="text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
318-
>
319-
{tComparison('breadcrumb.aiCodingStack')}
320-
</Link>
321-
<span className="text-[var(--color-text-muted)]">/</span>
322-
<Link
323-
href={`/${locale}/clis`}
324-
className="text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
325-
>
326-
{tComparison('breadcrumb.clis')}
327-
</Link>
328-
<span className="text-[var(--color-text-muted)]">/</span>
329-
<span className="text-[var(--color-text)] font-medium">
330-
{tComparison('breadcrumb.comparison')}
331-
</span>
332-
</nav>
333-
</div>
334-
</section>
335-
)}
336-
337-
{/* Breadcrumb (original position) */}
338-
<section
339-
ref={breadcrumbRef}
340-
className="py-[var(--spacing-sm)] bg-[var(--color-hover)] border-b border-[var(--color-border)]"
341-
>
342-
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
343-
<nav
344-
className={`flex items-center gap-[var(--spacing-xs)] text-[0.8125rem] ${isBreadcrumbFixed ? 'invisible' : ''}`}
345-
>
346-
<Link
347-
href={`/${locale}/ai-coding-stack`}
348-
className="text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
349-
>
350-
{tComparison('breadcrumb.aiCodingStack')}
351-
</Link>
352-
<span className="text-[var(--color-text-muted)]">/</span>
353-
<Link
354-
href={`/${locale}/clis`}
355-
className="text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
356-
>
357-
{tComparison('breadcrumb.clis')}
358-
</Link>
359-
<span className="text-[var(--color-text-muted)]">/</span>
360-
<span className="text-[var(--color-text)] font-medium">
361-
{tComparison('breadcrumb.comparison')}
362-
</span>
363-
</nav>
364-
</div>
365-
</section>
281+
<Breadcrumb
282+
sticky
283+
items={[
284+
{ name: tStacks('aiCodingStack'), href: '/ai-coding-stack' },
285+
{ name: tStacks('clis'), href: '/clis' },
286+
{ name: tStacks('comparison'), href: '/clis/comparison' },
287+
]}
288+
/>
366289

367290
{/* Page Header */}
368291
<section className="py-[var(--spacing-lg)] border-[var(--color-border)]">
369292
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
370-
<h1 className="text-[2rem] font-semibold tracking-[-0.03em] mb-[var(--spacing-sm)]">
293+
<h1 className="text-3xl font-semibold tracking-[-0.03em] mb-[var(--spacing-sm)]">
371294
{tComparison('clis.title')}
372295
</h1>
373296
<p className="text-base text-[var(--color-text-secondary)] font-light">
@@ -383,7 +306,6 @@ export default function CLIComparisonPageClient({ locale }: Props) {
383306
items={clis as unknown as Record<string, unknown>[]}
384307
columns={columns}
385308
itemLinkPrefix={`/${locale}/clis`}
386-
stickyTopOffset={60 + (isBreadcrumbFixed ? breadcrumbHeight : 0)}
387309
/>
388310
</div>
389311
</section>

src/app/[locale]/extensions/comparison/page.client.tsx

Lines changed: 11 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import { Download, FileText, Github, Home, Linkedin, Twitter, Youtube } from 'lucide-react'
44
import { useTranslations } from 'next-intl'
5-
import { useEffect, useRef, useState } from 'react'
65
import ComparisonTable, { type ComparisonColumn } from '@/components/ComparisonTable'
6+
import { Breadcrumb } from '@/components/controls/Breadcrumb'
77
import Footer from '@/components/Footer'
88
import Header from '@/components/Header'
99
import { Link } from '@/i18n/navigation'
@@ -18,38 +18,9 @@ type Props = {
1818

1919
export default function ExtensionComparisonPageClient({ locale }: Props) {
2020
const tComparison = useTranslations('comparison')
21+
const tStacks = useTranslations('stacks')
2122
const tCommunity = useTranslations('community')
2223
const t = useTranslations()
23-
const breadcrumbRef = useRef<HTMLElement>(null)
24-
const [isBreadcrumbFixed, setIsBreadcrumbFixed] = useState(false)
25-
const [breadcrumbHeight, setBreadcrumbHeight] = useState(0)
26-
27-
useEffect(() => {
28-
const handleScroll = () => {
29-
if (!breadcrumbRef.current) return
30-
31-
const headerHeight = 60 // Height of the site header
32-
const breadcrumbTop = breadcrumbRef.current.offsetTop
33-
34-
if (window.scrollY > breadcrumbTop - headerHeight) {
35-
if (!isBreadcrumbFixed) {
36-
setBreadcrumbHeight(breadcrumbRef.current.offsetHeight)
37-
}
38-
setIsBreadcrumbFixed(true)
39-
} else {
40-
setIsBreadcrumbFixed(false)
41-
}
42-
}
43-
44-
window.addEventListener('scroll', handleScroll)
45-
window.addEventListener('resize', handleScroll)
46-
handleScroll()
47-
48-
return () => {
49-
window.removeEventListener('scroll', handleScroll)
50-
window.removeEventListener('resize', handleScroll)
51-
}
52-
}, [isBreadcrumbFixed])
5324

5425
const columns: ComparisonColumn[] = [
5526
{
@@ -293,67 +264,19 @@ export default function ExtensionComparisonPageClient({ locale }: Props) {
293264
<>
294265
<Header />
295266

296-
{/* Fixed Breadcrumb (when scrolled) */}
297-
{isBreadcrumbFixed && (
298-
<section className="fixed top-[60px] left-0 right-0 z-30 py-[var(--spacing-sm)] bg-[var(--color-hover)] border-b border-[var(--color-border)] shadow-sm">
299-
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
300-
<nav className="flex items-center gap-[var(--spacing-xs)] text-[0.8125rem]">
301-
<Link
302-
href={`/${locale}/ai-coding-stack`}
303-
className="text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
304-
>
305-
{tComparison('breadcrumb.aiCodingStack')}
306-
</Link>
307-
<span className="text-[var(--color-text-muted)]">/</span>
308-
<Link
309-
href={`/${locale}/extensions`}
310-
className="text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
311-
>
312-
{tComparison('breadcrumb.extensions')}
313-
</Link>
314-
<span className="text-[var(--color-text-muted)]">/</span>
315-
<span className="text-[var(--color-text)] font-medium">
316-
{tComparison('breadcrumb.comparison')}
317-
</span>
318-
</nav>
319-
</div>
320-
</section>
321-
)}
322-
323-
{/* Breadcrumb (original position) */}
324-
<section
325-
ref={breadcrumbRef}
326-
className="py-[var(--spacing-sm)] bg-[var(--color-hover)] border-b border-[var(--color-border)]"
327-
>
328-
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
329-
<nav
330-
className={`flex items-center gap-[var(--spacing-xs)] text-[0.8125rem] ${isBreadcrumbFixed ? 'invisible' : ''}`}
331-
>
332-
<Link
333-
href={`/${locale}/ai-coding-stack`}
334-
className="text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
335-
>
336-
{tComparison('breadcrumb.aiCodingStack')}
337-
</Link>
338-
<span className="text-[var(--color-text-muted)]">/</span>
339-
<Link
340-
href={`/${locale}/extensions`}
341-
className="text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
342-
>
343-
{tComparison('breadcrumb.extensions')}
344-
</Link>
345-
<span className="text-[var(--color-text-muted)]">/</span>
346-
<span className="text-[var(--color-text)] font-medium">
347-
{tComparison('breadcrumb.comparison')}
348-
</span>
349-
</nav>
350-
</div>
351-
</section>
267+
<Breadcrumb
268+
sticky
269+
items={[
270+
{ name: tStacks('aiCodingStack'), href: '/ai-coding-stack' },
271+
{ name: tStacks('extensions'), href: '/extensions' },
272+
{ name: tStacks('comparison'), href: '/extensions/comparison' },
273+
]}
274+
/>
352275

353276
{/* Page Header */}
354277
<section className="py-[var(--spacing-lg)] border-[var(--color-border)]">
355278
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
356-
<h1 className="text-[2rem] font-semibold tracking-[-0.03em] mb-[var(--spacing-sm)]">
279+
<h1 className="text-3xl font-semibold tracking-[-0.03em] mb-[var(--spacing-sm)]">
357280
{tComparison('extensions.title')}
358281
</h1>
359282
<p className="text-base text-[var(--color-text-secondary)] font-light">
@@ -369,7 +292,6 @@ export default function ExtensionComparisonPageClient({ locale }: Props) {
369292
items={extensions as unknown as Record<string, unknown>[]}
370293
columns={columns}
371294
itemLinkPrefix={`/${locale}/extensions`}
372-
stickyTopOffset={60 + (isBreadcrumbFixed ? breadcrumbHeight : 0)}
373295
/>
374296
</div>
375297
</section>

0 commit comments

Comments
 (0)