diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt
index 31cf216e82..980ac54aa5 100644
--- a/cspell/custom-dict.txt
+++ b/cspell/custom-dict.txt
@@ -6,6 +6,7 @@ BOTTOMPADDING
CCSP
CISSP
Cañón
+Cyclonedx
DRF
GBP
GFKs
@@ -13,6 +14,7 @@ GSOC
GTM
Héllo
Kateryna
+Kimminich
NETTACKER
NOASSERTION
NOSONAR
@@ -43,6 +45,7 @@ arithmatex
arkid15r
askowasp
bangbang
+bjornkimminich
bsky
certbot
collectstatic
@@ -52,6 +55,7 @@ csrfguard
csrfprotector
csrftoken
cva
+cyclonedx
demojize
dismissable
dsn
diff --git a/frontend/__tests__/unit/components/BreadCrumbs.test.tsx b/frontend/__tests__/unit/components/BreadCrumbs.test.tsx
index 17511e18ad..13fdddbd8e 100644
--- a/frontend/__tests__/unit/components/BreadCrumbs.test.tsx
+++ b/frontend/__tests__/unit/components/BreadCrumbs.test.tsx
@@ -1,68 +1,98 @@
import { render, screen } from '@testing-library/react'
-import { usePathname } from 'next/navigation'
-import BreadCrumbs from 'components/BreadCrumbs'
+import BreadCrumbRenderer from 'components/BreadCrumbs'
import '@testing-library/jest-dom'
-jest.mock('next/navigation', () => ({
- usePathname: jest.fn(),
-}))
+describe('BreadCrumbRenderer', () => {
+ const mockItems = [
+ { title: 'Home', path: '/' },
+ { title: 'Projects', path: '/projects' },
+ { title: 'OWASP ZAP', path: '/projects/zap' },
+ ]
-describe('BreadCrumb', () => {
- afterEach(() => {
- jest.clearAllMocks()
+ test('renders all breadcrumb items', () => {
+ render()
+
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.getByText('Projects')).toBeInTheDocument()
+ expect(screen.getByText('OWASP ZAP')).toBeInTheDocument()
})
- test('does not render on root path "/"', () => {
- ;(usePathname as jest.Mock).mockReturnValue('/')
+ test('renders navigation element with correct aria-label', () => {
+ render()
- render()
- expect(screen.queryByText('Home')).not.toBeInTheDocument()
+ const nav = screen.getByRole('navigation')
+ expect(nav).toHaveAttribute('aria-label', 'breadcrumb')
})
- test('renders breadcrumb with multiple segments', () => {
- ;(usePathname as jest.Mock).mockReturnValue('/dashboard/users/profile')
+ test('renders clickable links for non-last items', () => {
+ render()
- render()
+ const homeLink = screen.getByText('Home').closest('a')
+ const projectsLink = screen.getByText('Projects').closest('a')
- expect(screen.getByText('Home')).toBeInTheDocument()
- expect(screen.getByText('Dashboard')).toBeInTheDocument()
- expect(screen.getByText('Users')).toBeInTheDocument()
- expect(screen.getByText('Profile')).toBeInTheDocument()
+ expect(homeLink).toHaveAttribute('href', '/')
+ expect(projectsLink).toHaveAttribute('href', '/projects')
})
- test('disables the last segment (non-clickable)', () => {
- ;(usePathname as jest.Mock).mockReturnValue('/settings/account')
+ test('disables the last item (non-clickable)', () => {
+ render()
+
+ const lastItem = screen.getByText('OWASP ZAP')
+ expect(lastItem).not.toHaveAttribute('href')
+ expect(lastItem.tagName).toBe('SPAN')
+ })
- render()
+ test('applies hover styles to clickable links', () => {
+ render()
- const lastSegment = screen.getByText('Account')
- expect(lastSegment).toBeInTheDocument()
- expect(lastSegment).not.toHaveAttribute('href')
+ const homeLink = screen.getByText('Home').closest('a')
+ expect(homeLink).toHaveClass('hover:text-blue-700', 'hover:underline')
})
- test('links have correct href attributes', () => {
- ;(usePathname as jest.Mock).mockReturnValue('/dashboard/users/profile')
+ test('applies disabled styling to last breadcrumb', () => {
+ render()
- render()
+ const lastItem = screen.getByText('OWASP ZAP')
+ expect(lastItem).toHaveClass('cursor-default', 'font-semibold')
+ })
- const homeLink = screen.getByText('Home').closest('a')
- const dashboardLink = screen.getByText('Dashboard').closest('a')
- const usersLink = screen.getByText('Users').closest('a')
+ test('renders chevron separators between items', () => {
+ const { container } = render()
- expect(homeLink).toHaveAttribute('href', '/')
- expect(dashboardLink).toHaveAttribute('href', '/dashboard')
- expect(usersLink).toHaveAttribute('href', '/dashboard/users')
+ const separators = container.querySelectorAll('[data-slot="separator"]')
+ expect(separators).toHaveLength(2)
})
- test('links have hover styles', () => {
- ;(usePathname as jest.Mock).mockReturnValue('/dashboard/users')
+ test('handles single item (home only)', () => {
+ const singleItem = [{ title: 'Home', path: '/' }]
+ render()
- render()
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ const separators = screen.queryByRole('separator')
+ expect(separators).not.toBeInTheDocument()
+ })
+
+ test('handles empty items array', () => {
+ const { container } = render()
+
+ const breadcrumbList = container.querySelector('[data-slot="list"]')
+ expect(breadcrumbList?.children).toHaveLength(0)
+ })
+
+ test('applies correct wrapper styling', () => {
+ const { container } = render()
+
+ const wrapper = container.querySelector('.mt-16')
+ expect(wrapper).toHaveClass('w-full', 'pt-4')
+ })
+
+ test('links have correct href attributes', () => {
+ render()
const homeLink = screen.getByText('Home').closest('a')
- const dashboardLink = screen.getByText('Dashboard').closest('a')
+ const projectsLink = screen.getByText('Projects').closest('a')
- expect(homeLink).toHaveClass('hover:text-blue-700', 'hover:underline')
- expect(dashboardLink).toHaveClass('hover:text-blue-700', 'hover:underline')
+ expect(homeLink).toHaveAttribute('href', '/')
+ expect(projectsLink).toHaveAttribute('href', '/projects')
})
})
diff --git a/frontend/__tests__/unit/components/BreadCrumbsWrapper.test.tsx b/frontend/__tests__/unit/components/BreadCrumbsWrapper.test.tsx
new file mode 100644
index 0000000000..5fe4b96d12
--- /dev/null
+++ b/frontend/__tests__/unit/components/BreadCrumbsWrapper.test.tsx
@@ -0,0 +1,148 @@
+import { render, screen } from '@testing-library/react'
+import { usePathname } from 'next/navigation'
+import BreadCrumbsWrapper from 'components/BreadCrumbsWrapper'
+import '@testing-library/jest-dom'
+
+jest.mock('next/navigation', () => ({
+ usePathname: jest.fn(),
+}))
+
+describe('BreadCrumbsWrapper', () => {
+ afterEach(() => {
+ jest.clearAllMocks()
+ })
+
+ describe('Route Detection - Should Hide', () => {
+ test('returns null on home page', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/')
+
+ const { container } = render()
+ expect(container.firstChild).toBeNull()
+ })
+
+ test('returns null for project detail pages', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/zap')
+
+ const { container } = render()
+ expect(container.firstChild).toBeNull()
+ })
+
+ test('returns null for member detail pages', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/members/bjornkimminich')
+
+ const { container } = render()
+ expect(container.firstChild).toBeNull()
+ })
+
+ test('returns null for chapter detail pages', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/chapters/bangalore')
+
+ const { container } = render()
+ expect(container.firstChild).toBeNull()
+ })
+
+ test('returns null for committee detail pages', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/committees/outreach')
+
+ const { container } = render()
+ expect(container.firstChild).toBeNull()
+ })
+
+ test('returns null for organization detail pages', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/organizations/cyclonedx')
+
+ const { container } = render()
+ expect(container.firstChild).toBeNull()
+ })
+
+ test('returns null for nested repository pages', () => {
+ ;(usePathname as jest.Mock).mockReturnValue(
+ '/organizations/cyclonedx/repositories/cyclonedx-python'
+ )
+
+ const { container } = render()
+ expect(container.firstChild).toBeNull()
+ })
+ })
+
+ describe('Route Detection - Should Render', () => {
+ test('renders for projects list page', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects')
+
+ render()
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.getByText('Projects')).toBeInTheDocument()
+ })
+
+ test('renders for members list page', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/members')
+
+ render()
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.getByText('Members')).toBeInTheDocument()
+ })
+
+ test('renders for chapters list page', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/chapters')
+
+ render()
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.getByText('Chapters')).toBeInTheDocument()
+ })
+ })
+
+ describe('Auto-Generation Logic', () => {
+ test('capitalizes single-word segments', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/about')
+
+ render()
+ expect(screen.getByText('About')).toBeInTheDocument()
+ })
+
+ test('replaces dashes with spaces and capitalizes', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/some-page')
+
+ render()
+ expect(screen.getByText('Some page')).toBeInTheDocument()
+ })
+
+ test('handles multi-segment paths correctly', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/community/events')
+
+ render()
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.getByText('Community')).toBeInTheDocument()
+ expect(screen.getByText('Events')).toBeInTheDocument()
+ })
+
+ test('builds progressive paths for links', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/community/events/conferences')
+
+ render()
+
+ const homeLink = screen.getByText('Home').closest('a')
+ const communityLink = screen.getByText('Community').closest('a')
+ const eventsLink = screen.getByText('Events').closest('a')
+
+ expect(homeLink).toHaveAttribute('href', '/')
+ expect(communityLink).toHaveAttribute('href', '/community')
+ expect(eventsLink).toHaveAttribute('href', '/community/events')
+ })
+ })
+
+ describe('Edge Cases', () => {
+ test('handles trailing slashes', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/')
+
+ render()
+ expect(screen.getByText('Projects')).toBeInTheDocument()
+ })
+
+ test('handles paths with special characters in segment names', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/test-page-name')
+
+ render()
+ expect(screen.getByText('Test page name')).toBeInTheDocument()
+ })
+ })
+})
diff --git a/frontend/__tests__/unit/components/PageLayout.test.tsx b/frontend/__tests__/unit/components/PageLayout.test.tsx
new file mode 100644
index 0000000000..7251942180
--- /dev/null
+++ b/frontend/__tests__/unit/components/PageLayout.test.tsx
@@ -0,0 +1,137 @@
+import { render, screen } from '@testing-library/react'
+import { usePathname } from 'next/navigation'
+import PageLayout from 'components/PageLayout'
+import '@testing-library/jest-dom'
+
+jest.mock('next/navigation', () => ({
+ usePathname: jest.fn(),
+}))
+
+describe('PageLayout', () => {
+ afterEach(() => {
+ jest.clearAllMocks()
+ })
+
+ test('renders children components', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/zap')
+
+ render(
+
+ Child Content
+
+ )
+
+ expect(screen.getByText('Child Content')).toBeInTheDocument()
+ })
+
+ test('renders breadcrumbs above children', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/zap')
+
+ const { container } = render(
+
+ Child Content
+
+ )
+
+ const breadcrumbNav = container.querySelector('nav')
+ const childContent = screen.getByText('Child Content')
+
+ expect(breadcrumbNav?.compareDocumentPosition(childContent)).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ )
+ })
+
+ describe('BreadcrumbData Prop Handling', () => {
+ test('passes projectName to breadcrumbs', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/zap')
+
+ render(
+
+ Content
+
+ )
+
+ expect(screen.getByText('OWASP ZAP')).toBeInTheDocument()
+ })
+
+ test('passes memberName to breadcrumbs', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/members/bjornkimminich')
+
+ render(
+
+ Content
+
+ )
+
+ expect(screen.getByText('Björn Kimminich')).toBeInTheDocument()
+ })
+
+ test('passes chapterName to breadcrumbs', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/chapters/bangalore')
+
+ render(
+
+ Content
+
+ )
+
+ expect(screen.getByText('Bangalore Chapter')).toBeInTheDocument()
+ })
+
+ test('passes committeeName to breadcrumbs', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/committees/outreach')
+
+ render(
+
+ Content
+
+ )
+
+ expect(screen.getByText('Outreach Committee')).toBeInTheDocument()
+ })
+
+ test('passes orgName and repoName for repository pages', () => {
+ ;(usePathname as jest.Mock).mockReturnValue(
+ '/organizations/cyclonedx/repositories/cyclonedx-python'
+ )
+
+ render(
+
+ Content
+
+ )
+
+ expect(screen.getByText('CycloneDX BOM Standard')).toBeInTheDocument()
+ expect(screen.getByText('Cyclonedx Python')).toBeInTheDocument()
+ })
+
+ test('handles undefined breadcrumbData', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/juice-shop')
+
+ render(
+
+ Content
+
+ )
+
+ expect(screen.getByText('Juice shop')).toBeInTheDocument()
+ })
+
+ test('handles partial breadcrumbData (wrong field for route type)', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/zap')
+
+ render(
+
+ Content
+
+ )
+
+ expect(screen.getByText('John Doe')).toBeInTheDocument()
+ })
+ })
+})
diff --git a/frontend/__tests__/unit/hooks/useBreadcrumbs.test.ts b/frontend/__tests__/unit/hooks/useBreadcrumbs.test.ts
new file mode 100644
index 0000000000..ea04830dc5
--- /dev/null
+++ b/frontend/__tests__/unit/hooks/useBreadcrumbs.test.ts
@@ -0,0 +1,242 @@
+import { renderHook } from '@testing-library/react'
+import { useBreadcrumbs } from 'hooks/useBreadcrumbs'
+import { usePathname } from 'next/navigation'
+
+jest.mock('next/navigation', () => ({
+ usePathname: jest.fn(),
+}))
+
+describe('useBreadcrumbs', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ describe('Single-slug routes', () => {
+ test('generates breadcrumbs for project page with projectName', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/zap')
+
+ const { result } = renderHook(() => useBreadcrumbs({ projectName: 'OWASP ZAP' }))
+
+ expect(result.current).toEqual([
+ { title: 'Home', path: '/' },
+ { title: 'Projects', path: '/projects' },
+ { title: 'OWASP ZAP', path: '/projects/zap' },
+ ])
+ })
+
+ test('fallback to formatted slug if projectName not provided', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/juice-shop')
+
+ const { result } = renderHook(() => useBreadcrumbs({}))
+
+ expect(result.current).toEqual([
+ { title: 'Home', path: '/' },
+ { title: 'Projects', path: '/projects' },
+ { title: 'Juice shop', path: '/projects/juice-shop' },
+ ])
+ })
+
+ test('generates breadcrumbs for member page with memberName', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/members/bjornkimminich')
+
+ const { result } = renderHook(() => useBreadcrumbs({ memberName: 'Björn Kimminich' }))
+
+ expect(result.current).toEqual([
+ { title: 'Home', path: '/' },
+ { title: 'Members', path: '/members' },
+ { title: 'Björn Kimminich', path: '/members/bjornkimminich' },
+ ])
+ })
+
+ test('generates breadcrumbs for chapter page with chapterName', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/chapters/bangalore')
+
+ const { result } = renderHook(() => useBreadcrumbs({ chapterName: 'Bangalore Chapter' }))
+
+ expect(result.current).toEqual([
+ { title: 'Home', path: '/' },
+ { title: 'Chapters', path: '/chapters' },
+ { title: 'Bangalore Chapter', path: '/chapters/bangalore' },
+ ])
+ })
+
+ test('generates breadcrumbs for committee page with committeeName', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/committees/outreach')
+
+ const { result } = renderHook(() => useBreadcrumbs({ committeeName: 'Outreach Committee' }))
+
+ expect(result.current).toEqual([
+ { title: 'Home', path: '/' },
+ { title: 'Committees', path: '/committees' },
+ { title: 'Outreach Committee', path: '/committees/outreach' },
+ ])
+ })
+
+ test('generates breadcrumbs for organization page with orgName', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/organizations/cyclonedx')
+
+ const { result } = renderHook(() => useBreadcrumbs({ orgName: 'CycloneDX BOM Standard' }))
+
+ expect(result.current).toEqual([
+ { title: 'Home', path: '/' },
+ { title: 'Organizations', path: '/organizations' },
+ { title: 'CycloneDX BOM Standard', path: '/organizations/cyclonedx' },
+ ])
+ })
+ })
+
+ describe('Multi-slug routes (nested repositories)', () => {
+ test('detects nested repository route pattern', () => {
+ ;(usePathname as jest.Mock).mockReturnValue(
+ '/organizations/cyclonedx/repositories/cyclonedx-python'
+ )
+
+ const { result } = renderHook(() =>
+ useBreadcrumbs({
+ orgName: 'CycloneDX BOM Standard',
+ repoName: 'Cyclonedx Python',
+ })
+ )
+
+ expect(result.current).toHaveLength(4)
+ })
+
+ test('generates correct breadcrumbs for repository page', () => {
+ ;(usePathname as jest.Mock).mockReturnValue(
+ '/organizations/cyclonedx/repositories/cyclonedx-python'
+ )
+
+ const { result } = renderHook(() =>
+ useBreadcrumbs({
+ orgName: 'CycloneDX BOM Standard',
+ repoName: 'Cyclonedx Python',
+ })
+ )
+
+ expect(result.current).toEqual([
+ { title: 'Home', path: '/' },
+ { title: 'Organizations', path: '/organizations' },
+ {
+ title: 'CycloneDX BOM Standard',
+ path: '/organizations/cyclonedx',
+ },
+ {
+ title: 'Cyclonedx Python',
+ path: '/organizations/cyclonedx/repositories/cyclonedx-python',
+ },
+ ])
+ })
+
+ test('uses orgName from breadcrumbData', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/organizations/owasp-amass/repositories/amass')
+
+ const { result } = renderHook(() =>
+ useBreadcrumbs({
+ orgName: 'OWASP Amass Project',
+ repoName: 'Amass',
+ })
+ )
+
+ const orgBreadcrumb = result.current.find(
+ (item) => item.path === '/organizations/owasp-amass'
+ )
+ expect(orgBreadcrumb?.title).toBe('OWASP Amass Project')
+ })
+
+ test('formats repo name by splitting dashes and capitalizing', () => {
+ ;(usePathname as jest.Mock).mockReturnValue(
+ '/organizations/cyclonedx/repositories/cyclonedx-maven-plugin'
+ )
+
+ const { result } = renderHook(() => useBreadcrumbs({}))
+
+ const repoBreadcrumb = result.current[result.current.length - 1]
+ expect(repoBreadcrumb.title).toBe('Cyclonedx maven plugin')
+ })
+
+ test('fallback to formatted slugs if data not provided', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/organizations/some-org/repositories/some-repo')
+
+ const { result } = renderHook(() => useBreadcrumbs({}))
+
+ expect(result.current).toEqual([
+ { title: 'Home', path: '/' },
+ { title: 'Organizations', path: '/organizations' },
+ { title: 'Some org', path: '/organizations/some-org' },
+ {
+ title: 'Some repo',
+ path: '/organizations/some-org/repositories/some-repo',
+ },
+ ])
+ })
+ })
+
+ describe('Path building', () => {
+ test('builds progressive paths correctly', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/zap')
+
+ const { result } = renderHook(() => useBreadcrumbs({ projectName: 'OWASP ZAP' }))
+
+ expect(result.current[0].path).toBe('/')
+ expect(result.current[1].path).toBe('/projects')
+ expect(result.current[2].path).toBe('/projects/zap')
+ })
+
+ test('builds correct nested paths for repos', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/organizations/org/repositories/repo')
+
+ const { result } = renderHook(() =>
+ useBreadcrumbs({ orgName: 'Org Name', repoName: 'Repo Name' })
+ )
+
+ expect(result.current[0].path).toBe('/')
+ expect(result.current[1].path).toBe('/organizations')
+ expect(result.current[2].path).toBe('/organizations/org')
+ expect(result.current[3].path).toBe('/organizations/org/repositories/repo')
+ })
+ })
+
+ describe('Edge cases', () => {
+ test('handles undefined breadcrumbData', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/test')
+
+ const { result } = renderHook(() => useBreadcrumbs(undefined))
+
+ expect(result.current).toEqual([
+ { title: 'Home', path: '/' },
+ { title: 'Projects', path: '/projects' },
+ { title: 'Test', path: '/projects/test' },
+ ])
+ })
+
+ test('handles partial breadcrumbData (uses first matching field)', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/zap')
+
+ const { result } = renderHook(() => useBreadcrumbs({ memberName: 'John Doe' }))
+
+ expect(result.current[2].title).toBe('John Doe')
+ })
+
+ test('handles paths with trailing slashes', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/zap/')
+
+ const { result } = renderHook(() => useBreadcrumbs({ projectName: 'OWASP ZAP' }))
+
+ expect(result.current).toHaveLength(3)
+ expect(result.current[2].title).toBe('OWASP ZAP')
+ })
+
+ test('each breadcrumb item has title and path properties', () => {
+ ;(usePathname as jest.Mock).mockReturnValue('/projects/zap')
+
+ const { result } = renderHook(() => useBreadcrumbs({ projectName: 'OWASP ZAP' }))
+
+ result.current.forEach((item) => {
+ expect(item).toHaveProperty('title')
+ expect(item).toHaveProperty('path')
+ expect(typeof item.title).toBe('string')
+ expect(typeof item.path).toBe('string')
+ })
+ })
+ })
+})
diff --git a/frontend/__tests__/unit/pages/ChapterDetails.test.tsx b/frontend/__tests__/unit/pages/ChapterDetails.test.tsx
index 978f55026a..96da372687 100644
--- a/frontend/__tests__/unit/pages/ChapterDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/ChapterDetails.test.tsx
@@ -28,6 +28,7 @@ jest.mock('next/navigation', () => ({
...jest.requireActual('next/navigation'),
useRouter: jest.fn(() => mockRouter),
useParams: () => ({ chapterKey: 'test-chapter' }),
+ usePathname: jest.fn(() => '/chapters/test-chapter'),
}))
describe('chapterDetailsPage Component', () => {
diff --git a/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx b/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx
index e27fff0286..a9c776505f 100644
--- a/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx
@@ -22,6 +22,7 @@ jest.mock('next/navigation', () => ({
...jest.requireActual('next/navigation'),
useRouter: jest.fn(() => mockRouter),
useParams: () => ({ committeeKey: 'test-committee' }),
+ usePathname: jest.fn(() => '/committees/test-committee'),
}))
describe('CommitteeDetailsPage Component', () => {
@@ -52,7 +53,7 @@ describe('CommitteeDetailsPage Component', () => {
test('renders committee data correctly', async () => {
render()
await waitFor(() => {
- expect(screen.getByText('Test Committee')).toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: 'Test Committee' })).toBeInTheDocument()
})
expect(screen.getByText('This is a test committee summary.')).toBeInTheDocument()
expect(screen.getByText('Leader 1')).toBeInTheDocument()
diff --git a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
index 1b1281fe65..1a4e27d98b 100644
--- a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
@@ -28,6 +28,7 @@ jest.mock('next/navigation', () => ({
...jest.requireActual('next/navigation'),
useRouter: jest.fn(() => mockRouter),
useParams: () => ({ repositoryKey: 'test-org' }),
+ usePathname: jest.fn(() => '/organizations/test-org'),
}))
const mockError = {
@@ -70,7 +71,7 @@ describe('OrganizationDetailsPage', () => {
render()
await waitFor(() => {
- expect(screen.getByText('Test Organization')).toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: 'Test Organization' })).toBeInTheDocument()
})
expect(screen.getByText('@test-org')).toBeInTheDocument()
diff --git a/frontend/src/app/chapters/[chapterKey]/page.tsx b/frontend/src/app/chapters/[chapterKey]/page.tsx
index b425c42838..706c56d3a3 100644
--- a/frontend/src/app/chapters/[chapterKey]/page.tsx
+++ b/frontend/src/app/chapters/[chapterKey]/page.tsx
@@ -10,6 +10,7 @@ import type { Contributor } from 'types/contributor'
import { formatDate } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
+import PageLayout from 'components/PageLayout'
export default function ChapterDetailsPage() {
const { chapterKey } = useParams<{ chapterKey: string }>()
@@ -60,17 +61,19 @@ export default function ChapterDetailsPage() {
},
]
return (
-
+
+
+
)
}
diff --git a/frontend/src/app/committees/[committeeKey]/page.tsx b/frontend/src/app/committees/[committeeKey]/page.tsx
index dea63a3afe..74d7ccd84a 100644
--- a/frontend/src/app/committees/[committeeKey]/page.tsx
+++ b/frontend/src/app/committees/[committeeKey]/page.tsx
@@ -17,6 +17,7 @@ import type { Contributor } from 'types/contributor'
import { formatDate } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
+import PageLayout from 'components/PageLayout'
export default function CommitteeDetailsPage() {
const { committeeKey } = useParams<{ committeeKey: string }>()
@@ -80,14 +81,16 @@ export default function CommitteeDetailsPage() {
]
return (
-
+
+
+
)
}
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 62b643fa3f..7b46dd8dae 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -6,7 +6,7 @@ import { Providers } from 'wrappers/provider'
import { GTM_ID } from 'utils/env.client'
import { IS_GITHUB_AUTH_ENABLED } from 'utils/env.server'
import AutoScrollToTop from 'components/AutoScrollToTop'
-import BreadCrumbs from 'components/BreadCrumbs'
+import BreadCrumbsWrapper from 'components/BreadCrumbsWrapper'
import Footer from 'components/Footer'
import Header from 'components/Header'
import ScrollToTop from 'components/ScrollToTop'
@@ -74,7 +74,7 @@ export default function RootLayout({
-
+
{children}
diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx
index e4a56e148d..fc380c8ce6 100644
--- a/frontend/src/app/members/[memberKey]/page.tsx
+++ b/frontend/src/app/members/[memberKey]/page.tsx
@@ -26,6 +26,7 @@ import { drawContributions, fetchHeatmapData, HeatmapData } from 'utils/helpers/
import Badges from 'components/Badges'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
+import PageLayout from 'components/PageLayout'
const UserDetailsPage: React.FC = () => {
const { memberKey } = useParams<{ memberKey: string }>()
@@ -231,19 +232,21 @@ const UserDetailsPage: React.FC = () => {
)
return (
-
}
- />
+
+ }
+ />
+
)
}
diff --git a/frontend/src/app/organizations/[organizationKey]/page.tsx b/frontend/src/app/organizations/[organizationKey]/page.tsx
index 1541f91d95..71b6f26a70 100644
--- a/frontend/src/app/organizations/[organizationKey]/page.tsx
+++ b/frontend/src/app/organizations/[organizationKey]/page.tsx
@@ -15,6 +15,7 @@ import { GetOrganizationDataDocument } from 'types/__generated__/organizationQue
import { formatDate } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
+import PageLayout from 'components/PageLayout'
const OrganizationDetailsPage = () => {
const { organizationKey } = useParams<{ organizationKey: string }>()
const [organization, setOrganization] = useState(null)
@@ -113,19 +114,21 @@ const OrganizationDetailsPage = () => {
]
return (
-
+
+
+
)
}
diff --git a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx
index 9e4d82c3b1..5f3252d2ec 100644
--- a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx
+++ b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx
@@ -7,6 +7,7 @@ import {
faStar,
faUsers,
} from '@fortawesome/free-solid-svg-icons'
+import upperFirst from 'lodash/upperFirst'
import Link from 'next/link'
import { useParams } from 'next/navigation'
import { useEffect, useState } from 'react'
@@ -16,6 +17,7 @@ import type { Contributor } from 'types/contributor'
import { formatDate } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
+import PageLayout from 'components/PageLayout'
const RepositoryDetailsPage = () => {
const { repositoryKey, organizationKey } = useParams<{
@@ -108,23 +110,41 @@ const RepositoryDetailsPage = () => {
},
]
return (
-
+
upperFirst(word))
+ .join(' ')
+ : repositoryKey
+ .split('-')
+ .map((word) => upperFirst(word))
+ .join(' '),
+ }}
+ >
+
+
)
}
export default RepositoryDetailsPage
diff --git a/frontend/src/app/projects/[projectKey]/page.tsx b/frontend/src/app/projects/[projectKey]/page.tsx
index 47ae45a856..4503f099c9 100644
--- a/frontend/src/app/projects/[projectKey]/page.tsx
+++ b/frontend/src/app/projects/[projectKey]/page.tsx
@@ -18,6 +18,7 @@ import type { Project } from 'types/project'
import { formatDate } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
+import PageLayout from 'components/PageLayout'
const ProjectDetailsPage = () => {
const { projectKey } = useParams<{ projectKey: string }>()
const [isLoading, setIsLoading] = useState
(true)
@@ -89,25 +90,27 @@ const ProjectDetailsPage = () => {
]
return (
-
+
+
+
)
}
diff --git a/frontend/src/components/BreadCrumbs.tsx b/frontend/src/components/BreadCrumbs.tsx
index 83a319a8a8..bdaefd60e7 100644
--- a/frontend/src/components/BreadCrumbs.tsx
+++ b/frontend/src/components/BreadCrumbs.tsx
@@ -1,19 +1,14 @@
-'use client'
-
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { Breadcrumbs, BreadcrumbItem } from '@heroui/react'
-import upperFirst from 'lodash/upperFirst'
+import { Breadcrumbs, BreadcrumbItem as HeroUIBreadcrumbItem } from '@heroui/react'
+import type { BreadCrumbItem } from 'hooks/useBreadcrumbs'
import Link from 'next/link'
-import { usePathname } from 'next/navigation'
-
-export default function BreadCrumbs() {
- const homeRoute = '/'
- const pathname = usePathname()
- const segments = pathname.split(homeRoute).filter(Boolean)
- if (pathname === homeRoute) return null
+interface BreadCrumbRendererProps {
+ items: BreadCrumbItem[]
+}
+export default function BreadCrumbRenderer({ items }: BreadCrumbRendererProps) {
return (
@@ -32,35 +27,27 @@ export default function BreadCrumbs() {
separator: 'flex items-center',
}}
>
-
-
- Home
-
-
-
- {segments.map((segment, index) => {
- const href = homeRoute + segments.slice(0, index + 1).join(homeRoute)
- const label = upperFirst(segment).replaceAll('-', ' ')
- const isLast = index === segments.length - 1
+ {items.map((item, index) => {
+ const isLast = index === items.length - 1
return (
-
+
{isLast ? (
-
- {label}
+
+ {item.title}
) : (
- {label}
+ {item.title}
)}
-
+
)
})}
diff --git a/frontend/src/components/BreadCrumbsWrapper.tsx b/frontend/src/components/BreadCrumbsWrapper.tsx
new file mode 100644
index 0000000000..8e07189414
--- /dev/null
+++ b/frontend/src/components/BreadCrumbsWrapper.tsx
@@ -0,0 +1,78 @@
+'use client'
+
+import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { Breadcrumbs, BreadcrumbItem as HeroUIBreadcrumbItem } from '@heroui/react'
+import upperFirst from 'lodash/upperFirst'
+import Link from 'next/link'
+import { usePathname } from 'next/navigation'
+
+const ROUTES_WITH_PAGE_LAYOUT = [
+ /^\/members\/[^/]+$/,
+ /^\/projects\/[^/]+$/,
+ /^\/chapters\/[^/]+$/,
+ /^\/committees\/[^/]+$/,
+ /^\/organizations\/[^/]+$/,
+ /^\/organizations\/[^/]+\/repositories\/[^/]+$/,
+]
+
+export default function BreadCrumbsWrapper() {
+ const pathname = usePathname()
+
+ if (pathname === '/') return null
+
+ const usesPageLayout = ROUTES_WITH_PAGE_LAYOUT.some((pattern) => pattern.test(pathname))
+ if (usesPageLayout) return null
+
+ const segments = pathname.split('/').filter(Boolean)
+ const items = [
+ { title: 'Home', path: '/' },
+ ...segments.map((segment, index) => ({
+ title: upperFirst(segment).replaceAll('-', ' '),
+ path: '/' + segments.slice(0, index + 1).join('/'),
+ })),
+ ]
+
+ return (
+
+
+
+ }
+ className="text-gray-800 dark:text-gray-200"
+ itemClasses={{
+ base: 'transition-colors duration-200',
+ item: 'text-sm font-medium',
+ separator: 'flex items-center',
+ }}
+ >
+ {items.map((item, index) => {
+ const isLast = index === items.length - 1
+
+ return (
+
+ {isLast ? (
+
+ {item.title}
+
+ ) : (
+
+ {item.title}
+
+ )}
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/frontend/src/components/PageLayout.tsx b/frontend/src/components/PageLayout.tsx
new file mode 100644
index 0000000000..f2802053d4
--- /dev/null
+++ b/frontend/src/components/PageLayout.tsx
@@ -0,0 +1,28 @@
+'use client'
+
+import { useBreadcrumbs } from 'hooks/useBreadcrumbs'
+import type { ReactNode } from 'react'
+import BreadCrumbRenderer from 'components/BreadCrumbs'
+
+interface PageLayoutProps {
+ breadcrumbData?: {
+ projectName?: string
+ memberName?: string
+ chapterName?: string
+ committeeName?: string
+ orgName?: string
+ repoName?: string
+ }
+ children: ReactNode
+}
+
+export default function PageLayout({ breadcrumbData, children }: PageLayoutProps) {
+ const breadcrumbItems = useBreadcrumbs(breadcrumbData)
+
+ return (
+ <>
+
+ {children}
+ >
+ )
+}
diff --git a/frontend/src/components/RepositoryCard.tsx b/frontend/src/components/RepositoryCard.tsx
index 02450e2f61..f40272317d 100644
--- a/frontend/src/components/RepositoryCard.tsx
+++ b/frontend/src/components/RepositoryCard.tsx
@@ -23,7 +23,12 @@ const RepositoryCard: React.FC
= ({
{displayedRepositories.map((repository) => {
- return
+ return (
+
+ )
})}
{repositories.length > maxInitialDisplay &&
}
diff --git a/frontend/src/hooks/useBreadcrumbs.ts b/frontend/src/hooks/useBreadcrumbs.ts
new file mode 100644
index 0000000000..d389f722be
--- /dev/null
+++ b/frontend/src/hooks/useBreadcrumbs.ts
@@ -0,0 +1,64 @@
+import upperFirst from 'lodash/upperFirst'
+import { usePathname } from 'next/navigation'
+
+export interface BreadCrumbItem {
+ title: string
+ path: string
+}
+
+interface BreadcrumbData {
+ projectName?: string
+ memberName?: string
+ chapterName?: string
+ committeeName?: string
+ orgName?: string
+ repoName?: string
+}
+
+export function useBreadcrumbs(breadcrumbData?: BreadcrumbData): BreadCrumbItem[] {
+ const pathname = usePathname()
+ const breadcrumbs: BreadCrumbItem[] = [{ title: 'Home', path: '/' }]
+
+ if (!pathname) return breadcrumbs
+
+ const segments = pathname.split('/').filter(Boolean)
+
+ const isNestedRepoRoute =
+ segments.length === 4 && segments[0] === 'organizations' && segments[2] === 'repositories'
+
+ if (isNestedRepoRoute) {
+ breadcrumbs.push({ title: 'Organizations', path: '/organizations' })
+
+ const orgTitle = breadcrumbData?.orgName || upperFirst(segments[1]).replaceAll('-', ' ')
+ breadcrumbs.push({
+ title: orgTitle,
+ path: `/organizations/${segments[1]}`,
+ })
+
+ const repoTitle = breadcrumbData?.repoName || upperFirst(segments[3]).replaceAll('-', ' ')
+ breadcrumbs.push({
+ title: repoTitle,
+ path: `/organizations/${segments[1]}/repositories/${segments[3]}`,
+ })
+ } else {
+ segments.forEach((segment, index) => {
+ const isLastSegment = index === segments.length - 1
+ const path = '/' + segments.slice(0, index + 1).join('/')
+
+ let title = upperFirst(segment).replaceAll('-', ' ')
+
+ if (isLastSegment && breadcrumbData) {
+ title =
+ breadcrumbData.projectName ||
+ breadcrumbData.memberName ||
+ breadcrumbData.chapterName ||
+ breadcrumbData.committeeName ||
+ breadcrumbData.orgName ||
+ title
+ }
+ breadcrumbs.push({ title, path })
+ })
+ }
+
+ return breadcrumbs
+}
diff --git a/frontend/src/server/queries/repositoryQueries.ts b/frontend/src/server/queries/repositoryQueries.ts
index 51a0addc14..27f93ffc40 100644
--- a/frontend/src/server/queries/repositoryQueries.ts
+++ b/frontend/src/server/queries/repositoryQueries.ts
@@ -31,6 +31,7 @@ export const GET_REPOSITORY_DATA = gql`
organization {
id
login
+ name
}
project {
id
diff --git a/frontend/src/types/__generated__/repositoryQueries.generated.ts b/frontend/src/types/__generated__/repositoryQueries.generated.ts
index 9acb615929..3a5341098e 100644
--- a/frontend/src/types/__generated__/repositoryQueries.generated.ts
+++ b/frontend/src/types/__generated__/repositoryQueries.generated.ts
@@ -7,7 +7,7 @@ export type GetRepositoryDataQueryVariables = Types.Exact<{
}>;
-export type GetRepositoryDataQuery = { repository: { __typename: 'RepositoryNode', id: string, commitsCount: number, contributorsCount: number, createdAt: any, description: string, forksCount: number, isArchived: boolean, key: string, languages: Array
, license: string, name: string, openIssuesCount: number, size: number, starsCount: number, topics: Array, updatedAt: any, url: string, issues: Array<{ __typename: 'IssueNode', id: string, organizationName: string | null, repositoryName: string | null, createdAt: any, title: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }>, organization: { __typename: 'OrganizationNode', id: string, login: string } | null, project: { __typename: 'ProjectNode', id: string, key: string, name: string } | null, releases: Array<{ __typename: 'ReleaseNode', id: string, isPreRelease: boolean, name: string, organizationName: string | null, publishedAt: any | null, repositoryName: string | null, tagName: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, name: string, login: string } | null }>, recentMilestones: Array<{ __typename: 'MilestoneNode', id: string, title: string, openIssuesCount: number, closedIssuesCount: number, repositoryName: string | null, organizationName: string | null, createdAt: any, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }> } | null, topContributors: Array<{ __typename: 'RepositoryContributorNode', id: string, avatarUrl: string, login: string, name: string }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', id: string, createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }> };
+export type GetRepositoryDataQuery = { repository: { __typename: 'RepositoryNode', id: string, commitsCount: number, contributorsCount: number, createdAt: any, description: string, forksCount: number, isArchived: boolean, key: string, languages: Array, license: string, name: string, openIssuesCount: number, size: number, starsCount: number, topics: Array, updatedAt: any, url: string, issues: Array<{ __typename: 'IssueNode', id: string, organizationName: string | null, repositoryName: string | null, createdAt: any, title: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }>, organization: { __typename: 'OrganizationNode', id: string, login: string, name: string } | null, project: { __typename: 'ProjectNode', id: string, key: string, name: string } | null, releases: Array<{ __typename: 'ReleaseNode', id: string, isPreRelease: boolean, name: string, organizationName: string | null, publishedAt: any | null, repositoryName: string | null, tagName: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, name: string, login: string } | null }>, recentMilestones: Array<{ __typename: 'MilestoneNode', id: string, title: string, openIssuesCount: number, closedIssuesCount: number, repositoryName: string | null, organizationName: string | null, createdAt: any, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }> } | null, topContributors: Array<{ __typename: 'RepositoryContributorNode', id: string, avatarUrl: string, login: string, name: string }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', id: string, createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }> };
export type GetRepositoryMetadataQueryVariables = Types.Exact<{
repositoryKey: Types.Scalars['String']['input'];
@@ -18,5 +18,5 @@ export type GetRepositoryMetadataQueryVariables = Types.Exact<{
export type GetRepositoryMetadataQuery = { repository: { __typename: 'RepositoryNode', id: string, description: string, name: string } | null };
-export const GetRepositoryDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetRepositoryData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"repositoryKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organizationKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"repository"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"repositoryKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repositoryKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"organizationKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organizationKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"commitsCount"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"isArchived"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"issues"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}},{"kind":"Field","name":{"kind":"Name","value":"languages"}},{"kind":"Field","name":{"kind":"Name","value":"license"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"organization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}}]}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"releases"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"login"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isPreRelease"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"publishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"tagName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"topics"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"recentMilestones"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"closedIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"topContributors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organization"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organizationKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"repository"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repositoryKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPullRequests"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}},{"kind":"Argument","name":{"kind":"Name","value":"organization"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organizationKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"repository"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repositoryKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode;
+export const GetRepositoryDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetRepositoryData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"repositoryKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organizationKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"repository"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"repositoryKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repositoryKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"organizationKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organizationKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"commitsCount"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"isArchived"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"issues"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}},{"kind":"Field","name":{"kind":"Name","value":"languages"}},{"kind":"Field","name":{"kind":"Name","value":"license"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"organization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"releases"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"login"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isPreRelease"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"publishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"tagName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"topics"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"recentMilestones"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"closedIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"topContributors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organization"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organizationKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"repository"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repositoryKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPullRequests"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}},{"kind":"Argument","name":{"kind":"Name","value":"organization"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organizationKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"repository"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repositoryKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode;
export const GetRepositoryMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetRepositoryMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"repositoryKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organizationKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"repository"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"repositoryKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repositoryKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"organizationKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organizationKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode;
\ No newline at end of file