Skip to content

Commit 22b1114

Browse files
committed
breadcrumb component
1 parent 75a6d0f commit 22b1114

File tree

3 files changed

+119
-0
lines changed

3 files changed

+119
-0
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { render, screen } from '@testing-library/react'
2+
import { usePathname } from 'next/navigation'
3+
import BreadCrumb from 'components/BreadCrumb'
4+
import '@testing-library/jest-dom'
5+
6+
jest.mock('next/navigation', () => ({
7+
usePathname: jest.fn(),
8+
}))
9+
10+
describe('BreadCrumb', () => {
11+
afterEach(() => {
12+
jest.clearAllMocks()
13+
})
14+
15+
test('does not render on root path "/"', () => {
16+
;(usePathname as jest.Mock).mockReturnValue('/')
17+
18+
render(<BreadCrumb />)
19+
expect(screen.queryByText('Home')).not.toBeInTheDocument()
20+
})
21+
22+
test('renders breadcrumb with single segment', () => {
23+
;(usePathname as jest.Mock).mockReturnValue('/about-us')
24+
25+
render(<BreadCrumb />)
26+
27+
expect(screen.getByText('Home')).toBeInTheDocument()
28+
expect(screen.getByText('About us')).toBeInTheDocument()
29+
})
30+
31+
test('renders breadcrumb with multiple segments', () => {
32+
;(usePathname as jest.Mock).mockReturnValue('/dashboard/users/profile')
33+
34+
render(<BreadCrumb />)
35+
36+
expect(screen.getByText('Home')).toBeInTheDocument()
37+
expect(screen.getByText('Dashboard')).toBeInTheDocument()
38+
expect(screen.getByText('Users')).toBeInTheDocument()
39+
expect(screen.getByText('Profile')).toBeInTheDocument()
40+
})
41+
42+
test('disables the last segment (non-clickable)', () => {
43+
;(usePathname as jest.Mock).mockReturnValue('/settings/account')
44+
45+
render(<BreadCrumb />)
46+
47+
const lastSegment = screen.getByText('Account')
48+
expect(lastSegment).toBeInTheDocument()
49+
expect(lastSegment).not.toHaveAttribute('href')
50+
})
51+
})

frontend/src/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
22
import { Geist, Geist_Mono } from 'next/font/google'
33
import React from 'react'
44
import { Providers } from 'wrappers/provider'
5+
import Breadcrumb from 'components/BreadCrumb'
56
import Footer from 'components/Footer'
67

78
import Header from 'components/Header'
@@ -41,6 +42,7 @@ export default function RootLayout({
4142
>
4243
<Providers>
4344
<Header />
45+
<Breadcrumb />
4446
{children}
4547
<Footer />
4648
<ScrollToTop />
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use client'
2+
3+
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
4+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
5+
import { Breadcrumbs, BreadcrumbItem } from '@heroui/react'
6+
import Link from 'next/link'
7+
import { usePathname } from 'next/navigation'
8+
9+
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1).replace(/-/g, ' ')
10+
11+
export default function BreadCrumb() {
12+
const pathname = usePathname()
13+
const segments = pathname.split('/').filter(Boolean)
14+
15+
if (pathname === '/') return null
16+
17+
return (
18+
<div className="absolute bottom-2 left-0 top-1 mt-16 w-full py-2">
19+
<div className="w-full px-8 sm:px-8 md:px-8 lg:px-8">
20+
<Breadcrumbs
21+
separator={
22+
<FontAwesomeIcon
23+
icon={faChevronRight}
24+
className="mx-1 text-xs text-gray-400 dark:text-gray-500"
25+
/>
26+
}
27+
className="text-gray-800 dark:text-gray-200"
28+
itemClasses={{
29+
base: 'transition-colors duration-200',
30+
item: 'text-sm font-medium',
31+
separator: 'flex items-center',
32+
}}
33+
>
34+
<BreadcrumbItem>
35+
<Link href="/" className="hover:text-blue-700 hover:underline dark:text-blue-400">
36+
Home
37+
</Link>
38+
</BreadcrumbItem>
39+
40+
{segments.map((segment, index) => {
41+
const href = '/' + segments.slice(0, index + 1).join('/')
42+
const label = capitalize(segment)
43+
const isLast = index === segments.length - 1
44+
45+
return (
46+
<BreadcrumbItem key={href} isDisabled={isLast}>
47+
{isLast ? (
48+
<span className="cursor-default font-semibold text-gray-600 dark:text-gray-300">
49+
{label}
50+
</span>
51+
) : (
52+
<Link
53+
href={href}
54+
className="hover:text-blue-700 hover:underline dark:text-blue-400"
55+
>
56+
{label}
57+
</Link>
58+
)}
59+
</BreadcrumbItem>
60+
)
61+
})}
62+
</Breadcrumbs>
63+
</div>
64+
</div>
65+
)
66+
}

0 commit comments

Comments
 (0)