diff --git a/frontend/__tests__/testUtils/sharedAssertions.ts b/frontend/__tests__/testUtils/sharedAssertions.ts new file mode 100644 index 0000000000..c39e30dfff --- /dev/null +++ b/frontend/__tests__/testUtils/sharedAssertions.ts @@ -0,0 +1,70 @@ +import { screen, waitFor, fireEvent } from '@testing-library/react' + +export const assertRepoDetails = async ({ + heading, + license, + stars, + forks, + commits, + contributors, + issues, +}: { + heading: string + license: string + stars: string + forks: string + commits?: string + contributors?: string + issues: string +}) => { + await waitFor(() => { + const title = screen.getByRole('heading', { name: heading }) + expect(title).toBeInTheDocument() + expect(screen.getByText(license)).toBeInTheDocument() + }) + expect(screen.getByText(stars)).toBeInTheDocument() + expect(screen.getByText(forks)).toBeInTheDocument() + if (commits) expect(screen.getByText(commits)).toBeInTheDocument() + if (contributors) expect(screen.getByText(contributors)).toBeInTheDocument() + expect(screen.getByText(issues)).toBeInTheDocument() +} + +export const assertHeadingsAndTexts = async ({ + headingText, + texts, +}: { + headingText: string + texts: string[] +}) => { + await waitFor(() => { + const heading = screen.getByRole('heading', { name: headingText }) + expect(heading).toBeInTheDocument() + }) + + texts.forEach((text) => { + expect(screen.getByText(text)).toBeInTheDocument() + }) +} + +export const assertContributorToggle = async (initial: string, others: string[]) => { + await waitFor(() => { + expect(screen.getByText(initial)).toBeInTheDocument() + expect(screen.queryByText('Contributor 10')).not.toBeInTheDocument() + }) + + const showMoreButton = screen.getByRole('button', { name: /Show more/i }) + fireEvent.click(showMoreButton) + + await waitFor(() => { + others.forEach((name) => { + expect(screen.getByText(name)).toBeInTheDocument() + }) + }) + + const showLessButton = screen.getByRole('button', { name: /Show less/i }) + fireEvent.click(showLessButton) + + await waitFor(() => { + expect(screen.queryByText('Contributor 10')).not.toBeInTheDocument() + }) +} diff --git a/frontend/__tests__/unit/components/BreadCrumbs.test.tsx b/frontend/__tests__/unit/components/BreadCrumbs.test.tsx index 17511e18ad..01626ddda4 100644 --- a/frontend/__tests__/unit/components/BreadCrumbs.test.tsx +++ b/frontend/__tests__/unit/components/BreadCrumbs.test.tsx @@ -1,29 +1,23 @@ 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 = []) => 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 +25,36 @@ 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 href attributes', () => { - ;(usePathname as jest.Mock).mockReturnValue('/dashboard/users/profile') - - render() - - const homeLink = screen.getByText('Home').closest('a') - const dashboardLink = screen.getByText('Dashboard').closest('a') - const usersLink = screen.getByText('Users').closest('a') - - expect(homeLink).toHaveAttribute('href', '/') - expect(dashboardLink).toHaveAttribute('href', '/dashboard') - expect(usersLink).toHaveAttribute('href', '/dashboard/users') + 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', () => { - ;(usePathname as jest.Mock).mockReturnValue('/dashboard/users') - - render() - - 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') + 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' + ) }) }) diff --git a/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx b/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx index b4c89a164f..819b3fd501 100644 --- a/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx +++ b/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx @@ -1,5 +1,6 @@ import { useQuery } from '@apollo/client' import { screen, waitFor } from '@testing-library/react' +import { assertHeadingsAndTexts } from '@testUtils/sharedAssertions' import { mockCommitteeDetailsData } from '@unit/data/mockCommitteeDetailsData' import { render } from 'wrappers/testUtil' import CommitteeDetailsPage from 'app/committees/[committeeKey]/page' @@ -50,13 +51,15 @@ describe('CommitteeDetailsPage Component', () => { test('renders committee data correctly', async () => { render() - await waitFor(() => { - expect(screen.getByText('Test Committee')).toBeInTheDocument() + await assertHeadingsAndTexts({ + headingText: 'Test Committee', + texts: [ + 'This is a test committee summary.', + 'Leader 1', + 'Leader 2', + 'https://owasp.org/test-committee', + ], }) - 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 () => { diff --git a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx index 99bf86d8d6..7004f2ee7a 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() diff --git a/frontend/__tests__/unit/pages/ProjectDetails.test.tsx b/frontend/__tests__/unit/pages/ProjectDetails.test.tsx index b23025fd37..40211c90b8 100644 --- a/frontend/__tests__/unit/pages/ProjectDetails.test.tsx +++ b/frontend/__tests__/unit/pages/ProjectDetails.test.tsx @@ -1,6 +1,7 @@ import { useQuery } from '@apollo/client' import { addToast } from '@heroui/toast' import { act, fireEvent, screen, waitFor, within } from '@testing-library/react' +import { assertRepoDetails } from '@testUtils/sharedAssertions' import { mockProjectDetailsData } from '@unit/data/mockProjectDetailsData' import { render } from 'wrappers/testUtil' import ProjectDetailsPage from 'app/projects/[projectKey]/page' @@ -68,6 +69,7 @@ describe('ProjectDetailsPage', () => { }) }) + // eslint-disable-next-line jest/expect-expect test('renders project details when data is available', async () => { ;(useQuery as jest.Mock).mockReturnValue({ data: mockProjectDetailsData, @@ -75,14 +77,13 @@ describe('ProjectDetailsPage', () => { }) render() - - await waitFor(() => { - expect(screen.getByText('Test Project')).toBeInTheDocument() - expect(screen.getByText('Lab')).toBeInTheDocument() + await assertRepoDetails({ + heading: 'Test Project', + license: 'Lab', + stars: '2.2K Stars', + forks: '10 Forks', + issues: '10 Issues', }) - 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..21b9d6b726 100644 --- a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx +++ b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx @@ -1,6 +1,7 @@ import { useQuery } from '@apollo/client' import { addToast } from '@heroui/toast' import { act, fireEvent, screen, waitFor } from '@testing-library/react' +import { assertRepoDetails } from '@testUtils/sharedAssertions' import { mockRepositoryData } from '@unit/data/mockRepositoryData' import { render } from 'wrappers/testUtil' import RepositoryDetailsPage from 'app/organizations/[organizationKey]/repositories/[repositoryKey]/page' @@ -67,15 +68,15 @@ describe('RepositoryDetailsPage', () => { render() - await waitFor(() => { - expect(screen.getByText('Test Repo')).toBeInTheDocument() - expect(screen.getByText('MIT')).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() + await assertRepoDetails({ + heading: 'Test Repo', + license: 'MIT', + stars: '50K Stars', + forks: '3K Forks', + commits: '10 Commits', + contributors: '5 Contributors', + issues: '2 Issues', + }) }) test('renders error message when GraphQL request fails', async () => { diff --git a/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx b/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx index e5256e1468..35782e71ef 100644 --- a/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx +++ b/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx @@ -1,6 +1,7 @@ import { useQuery } from '@apollo/client' import { addToast } from '@heroui/toast' import { fireEvent, screen, waitFor } from '@testing-library/react' +import { assertHeadingsAndTexts } from '@testUtils/sharedAssertions' import { mockSnapshotDetailsData } from '@unit/data/mockSnapshotData' import { render } from 'wrappers/testUtil' import SnapshotDetailsPage from 'app/snapshots/[id]/page' @@ -70,13 +71,10 @@ describe('SnapshotDetailsPage', () => { render() - await waitFor(() => { - expect(screen.getByText('New Snapshot')).toBeInTheDocument() + await assertHeadingsAndTexts({ + headingText: 'New Snapshot', + texts: ['New Chapters', 'New Projects', 'New Releases'], }) - - expect(screen.getByText('New Chapters')).toBeInTheDocument() - expect(screen.getByText('New Projects')).toBeInTheDocument() - expect(screen.getByText('New Releases')).toBeInTheDocument() }) test('renders error message when GraphQL request fails', async () => { @@ -145,7 +143,8 @@ describe('SnapshotDetailsPage', () => { 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() }) 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/jest.config.ts b/frontend/jest.config.ts index 8dc944b2f2..7445a2161b 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -32,12 +32,17 @@ const config: Config = { globals: {}, setupFilesAfterEnv: ['/jest.setup.ts'], testEnvironment: 'jest-environment-jsdom', - testPathIgnorePatterns: ['/__tests__/unit/data/', '/__tests__/e2e/'], + testPathIgnorePatterns: [ + '/__tests__/unit/data/', + '/__tests__/e2e/', + '/__tests__/testUtils/', + ], transform: { '^.+\\.tsx?$': '@swc/jest', }, moduleNameMapper: { '^@unit/(.*)$': '/__tests__/unit/$1', + '^@testUtils/(.*)$': '/__tests__/testUtils/$1', '^@/(.*)$': '/src/$1', '\\.(scss|sass|css)$': 'identity-obj-proxy', }, diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx index d8d7f46a5a..07db90d068 100644 --- a/frontend/src/app/about/page.tsx +++ b/frontend/src/app/about/page.tsx @@ -24,11 +24,11 @@ import type { Contributor } from 'types/contributor' import type { Project } from 'types/project' import type { User } from 'types/user' import { aboutText, technologies } from 'utils/aboutData' -import { capitalize } from 'utils/capitalize' 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 +99,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}

-
+ {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..7b270f38f9 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.is_active).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/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index b97a1e773f..88cc48dc81 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -22,6 +22,7 @@ import { formatDate } from 'utils/dateFormatter' import { drawContributions, fetchHeatmapData, HeatmapData } from 'utils/helpers/githubHeatmap' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' +import PageLayout from 'components/PageLayout' const UserDetailsPage: React.FC = () => { const { memberKey } = useParams() @@ -190,20 +191,22 @@ const UserDetailsPage: React.FC = () => { ) return ( - } - pullRequests={pullRequests} - recentIssues={issues} - recentMilestones={milestones} - recentReleases={releases} - repositories={topRepositories} - showAvatar={false} - stats={userStats} - title={user?.name || user?.login} - type="user" - userSummary={} - /> + + } + pullRequests={pullRequests} + recentIssues={issues} + recentMilestones={milestones} + recentReleases={releases} + repositories={topRepositories} + showAvatar={false} + stats={userStats} + title={user?.name || user?.login} + type="user" + userSummary={} + /> + ) } diff --git a/frontend/src/app/members/page.tsx b/frontend/src/app/members/page.tsx index 955210d6d6..c98347118b 100644 --- a/frontend/src/app/members/page.tsx +++ b/frontend/src/app/members/page.tsx @@ -3,6 +3,7 @@ import { useSearchPage } from 'hooks/useSearchPage' import { useRouter } from 'next/navigation' import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper' import type { User } from 'types/user' +import PageLayout from 'components/PageLayout' import SearchPageLayout from 'components/SearchPageLayout' import UserCard from 'components/UserCard' @@ -51,21 +52,23 @@ const UsersPage = () => { } return ( - -
- {users && users.map((user) =>
{renderUserCard(user)}
)} -
-
+ + +
+ {users && users.map((user) =>
{renderUserCard(user)}
)} +
+
+
) } diff --git a/frontend/src/app/organizations/[organizationKey]/page.tsx b/frontend/src/app/organizations/[organizationKey]/page.tsx index 614585ca1d..f9ac4cbcff 100644 --- a/frontend/src/app/organizations/[organizationKey]/page.tsx +++ b/frontend/src/app/organizations/[organizationKey]/page.tsx @@ -15,6 +15,7 @@ import { GET_ORGANIZATION_DATA } from 'server/queries/organizationQueries' 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() 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 05aa7329d4..7d474d4712 100644 --- a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx +++ b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/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' const RepositoryDetailsPage = () => { const { repositoryKey, organizationKey } = useParams() @@ -106,22 +107,24 @@ const RepositoryDetailsPage = () => { }, ] return ( - + + + ) } export default RepositoryDetailsPage diff --git a/frontend/src/app/organizations/page.tsx b/frontend/src/app/organizations/page.tsx index 754d517045..13b089f1f8 100644 --- a/frontend/src/app/organizations/page.tsx +++ b/frontend/src/app/organizations/page.tsx @@ -3,6 +3,7 @@ import { useSearchPage } from 'hooks/useSearchPage' import { useRouter } from 'next/navigation' import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper' import type { Organization } from 'types/organization' +import PageLayout from 'components/PageLayout' import SearchPageLayout from 'components/SearchPageLayout' import UserCard from 'components/UserCard' @@ -51,21 +52,23 @@ const OrganizationPage = () => { } return ( - -
- {organizations && organizations.map(renderOrganizationCard)} -
-
+ + +
+ {organizations && organizations.map(renderOrganizationCard)} +
+
+
) } diff --git a/frontend/src/app/projects/[projectKey]/page.tsx b/frontend/src/app/projects/[projectKey]/page.tsx index a563a1daca..0a5be14519 100644 --- a/frontend/src/app/projects/[projectKey]/page.tsx +++ b/frontend/src/app/projects/[projectKey]/page.tsx @@ -18,6 +18,7 @@ import { capitalize } from 'utils/capitalize' 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() const [isLoading, setIsLoading] = useState(true) @@ -89,24 +90,26 @@ const ProjectDetailsPage = () => { ] return ( - + + + ) } diff --git a/frontend/src/app/projects/page.tsx b/frontend/src/app/projects/page.tsx index 74081137da..0d47425079 100644 --- a/frontend/src/app/projects/page.tsx +++ b/frontend/src/app/projects/page.tsx @@ -7,6 +7,7 @@ import { level } from 'utils/data' import { sortOptionsProject } from 'utils/sortingOptions' import { getFilteredIcons } from 'utils/utility' import Card from 'components/Card' +import PageLayout from 'components/PageLayout' import SearchPageLayout from 'components/SearchPageLayout' import SortBy from 'components/SortBy' const ProjectsPage = () => { @@ -58,28 +59,30 @@ const ProjectsPage = () => { } return ( - - } - totalPages={totalPages} - > - {projects && projects.filter((project) => project.isActive).map(renderProjectCard)} - + + + } + totalPages={totalPages} + > + {projects && projects.filter((project) => project.isActive).map(renderProjectCard)} + + ) } diff --git a/frontend/src/app/snapshots/[id]/page.tsx b/frontend/src/app/snapshots/[id]/page.tsx index 02bfe38e45..820f739c85 100644 --- a/frontend/src/app/snapshots/[id]/page.tsx +++ b/frontend/src/app/snapshots/[id]/page.tsx @@ -16,6 +16,7 @@ import { getFilteredIcons, handleSocialUrls } from 'utils/utility' import Card from 'components/Card' import ChapterMapWrapper from 'components/ChapterMapWrapper' import LoadingSpinner from 'components/LoadingSpinner' +import PageLayout from 'components/PageLayout' const SnapshotDetailsPage: React.FC = () => { const { id: snapshotKey } = useParams() @@ -109,88 +110,90 @@ const SnapshotDetailsPage: React.FC = () => { } return ( -
-
-
-
-

- {snapshot.title} -

-
-
- - - {formatDate(snapshot.startAt)} - {formatDate(snapshot.endAt)} - + +
+
+
+
+

+ {snapshot.title} +

+
+
+ + + {formatDate(snapshot.startAt)} - {formatDate(snapshot.endAt)} + +
-
- {snapshot.newChapters && snapshot.newChapters.length > 0 && ( -
-

- New Chapters -

-
- -
-
- {snapshot.newChapters.filter((chapter) => chapter.isActive).map(renderChapterCard)} + {snapshot.newChapters && snapshot.newChapters.length > 0 && ( +
+

+ New Chapters +

+
+ +
+
+ {snapshot.newChapters.filter((chapter) => chapter.isActive).map(renderChapterCard)} +
-
- )} - - {snapshot.newProjects && snapshot.newProjects.length > 0 && ( -
-

- New Projects -

-
- {snapshot.newProjects.filter((project) => project.isActive).map(renderProjectCard)} + )} + + {snapshot.newProjects && snapshot.newProjects.length > 0 && ( +
+

+ New Projects +

+
+ {snapshot.newProjects.filter((project) => project.isActive).map(renderProjectCard)} +
-
- )} - - {snapshot.newReleases && snapshot.newReleases.length > 0 && ( -
-

New Releases

-
- {snapshot.newReleases.map((release, index) => ( -
-
-
-
- {release.name} + )} + + {snapshot.newReleases && snapshot.newReleases.length > 0 && ( +
+

New Releases

+
+ {snapshot.newReleases.map((release, index) => ( +
+
+
+
+ {release.name} +
+
+
+ + {release.projectName} + + + {release.tagName} + +
+
+ + Released: {formatDate(release.publishedAt)}
-
-
- - {release.projectName} - - - {release.tagName} - -
-
- - Released: {formatDate(release.publishedAt)}
-
- ))} + ))} +
-
- )} -
+ )} +
+ ) } diff --git a/frontend/src/app/snapshots/page.tsx b/frontend/src/app/snapshots/page.tsx index d9d4bfc548..efd009ae95 100644 --- a/frontend/src/app/snapshots/page.tsx +++ b/frontend/src/app/snapshots/page.tsx @@ -7,6 +7,7 @@ import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper' import { GET_COMMUNITY_SNAPSHOTS } from 'server/queries/snapshotQueries' import type { Snapshot } from 'types/snapshot' import LoadingSpinner from 'components/LoadingSpinner' +import PageLayout from 'components/PageLayout' import SnapshotCard from 'components/SnapshotCard' const SnapshotsPage: React.FC = () => { @@ -62,19 +63,21 @@ const SnapshotsPage: React.FC = () => { } return ( -
-
-
- {!snapshots?.length ? ( -
No Snapshots found
- ) : ( - snapshots.map((snapshot: Snapshot) => ( -
{renderSnapshotCard(snapshot)}
- )) - )} + +
+
+
+ {!snapshots?.length ? ( +
No Snapshots found
+ ) : ( + snapshots.map((snapshot: Snapshot) => ( +
{renderSnapshotCard(snapshot)}
+ )) + )} +
-
+ ) } diff --git a/frontend/src/components/BreadCrumbs.tsx b/frontend/src/components/BreadCrumbs.tsx index a9bce0fda1..35affa7768 100644 --- a/frontend/src/components/BreadCrumbs.tsx +++ b/frontend/src/components/BreadCrumbs.tsx @@ -3,16 +3,20 @@ import { faChevronRight } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Breadcrumbs, BreadcrumbItem } from '@heroui/react' +import _ from 'lodash' import Link from 'next/link' -import { usePathname } from 'next/navigation' -import { capitalize } from 'utils/capitalize' -export default function BreadCrumbs() { - const homeRoute = '/' - const pathname = usePathname() - const segments = pathname.split(homeRoute).filter(Boolean) +export interface BreadCrumbItem { + title: string + path: string +} + +export interface BreadCrumbsProps { + breadcrumbItems: BreadCrumbItem[] +} - if (pathname === homeRoute) return null +export default function BreadCrumbs({ breadcrumbItems }: BreadCrumbsProps) { + if (_.isEmpty(breadcrumbItems)) return null return (
@@ -33,31 +37,25 @@ export default function BreadCrumbs() { }} > - + Home - {segments.map((segment, index) => { - const href = homeRoute + segments.slice(0, index + 1).join(homeRoute) - const label = capitalize(segment).replace(/-/g, ' ') - const isLast = index === segments.length - 1 - + {breadcrumbItems.map((item, index) => { + const isLast = index === breadcrumbItems.length - 1 return ( - + {isLast ? ( - {label} + {item.title} ) : ( - {label} + {item.title} )} diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 4eb1e9f18f..7c041a8339 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -56,7 +56,7 @@ const DetailsCard = ({ userSummary, }: DetailsCardProps) => { return ( -
+
diff --git a/frontend/src/components/PageLayout.tsx b/frontend/src/components/PageLayout.tsx new file mode 100644 index 0000000000..934bdaca55 --- /dev/null +++ b/frontend/src/components/PageLayout.tsx @@ -0,0 +1,44 @@ +import _ from 'lodash' +import { usePathname } from 'next/navigation' +import React from 'react' +import BreadCrumbs, { BreadCrumbItem } from 'components/BreadCrumbs' + +export interface crumbItem { + title: string +} + +export interface PageLayoutProps { + breadcrumbItems?: crumbItem + children: React.ReactNode +} + +function generateBreadcrumbs(pathname: string, excludeLast = false): BreadCrumbItem[] { + let segments = _.compact(_.split(pathname, '/')) + if (excludeLast) { + segments = _.dropRight(segments) + } + + return _.map(segments, (segment, index) => { + const path = '/' + _.join(_.slice(segments, 0, index + 1), '/') + return { + title: _.capitalize(segment), + path, + } + }) +} + +export default function PageLayout({ breadcrumbItems, children }: PageLayoutProps) { + const pathname = usePathname() + const isBreadCrumbItemsEmpty = _.isEmpty(breadcrumbItems) + const autoBreadcrumbs = generateBreadcrumbs(pathname, !isBreadCrumbItemsEmpty) + const allBreadcrumbs = isBreadCrumbItemsEmpty + ? autoBreadcrumbs + : [...autoBreadcrumbs, { title: _.get(breadcrumbItems, 'title', ''), path: pathname }] + + return ( + <> + +
{children}
+ + ) +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 56e45285a8..0c9b493bb1 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -24,6 +24,7 @@ "paths": { "@e2e/*": ["__tests__/e2e/*"], "@unit/*": ["__tests__/unit/*"], + "@testUtils/*": ["__tests__/testUtils/*"], "*": ["./src/*"], "app/*": ["src/app/*"], "components/*": ["src/components/*"],