- {technologies.map((tech) => (
-
-
{tech.section}
-
- {Object.entries(tech.tools).map(([name, details]) => (
- -
-
-
- {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]) => (
+ -
+
+
-
- 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) => (
-
- ))}
+ {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) => (
+
+ ))}
+
-
+
)
}
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.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/*"],