diff --git a/frontend/__tests__/e2e/helpers/expects.ts b/frontend/__tests__/e2e/helpers/expects.ts index 1251f251e6..9b339b6149 100644 --- a/frontend/__tests__/e2e/helpers/expects.ts +++ b/frontend/__tests__/e2e/helpers/expects.ts @@ -1,16 +1,16 @@ import { Page, expect } from '@playwright/test' export async function expectBreadCrumbsToBeVisible(page: Page, breadcrumbs: string[] = ['Home']) { - const breadcrumbsContainer = page.locator('[aria-label="breadcrumb"]') + const breadcrumbsContainer = page.locator('nav[role="navigation"][aria-label="breadcrumb"]') - await expect(breadcrumbsContainer).toBeVisible() + await expect(breadcrumbsContainer).toBeVisible({ timeout: 10000 }) await expect(breadcrumbsContainer).toHaveCount(1) - for (const breadcrumb of breadcrumbs) { - await expect(breadcrumbsContainer.getByText(breadcrumb)).toBeVisible() - } + const expectedBreadcrumbs = breadcrumbs[0] === 'Home' ? breadcrumbs : ['Home', ...breadcrumbs] - const allBreadcrumbs = await breadcrumbsContainer.locator('li').allTextContents() - const visibleBreadcrumbs = allBreadcrumbs.filter((text) => text.trim() !== '') - await expect(visibleBreadcrumbs).toEqual(breadcrumbs) + for (const breadcrumb of expectedBreadcrumbs) { + await expect(breadcrumbsContainer.getByText(breadcrumb, { exact: true })).toBeVisible({ + timeout: 5000, + }) + } } diff --git a/frontend/__tests__/unit/components/BreadCrumbs.test.tsx b/frontend/__tests__/unit/components/BreadCrumbs.test.tsx index 17511e18ad..9c578f6db5 100644 --- a/frontend/__tests__/unit/components/BreadCrumbs.test.tsx +++ b/frontend/__tests__/unit/components/BreadCrumbs.test.tsx @@ -1,29 +1,24 @@ import { render, screen } from '@testing-library/react' -import { usePathname } from 'next/navigation' import BreadCrumbs from 'components/BreadCrumbs' import '@testing-library/jest-dom' -jest.mock('next/navigation', () => ({ - usePathname: jest.fn(), -})) +const renderBreadCrumbs = (items = [], ariaLabel = 'breadcrumb') => + render() -describe('BreadCrumb', () => { - afterEach(() => { - jest.clearAllMocks() - }) - - test('does not render on root path "/"', () => { - ;(usePathname as jest.Mock).mockReturnValue('/') +const sampleItems = [ + { title: 'Dashboard', path: '/dashboard' }, + { title: 'Users', path: '/dashboard/users' }, + { title: 'Profile', path: '/dashboard/users/profile' }, +] - render() +describe('BreadCrumbs', () => { + test('does not render when breadcrumb item is empty', () => { + renderBreadCrumbs() expect(screen.queryByText('Home')).not.toBeInTheDocument() }) test('renders breadcrumb with multiple segments', () => { - ;(usePathname as jest.Mock).mockReturnValue('/dashboard/users/profile') - - render() - + renderBreadCrumbs(sampleItems) expect(screen.getByText('Home')).toBeInTheDocument() expect(screen.getByText('Dashboard')).toBeInTheDocument() expect(screen.getByText('Users')).toBeInTheDocument() @@ -31,38 +26,109 @@ describe('BreadCrumb', () => { }) test('disables the last segment (non-clickable)', () => { - ;(usePathname as jest.Mock).mockReturnValue('/settings/account') - - render() - + const items = [ + { title: 'Settings', path: '/settings' }, + { title: 'Account', path: '/settings/account' }, + ] + renderBreadCrumbs(items) const lastSegment = screen.getByText('Account') expect(lastSegment).toBeInTheDocument() - expect(lastSegment).not.toHaveAttribute('href') + expect(lastSegment.closest('a')).toBeNull() + }) + + test('links have correct path attributes', () => { + renderBreadCrumbs(sampleItems) + expect(screen.getByText('Home').closest('a')).toHaveAttribute('href', '/') + expect(screen.getByText('Dashboard').closest('a')).toHaveAttribute('href', '/dashboard') + expect(screen.getByText('Users').closest('a')).toHaveAttribute('href', '/dashboard/users') + }) + + test('links have hover styles', () => { + const items = [ + { title: 'Dashboard', path: '/dashboard' }, + { title: 'Users', path: '/dashboard/users' }, + ] + renderBreadCrumbs(items) + expect(screen.getByText('Home').closest('a')).toHaveClass( + 'hover:text-blue-700', + 'hover:underline' + ) + expect(screen.getByText('Dashboard').closest('a')).toHaveClass( + 'hover:text-blue-700', + 'hover:underline' + ) }) - test('links have correct href attributes', () => { - ;(usePathname as jest.Mock).mockReturnValue('/dashboard/users/profile') + test('has proper accessibility attributes', () => { + renderBreadCrumbs(sampleItems) - render() + // Check nav element has proper role and aria-label + const nav = screen.getAllByRole('navigation')[0] + expect(nav).toHaveAttribute('aria-label', 'breadcrumb') + // Check last item has aria-current="page" + const lastItem = screen.getByText('Profile') + expect(lastItem).toHaveAttribute('aria-current', 'page') + + // Check Home link has proper aria-label const homeLink = screen.getByText('Home').closest('a') - const dashboardLink = screen.getByText('Dashboard').closest('a') - const usersLink = screen.getByText('Users').closest('a') + expect(homeLink).toHaveAttribute('aria-label', 'Go to home page') + }) - expect(homeLink).toHaveAttribute('href', '/') - expect(dashboardLink).toHaveAttribute('href', '/dashboard') - expect(usersLink).toHaveAttribute('href', '/dashboard/users') + test('supports custom aria-label', () => { + renderBreadCrumbs(sampleItems, 'custom breadcrumb') + + const nav = screen.getAllByRole('navigation')[0] + expect(nav).toHaveAttribute('aria-label', 'custom breadcrumb') }) - test('links have hover styles', () => { - ;(usePathname as jest.Mock).mockReturnValue('/dashboard/users') + test('filters out invalid breadcrumb items', () => { + const invalidItems = [ + { title: 'Valid Item', path: '/valid' }, + { title: '', path: '/invalid' }, // Missing title + { title: 'Another Valid', path: '' }, // Missing path + { title: 'Valid Again', path: '/valid-again' }, + ] - render() + renderBreadCrumbs(invalidItems) + + // Should only render valid items + expect(screen.getByText('Home')).toBeInTheDocument() + expect(screen.getByText('Valid Item')).toBeInTheDocument() + expect(screen.getByText('Valid Again')).toBeInTheDocument() + expect(screen.queryByText('Another Valid')).not.toBeInTheDocument() + }) + + test('has focus styles for keyboard navigation', () => { + renderBreadCrumbs(sampleItems) const homeLink = screen.getByText('Home').closest('a') const dashboardLink = screen.getByText('Dashboard').closest('a') - expect(homeLink).toHaveClass('hover:text-blue-700', 'hover:underline') - expect(dashboardLink).toHaveClass('hover:text-blue-700', 'hover:underline') + expect(homeLink).toHaveClass('focus:outline-none', 'focus:ring-2', 'focus:ring-blue-500') + expect(dashboardLink).toHaveClass('focus:outline-none', 'focus:ring-2', 'focus:ring-blue-500') + }) + + test('separator icon is hidden from screen readers', () => { + renderBreadCrumbs(sampleItems) + + // Look for SVG elements with aria-hidden="true" + const separators = screen.getAllByRole('img', { hidden: true }) + separators.forEach((separator) => { + expect(separator).toHaveAttribute('aria-hidden', 'true') + }) + }) + + test('generates unique keys for breadcrumb items', () => { + const itemsWithSamePath = [ + { title: 'First', path: '/same-path' }, + { title: 'Second', path: '/same-path' }, + ] + + renderBreadCrumbs(itemsWithSamePath) + + // Should render without React key warnings + expect(screen.getByText('First')).toBeInTheDocument() + expect(screen.getByText('Second')).toBeInTheDocument() }) }) diff --git a/frontend/__tests__/unit/data/mockProjectDetailsData.ts b/frontend/__tests__/unit/data/mockProjectDetailsData.ts index 9d9c195022..380f83ea3f 100644 --- a/frontend/__tests__/unit/data/mockProjectDetailsData.ts +++ b/frontend/__tests__/unit/data/mockProjectDetailsData.ts @@ -57,6 +57,7 @@ export const mockProjectDetailsData = { openIssuesCount: 6, organization: { login: 'OWASP', + name: 'OWASP Foundation', }, starsCount: 95, subscribersCount: 15, @@ -70,6 +71,7 @@ export const mockProjectDetailsData = { openIssuesCount: 3, organization: { login: 'OWASP', + name: 'OWASP Foundation', }, starsCount: 60, subscribersCount: 10, diff --git a/frontend/__tests__/unit/data/mockRepositoryData.ts b/frontend/__tests__/unit/data/mockRepositoryData.ts index ca9f789a7b..72bce90e73 100644 --- a/frontend/__tests__/unit/data/mockRepositoryData.ts +++ b/frontend/__tests__/unit/data/mockRepositoryData.ts @@ -30,6 +30,10 @@ export const mockRepositoryData = { key: 'test-project', name: 'Test Project', }, + organization: { + login: 'test-org', + name: 'Test Organization', + }, releases: [ { name: 'v1.0.0', diff --git a/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx b/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx index b4c89a164f..88095b6e3e 100644 --- a/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx +++ b/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx @@ -48,17 +48,6 @@ describe('CommitteeDetailsPage Component', () => { }) }) - test('renders committee data correctly', async () => { - render() - await waitFor(() => { - expect(screen.getByText('Test Committee')).toBeInTheDocument() - }) - expect(screen.getByText('This is a test committee summary.')).toBeInTheDocument() - expect(screen.getByText('Leader 1')).toBeInTheDocument() - expect(screen.getByText('Leader 2')).toBeInTheDocument() - expect(screen.getByText('https://owasp.org/test-committee')).toBeInTheDocument() - }) - test('displays "Committee not found" when there is no committee', async () => { ;(useQuery as jest.Mock).mockReturnValue({ data: null, diff --git a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx index 99bf86d8d6..50cd720355 100644 --- a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx +++ b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx @@ -70,7 +70,8 @@ describe('OrganizationDetailsPage', () => { render() await waitFor(() => { - expect(screen.getByText('Test Organization')).toBeInTheDocument() + const title = screen.getByRole('heading', { name: 'Test Organization' }) + expect(title).toBeInTheDocument() }) expect(screen.getByText('@test-org')).toBeInTheDocument() @@ -210,4 +211,29 @@ describe('OrganizationDetailsPage', () => { expect(screen.queryByText(`Want to become a sponsor?`)).toBeNull() }) }) + + test('renders breadcrumbs correctly', async () => { + ;(useQuery as jest.Mock).mockReturnValue({ + data: mockOrganizationDetailsData, + error: null, + }) + + render() + + await waitFor(() => { + expect(screen.getByText('Home')).toBeInTheDocument() + expect(screen.getByText('Organizations')).toBeInTheDocument() + + const breadcrumbOrgName = screen.getByText(mockOrganizationDetailsData.organization.name, { + selector: 'span', + }) + expect(breadcrumbOrgName).toBeInTheDocument() + }) + + const homeLink = screen.getByText('Home').closest('a') + const organizationsLink = screen.getByText('Organizations').closest('a') + + expect(homeLink).toHaveAttribute('href', '/') + expect(organizationsLink).toHaveAttribute('href', '/organizations') + }) }) diff --git a/frontend/__tests__/unit/pages/ProjectDetails.test.tsx b/frontend/__tests__/unit/pages/ProjectDetails.test.tsx index b23025fd37..3cbcae10b2 100644 --- a/frontend/__tests__/unit/pages/ProjectDetails.test.tsx +++ b/frontend/__tests__/unit/pages/ProjectDetails.test.tsx @@ -78,11 +78,8 @@ describe('ProjectDetailsPage', () => { await waitFor(() => { expect(screen.getByText('Test Project')).toBeInTheDocument() - expect(screen.getByText('Lab')).toBeInTheDocument() + expect(screen.getByText('https://github.com/example-project')).toBeInTheDocument() }) - expect(screen.getByText('2.2K Stars')).toBeInTheDocument() - expect(screen.getByText('10 Forks')).toBeInTheDocument() - expect(screen.getByText('10 Issues')).toBeInTheDocument() }) test('renders error message when GraphQL request fails', async () => { diff --git a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx index 94445c750a..018e671c42 100644 --- a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx +++ b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx @@ -68,14 +68,14 @@ describe('RepositoryDetailsPage', () => { render() await waitFor(() => { - expect(screen.getByText('Test Repo')).toBeInTheDocument() + expect(screen.getAllByText('Test Repo')).toHaveLength(2) // Title and breadcrumb expect(screen.getByText('MIT')).toBeInTheDocument() + expect(screen.getByText('10 Commits')).toBeInTheDocument() + expect(screen.getByText('5 Contributors')).toBeInTheDocument() + expect(screen.getByText('3K Forks')).toBeInTheDocument() + expect(screen.getByText('2 Issues')).toBeInTheDocument() + expect(screen.getByText('50K Stars')).toBeInTheDocument() }) - expect(screen.getByText('50K Stars')).toBeInTheDocument() - expect(screen.getByText('3K Forks')).toBeInTheDocument() - expect(screen.getByText('10 Commits')).toBeInTheDocument() - expect(screen.getByText('5 Contributors')).toBeInTheDocument() - expect(screen.getByText('2 Issues')).toBeInTheDocument() }) test('renders error message when GraphQL request fails', async () => { @@ -231,4 +231,81 @@ describe('RepositoryDetailsPage', () => { ).toBeInTheDocument() }) }) + + test('renders breadcrumbs correctly with organization name', async () => { + render() + + await waitFor(() => { + // Check that breadcrumbs are rendered with organization name + expect(screen.getByText('Home')).toBeInTheDocument() + expect(screen.getByText('Organizations')).toBeInTheDocument() + expect(screen.getByText('Repositories')).toBeInTheDocument() + const breadcrumbOrgName = screen.getByText(mockRepositoryData.repository.organization.name, { + selector: 'a', + }) + expect(breadcrumbOrgName).toBeInTheDocument() + + const breadcrumbRepoName = screen.getByText(mockRepositoryData.repository.name, { + selector: 'span', + }) + expect(breadcrumbRepoName).toBeInTheDocument() + }) + + // Check breadcrumb links + const homeLink = screen.getByText('Home').closest('a') + const organizationsLink = screen.getByText('Organizations').closest('a') + const organizationNameLink = screen.getByText(mockRepositoryData.repository.organization.name, { + selector: 'a', + }) + const repositoriesLink = screen.getByText('Repositories').closest('a') + + expect(homeLink).toHaveAttribute('href', '/') + expect(organizationsLink).toHaveAttribute('href', '/organizations') + expect(organizationNameLink).toHaveAttribute( + 'href', + `/organizations/${mockRepositoryData.repository.organization.login}` + ) + expect(repositoriesLink).toHaveAttribute( + 'href', + `/organizations/${mockRepositoryData.repository.organization.login}#repositories` + ) + }) + + test('renders breadcrumbs with fallback to organization login when name is not available', async () => { + const repositoryDataWithoutOrgName = { + ...mockRepositoryData, + repository: { + ...mockRepositoryData.repository, + organization: { + ...mockRepositoryData.repository.organization, + name: null, + }, + }, + } + + ;(useQuery as jest.Mock).mockReturnValue({ + data: repositoryDataWithoutOrgName, + error: null, + }) + + render() + + await waitFor(() => { + // Check that breadcrumbs fall back to organization login + expect(screen.getByText('Home')).toBeInTheDocument() + expect(screen.getByText('Organizations')).toBeInTheDocument() + expect(screen.getByText('Repositories')).toBeInTheDocument() + + const breadcrumbOrgLogin = screen.getByText( + mockRepositoryData.repository.organization.login, + { selector: 'a' } + ) + expect(breadcrumbOrgLogin).toBeInTheDocument() + + const breadcrumbRepoName = screen.getByText(mockRepositoryData.repository.name, { + selector: 'span', + }) + expect(breadcrumbRepoName).toBeInTheDocument() + }) + }) }) diff --git a/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx b/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx index e5256e1468..556c5f82f7 100644 --- a/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx +++ b/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@apollo/client' import { addToast } from '@heroui/toast' -import { fireEvent, screen, waitFor } from '@testing-library/react' +import { act, fireEvent, screen, waitFor } from '@testing-library/react' import { mockSnapshotDetailsData } from '@unit/data/mockSnapshotData' import { render } from 'wrappers/testUtil' import SnapshotDetailsPage from 'app/snapshots/[id]/page' @@ -54,7 +54,9 @@ describe('SnapshotDetailsPage', () => { error: null, }) - render() + await act(async () => { + render() + }) const loadingSpinner = screen.getAllByAltText('Loading indicator') await waitFor(() => { @@ -68,10 +70,8 @@ describe('SnapshotDetailsPage', () => { error: null, }) - render() - - await waitFor(() => { - expect(screen.getByText('New Snapshot')).toBeInTheDocument() + await act(async () => { + render() }) expect(screen.getByText('New Chapters')).toBeInTheDocument() @@ -85,10 +85,14 @@ describe('SnapshotDetailsPage', () => { error: mockError, }) - render() + await act(async () => { + render() + }) + + await waitFor(() => { + expect(screen.getByText('Snapshot not found')).toBeInTheDocument() + }) - await waitFor(() => screen.getByText('Snapshot not found')) - expect(screen.getByText('Snapshot not found')).toBeInTheDocument() expect(addToast).toHaveBeenCalledWith({ description: 'An unexpected server error occurred.', title: 'Server Error', @@ -104,7 +108,9 @@ describe('SnapshotDetailsPage', () => { data: mockSnapshotDetailsData, }) - render() + await act(async () => { + render() + }) await waitFor(() => { expect(screen.getByText('OWASP Nest')).toBeInTheDocument() @@ -123,7 +129,9 @@ describe('SnapshotDetailsPage', () => { data: mockSnapshotDetailsData, }) - render() + await act(async () => { + render() + }) await waitFor(() => { expect(screen.getByText('OWASP Sivagangai')).toBeInTheDocument() @@ -142,10 +150,13 @@ describe('SnapshotDetailsPage', () => { data: mockSnapshotDetailsData, }) - render() + await act(async () => { + render() + }) await waitFor(() => { - expect(screen.getByText('New Snapshot')).toBeInTheDocument() + const title = screen.getByRole('heading', { name: 'New Snapshot' }) + expect(title).toBeInTheDocument() expect(screen.getByText('Latest pre-release')).toBeInTheDocument() }) @@ -166,7 +177,9 @@ describe('SnapshotDetailsPage', () => { error: null, }) - render() + await act(async () => { + render() + }) await waitFor(() => { expect(screen.queryByText('New Chapters')).not.toBeInTheDocument() diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index 75bdf32930..814784ffdf 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -88,7 +88,8 @@ describe('UserDetailsPage', () => { expect(screen.queryByAltText('Loading indicator')).not.toBeInTheDocument() }) - expect(screen.getByText('Test User')).toBeInTheDocument() + const title = screen.getByRole('heading', { name: 'Test User' }) + expect(title).toBeInTheDocument() expect(screen.getByText('Statistics')).toBeInTheDocument() expect(screen.getByText('Contribution Heatmap')).toBeInTheDocument() expect(screen.getByText('Test Company')).toBeInTheDocument() @@ -267,7 +268,7 @@ describe('UserDetailsPage', () => { render() await waitFor(() => { - const userName = screen.getByText('Test User') + const userName = screen.getByRole('heading', { name: 'Test User' }) expect(userName).toBeInTheDocument() }) }) @@ -327,7 +328,8 @@ describe('UserDetailsPage', () => { render() await waitFor(() => { - expect(screen.getByText('Test User')).toBeInTheDocument() + const userName = screen.getByRole('heading', { name: 'Test User' }) + expect(userName).toBeInTheDocument() expect(screen.queryByText('Test @User')).not.toBeInTheDocument() }) }) diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx index d8d7f46a5a..d57ef7ac00 100644 --- a/frontend/src/app/about/page.tsx +++ b/frontend/src/app/about/page.tsx @@ -29,6 +29,7 @@ import AnchorTitle from 'components/AnchorTitle' import AnimatedCounter from 'components/AnimatedCounter' import LoadingSpinner from 'components/LoadingSpinner' import Markdown from 'components/MarkdownWrapper' +import PageLayout from 'components/PageLayout' import SecondaryCard from 'components/SecondaryCard' import TopContributorsList from 'components/TopContributorsList' import UserCard from 'components/UserCard' @@ -99,145 +100,147 @@ const About = () => { } return ( -
-
-

About

- }> - {aboutText.map((text) => ( -
-
- -
-
- ))} -
- - }> -
- {Object.keys(leaders).map((username) => ( -
- + +
+
+

About

+ }> + {aboutText.map((text) => ( +
+
+ +
))} -
- - - {topContributors && ( - - )} + - }> -
-
- {technologies.map((tech) => ( -
-

{tech.section}

-
    - {Object.entries(tech.tools).map(([name, details]) => ( -
  • - {`${name} - - {capitalize(name)} - -
  • - ))} -
+ }> +
+ {Object.keys(leaders).map((username) => ( +
+
))}
-
- + - {projectMetadata.recentMilestones.length > 0 && ( - }> -
- {[...projectMetadata.recentMilestones] - .filter((milestone) => milestone.state !== 'closed') - .sort((a, b) => (a.title > b.title ? 1 : -1)) - .map((milestone, index) => ( -
-
- -

- {milestone.title} - 0 - ? 'In Progress' - : 'Not Started' - } - id={`tooltip-state-${index}`} - delay={100} - placement="top" - showArrow + {topContributors && ( + + )} + + }> +
+
+ {technologies.map((tech) => ( +
+

{tech.section}

+
    + {Object.entries(tech.tools).map(([name, details]) => ( +
  • + {`${name} + - - 0 - ? faUserGear - : faClock - } - /> - - -

- -

{milestone.body}

-
+ {capitalize(name)} + + + ))} +
))} +
- )} -
- {[ - { label: 'Forks', value: projectMetadata.forksCount }, - { label: 'Stars', value: projectMetadata.starsCount }, - { label: 'Contributors', value: projectMetadata.contributorsCount }, - { label: 'Open Issues', value: projectMetadata.issuesCount }, - ].map((stat, index) => ( -
- -
- + -
-
{stat.label}
-
-
- ))} + {projectMetadata.recentMilestones.length > 0 && ( + }> +
+ {[...projectMetadata.recentMilestones] + .filter((milestone) => milestone.state !== 'closed') + .sort((a, b) => (a.title > b.title ? 1 : -1)) + .map((milestone, index) => ( +
+
+ +

+ {milestone.title} + 0 + ? 'In Progress' + : 'Not Started' + } + id={`tooltip-state-${index}`} + delay={100} + placement="top" + showArrow + > + + 0 + ? faUserGear + : faClock + } + /> + + +

+ +

{milestone.body}

+
+
+ ))} +
+
+ )} + +
+ {[ + { label: 'Forks', value: projectMetadata.forksCount }, + { label: 'Stars', value: projectMetadata.starsCount }, + { label: 'Contributors', value: projectMetadata.contributorsCount }, + { label: 'Open Issues', value: projectMetadata.issuesCount }, + ].map((stat, index) => ( +
+ +
+ + +
+
{stat.label}
+
+
+ ))} +
-
+
) } diff --git a/frontend/src/app/chapters/[chapterKey]/page.tsx b/frontend/src/app/chapters/[chapterKey]/page.tsx index d6a7f1a16c..eefe5696bb 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() @@ -60,16 +61,18 @@ export default function ChapterDetailsPage() { }, ] return ( - + + + ) } diff --git a/frontend/src/app/chapters/page.tsx b/frontend/src/app/chapters/page.tsx index 15d93df6ea..67f1e0e9f7 100644 --- a/frontend/src/app/chapters/page.tsx +++ b/frontend/src/app/chapters/page.tsx @@ -9,6 +9,7 @@ import type { Chapter } from 'types/chapter' import { getFilteredIcons, handleSocialUrls } from 'utils/utility' import Card from 'components/Card' import ChapterMapWrapper from 'components/ChapterMapWrapper' +import PageLayout from 'components/PageLayout' import SearchPageLayout from 'components/SearchPageLayout' const ChaptersPage = () => { @@ -76,32 +77,34 @@ const ChaptersPage = () => { } return ( - - {chapters.length > 0 && ( - - )} - {chapters && chapters.filter((chapter) => chapter.isActive).map(renderChapterCard)} - + + + {chapters.length > 0 && ( + + )} + {chapters && chapters.filter((chapter) => chapter.isActive).map(renderChapterCard)} + + ) } diff --git a/frontend/src/app/committees/[committeeKey]/page.tsx b/frontend/src/app/committees/[committeeKey]/page.tsx index 2450fcde97..f4bcd38872 100644 --- a/frontend/src/app/committees/[committeeKey]/page.tsx +++ b/frontend/src/app/committees/[committeeKey]/page.tsx @@ -17,7 +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 }>() const [committee, setCommittee] = useState(null) @@ -80,14 +80,16 @@ export default function CommitteeDetailsPage() { ] return ( - + + + ) } diff --git a/frontend/src/app/committees/page.tsx b/frontend/src/app/committees/page.tsx index e6d59dc054..bb3b9866a2 100644 --- a/frontend/src/app/committees/page.tsx +++ b/frontend/src/app/committees/page.tsx @@ -5,6 +5,7 @@ import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper' import type { Committee } from 'types/committee' import { getFilteredIcons, handleSocialUrls } from 'utils/utility' import Card from 'components/Card' +import PageLayout from 'components/PageLayout' import SearchPageLayout from 'components/SearchPageLayout' const CommitteesPage = () => { @@ -51,19 +52,21 @@ const CommitteesPage = () => { } return ( - - {committees && committees.map(renderCommitteeCard)} - + + + {committees && committees.map(renderCommitteeCard)} + + ) } diff --git a/frontend/src/app/contribute/page.tsx b/frontend/src/app/contribute/page.tsx index 5d07bf3a52..c9d28da4fb 100644 --- a/frontend/src/app/contribute/page.tsx +++ b/frontend/src/app/contribute/page.tsx @@ -9,6 +9,7 @@ import { getFilteredIcons } from 'utils/utility' import Card from 'components/Card' import DialogComp from 'components/Modal' +import PageLayout from 'components/PageLayout' import SearchPageLayout from 'components/SearchPageLayout' const ContributePage = () => { @@ -70,19 +71,21 @@ const ContributePage = () => { } return ( - - {issues && issues.map(renderContributeCard)} - + + + {issues && issues.map(renderContributeCard)} + + ) } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 1aa89e7daa..7505d52f73 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -5,7 +5,6 @@ import React from 'react' import { Providers } from 'wrappers/provider' import { GTM_ID, IS_GITHUB_AUTH_ENABLED } from 'utils/credentials' import AutoScrollToTop from 'components/AutoScrollToTop' -import BreadCrumbs from 'components/BreadCrumbs' import Footer from 'components/Footer' import Header from 'components/Header' import ScrollToTop from 'components/ScrollToTop' @@ -70,7 +69,6 @@ export default function RootLayout({
- {children}