Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions frontend/__tests__/unit/components/BreadCrumb.test.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some e2e tests for breadcrumbs too?

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { render, screen } from '@testing-library/react'
import { usePathname } from 'next/navigation'
import BreadCrumb from 'components/BreadCrumb'
import '@testing-library/jest-dom'

jest.mock('next/navigation', () => ({
usePathname: jest.fn(),
}))

describe('BreadCrumb', () => {
afterEach(() => {
jest.clearAllMocks()
})

test('does not render on root path "/"', () => {
;(usePathname as jest.Mock).mockReturnValue('/')

render(<BreadCrumb />)
expect(screen.queryByText('Home')).not.toBeInTheDocument()
})

test('renders breadcrumb with multiple segments', () => {
;(usePathname as jest.Mock).mockReturnValue('/dashboard/users/profile')

render(<BreadCrumb />)

expect(screen.getByText('Home')).toBeInTheDocument()
expect(screen.getByText('Dashboard')).toBeInTheDocument()
expect(screen.getByText('Users')).toBeInTheDocument()
expect(screen.getByText('Profile')).toBeInTheDocument()
})

test('disables the last segment (non-clickable)', () => {
;(usePathname as jest.Mock).mockReturnValue('/settings/account')

render(<BreadCrumb />)

const lastSegment = screen.getByText('Account')
expect(lastSegment).toBeInTheDocument()
expect(lastSegment).not.toHaveAttribute('href')
})
})
2 changes: 2 additions & 0 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import React from 'react'
import { Providers } from 'wrappers/provider'
import BreadCrumb from 'components/BreadCrumb'
import Footer from 'components/Footer'

import Header from 'components/Header'
Expand Down Expand Up @@ -41,6 +42,7 @@ export default function RootLayout({
>
<Providers>
<Header />
<BreadCrumb />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<BreadCrumb />
<BreadCrumbs />

{children}
<Footer />
<ScrollToTop />
Expand Down
66 changes: 66 additions & 0 deletions frontend/src/components/BreadCrumb.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be plural BreadCrumbs?

Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use client'

import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Breadcrumbs, BreadcrumbItem } from '@heroui/react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'

const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1).replace(/-/g, ' ')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may have capitalize function in utils. Please use or extend that one.


export default function BreadCrumb() {
const pathname = usePathname()
const segments = pathname.split('/').filter(Boolean)

if (pathname === '/') return null
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could extract this / to a separate constant.


return (
<div className="absolute bottom-2 left-0 top-1 mt-16 w-full py-2">
<div className="w-full px-8 sm:px-8 md:px-8 lg:px-8">
<Breadcrumbs
separator={
<FontAwesomeIcon
icon={faChevronRight}
className="mx-1 text-xs text-gray-400 dark:text-gray-500"
/>
}
className="text-gray-800 dark:text-gray-200"
itemClasses={{
base: 'transition-colors duration-200',
item: 'text-sm font-medium',
separator: 'flex items-center',
}}
>
<BreadcrumbItem>
<Link href="/" className="hover:text-blue-700 hover:underline dark:text-blue-400">
Home
</Link>
</BreadcrumbItem>

{segments.map((segment, index) => {
const href = '/' + segments.slice(0, index + 1).join('/')
const label = capitalize(segment)
const isLast = index === segments.length - 1

return (
<BreadcrumbItem key={href} isDisabled={isLast}>
{isLast ? (
<span className="cursor-default font-semibold text-gray-600 dark:text-gray-300">
{label}
</span>
) : (
<Link
href={href}
className="hover:text-blue-700 hover:underline dark:text-blue-400"
>
{label}
</Link>
)}
</BreadcrumbItem>
)
})}
</Breadcrumbs>
</div>
</div>
)
}