From b40862aae6c42c65422c9a9235fe1f2d6fb7aeec Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Tue, 22 Jul 2025 12:39:31 +0530 Subject: [PATCH 1/5] Add leaders presentation block in chapters and projects --- backend/apps/owasp/graphql/nodes/common.py | 5 ++ frontend/src/app/about/page.tsx | 54 ++------------ .../src/app/projects/[projectKey]/page.tsx | 1 + frontend/src/components/CardDetailsPage.tsx | 12 ++++ frontend/src/components/LeadersListBlock.tsx | 71 +++++++++++++++++++ frontend/src/server/queries/projectQueries.ts | 1 + frontend/src/types/card.ts | 1 + frontend/src/types/leaders.ts | 5 ++ frontend/src/types/project.ts | 1 + 9 files changed, 101 insertions(+), 50 deletions(-) create mode 100644 frontend/src/components/LeadersListBlock.tsx diff --git a/backend/apps/owasp/graphql/nodes/common.py b/backend/apps/owasp/graphql/nodes/common.py index cf479349df..389e75200f 100644 --- a/backend/apps/owasp/graphql/nodes/common.py +++ b/backend/apps/owasp/graphql/nodes/common.py @@ -9,6 +9,11 @@ class GenericEntityNode: """Base node class for OWASP entities with common fields and resolvers.""" + @strawberry.field + def leaders_logins(self) -> list[str]: + """Resolve leaders logins.""" + return [leader.login for leader in self.leaders.all()] + @strawberry.field def leaders(self) -> list[str]: """Resolve leaders.""" diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx index fc9bb3b774..57d4fa2d3c 100644 --- a/frontend/src/app/about/page.tsx +++ b/frontend/src/app/about/page.tsx @@ -14,24 +14,20 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Tooltip } from '@heroui/tooltip' import Image from 'next/image' import Link from 'next/link' -import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' -import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper' import { ErrorDisplay, handleAppError } from 'app/global-error' import { GET_PROJECT_METADATA, GET_TOP_CONTRIBUTORS } from 'server/queries/projectQueries' -import { GET_LEADER_DATA } from 'server/queries/userQueries' 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 LeadersListBlock from 'components/LeadersListBlock' import LoadingSpinner from 'components/LoadingSpinner' import Markdown from 'components/MarkdownWrapper' import SecondaryCard from 'components/SecondaryCard' import TopContributorsList from 'components/TopContributorsList' -import UserCard from 'components/UserCard' const leaders = { arkid15r: 'CCSP, CISSP, CSSLP', @@ -112,15 +108,9 @@ const About = () => { ))} - }> -
- {Object.keys(leaders).map((username) => ( -
- -
- ))} -
-
+ {leaders && ( + + )} {topContributors && ( { ) } -const LeaderData = ({ username }: { username: string }) => { - const { data, loading, error } = useQuery(GET_LEADER_DATA, { - variables: { key: username }, - }) - const router = useRouter() - - if (loading) return

Loading {username}...

- if (error) return

Error loading {username}'s data

- - const user = data?.user - - if (!user) { - return

No data available for {username}

- } - - const handleButtonClick = (user: User) => { - router.push(`/members/${user.login}`) - } - - return ( - , - label: 'View Profile', - onclick: () => handleButtonClick(user), - }} - className="h-64 w-40 bg-inherit" - company={user.company} - description={leaders[user.login]} - location={user.location} - name={user.name || username} - /> - ) -} - export default About diff --git a/frontend/src/app/projects/[projectKey]/page.tsx b/frontend/src/app/projects/[projectKey]/page.tsx index 86993dae2d..db7d3521b4 100644 --- a/frontend/src/app/projects/[projectKey]/page.tsx +++ b/frontend/src/app/projects/[projectKey]/page.tsx @@ -103,6 +103,7 @@ const ProjectDetailsPage = () => { stats={projectStats} summary={project.summary} title={project.name} + leadersLogins={project.leadersLogins} topContributors={topContributors} topics={project.topics} type="project" diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index c8691b9621..05a8c6f490 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -11,6 +11,7 @@ import { import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Link from 'next/link' import type { DetailsCardProps } from 'types/card' +import { LeadersListBlockProps } from 'types/leaders' import { capitalize } from 'utils/capitalize' import { IS_PROJECT_HEALTH_ENABLED } from 'utils/credentials' import { getSocialIcon } from 'utils/urlIconMappings' @@ -19,6 +20,7 @@ import ChapterMapWrapper from 'components/ChapterMapWrapper' import HealthMetrics from 'components/HealthMetrics' import InfoBlock from 'components/InfoBlock' import LeadersList from 'components/LeadersList' +import LeadersListBlock from 'components/LeadersListBlock' import Milestones from 'components/Milestones' import RecentIssues from 'components/RecentIssues' import RecentPullRequests from 'components/RecentPullRequests' @@ -49,6 +51,7 @@ const DetailsCard = ({ stats, summary, title, + leadersLogins, topContributors, topics, type, @@ -197,6 +200,15 @@ const DetailsCard = ({ )} )} + {leadersLogins && ( + { + acc[login] = '' + return acc + }, {} as LeadersListBlockProps)} + /> + )} {topContributors && ( { + const LeaderData = ({ username }: { username: string }) => { + const { data, loading, error } = useQuery(GET_LEADER_DATA, { + variables: { key: username }, + }) + const router = useRouter() + + if (loading) return

Loading {username}...

+ if (error) return

Error loading {username}'s data

+ + const user = data?.user + + if (!user) { + return

No data available for {username}

+ } + + const handleButtonClick = (user: User) => { + router.push(`/members/${user.login}`) + } + + return ( + , + label: 'View Profile', + onclick: () => handleButtonClick(user), + }} + className="h-64 w-40 bg-inherit" + company={user.company} + description={leaders[user.login]} + location={user.location} + name={user.name || username} + /> + ) + } + + return ( + }> +
+ {Object.keys(leaders).map((username) => ( +
+ +
+ ))} +
+
+ ) +} + +export default LeadersListBlock diff --git a/frontend/src/server/queries/projectQueries.ts b/frontend/src/server/queries/projectQueries.ts index 8cab6d98fa..6d4467063a 100644 --- a/frontend/src/server/queries/projectQueries.ts +++ b/frontend/src/server/queries/projectQueries.ts @@ -10,6 +10,7 @@ export const GET_PROJECT_DATA = gql` key languages leaders + leadersLogins level name healthMetrics(limit: 30) { diff --git a/frontend/src/types/card.ts b/frontend/src/types/card.ts index df1bf9b989..86bdd02cba 100644 --- a/frontend/src/types/card.ts +++ b/frontend/src/types/card.ts @@ -53,6 +53,7 @@ export interface DetailsCardProps { stats?: Stats[] summary?: string title?: string + leadersLogins?: string[] topContributors?: Contributor[] topics?: string[] type: string diff --git a/frontend/src/types/leaders.ts b/frontend/src/types/leaders.ts index ca543e7220..c02d39686a 100644 --- a/frontend/src/types/leaders.ts +++ b/frontend/src/types/leaders.ts @@ -1,3 +1,8 @@ export type LeadersListProps = { leaders: string } + +// Maps username to certification strings (e.g., { arkid15r: 'CCSP, CISSP, CSSLP' }) +export type LeadersListBlockProps = { + [key: string]: string +} diff --git a/frontend/src/types/project.ts b/frontend/src/types/project.ts index d39e52e4de..63d90415ec 100644 --- a/frontend/src/types/project.ts +++ b/frontend/src/types/project.ts @@ -25,6 +25,7 @@ export type Project = { key: string languages: string[] leaders: string[] + leadersLogins: string[] level: string name: string openIssuesCount?: number From 1d9e438d9179ed49fce75ed6d6fd13c27fc561b9 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 24 Jul 2025 16:12:44 +0530 Subject: [PATCH 2/5] fix sonar issues --- frontend/src/components/CardDetailsPage.tsx | 4 +- frontend/src/components/LeadersListBlock.tsx | 80 +++++++++++--------- frontend/src/types/project.ts | 2 +- 3 files changed, 47 insertions(+), 39 deletions(-) diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 05a8c6f490..28343a8d3f 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -7,6 +7,7 @@ import { faTags, faUsers, faRectangleList, + faArrowUpRightFromSquare, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Link from 'next/link' @@ -200,8 +201,9 @@ const DetailsCard = ({ )} )} - {leadersLogins && ( + {leadersLogins && leadersLogins.length > 0 && ( { acc[login] = '' diff --git a/frontend/src/components/LeadersListBlock.tsx b/frontend/src/components/LeadersListBlock.tsx index b070420ec1..65ec2b2bc1 100644 --- a/frontend/src/components/LeadersListBlock.tsx +++ b/frontend/src/components/LeadersListBlock.tsx @@ -19,48 +19,12 @@ const LeadersListBlock = ({ label?: string icon?: IconProp }) => { - const LeaderData = ({ username }: { username: string }) => { - const { data, loading, error } = useQuery(GET_LEADER_DATA, { - variables: { key: username }, - }) - const router = useRouter() - - if (loading) return

Loading {username}...

- if (error) return

Error loading {username}'s data

- - const user = data?.user - - if (!user) { - return

No data available for {username}

- } - - const handleButtonClick = (user: User) => { - router.push(`/members/${user.login}`) - } - - return ( - , - label: 'View Profile', - onclick: () => handleButtonClick(user), - }} - className="h-64 w-40 bg-inherit" - company={user.company} - description={leaders[user.login]} - location={user.location} - name={user.name || username} - /> - ) - } - return ( }>
{Object.keys(leaders).map((username) => (
- +
))}
@@ -68,4 +32,46 @@ const LeadersListBlock = ({ ) } +const LeaderData = ({ + username, + leaders, +}: { + username: string + leaders: LeadersListBlockProps +}) => { + const { data, loading, error } = useQuery(GET_LEADER_DATA, { + variables: { key: username }, + }) + const router = useRouter() + + if (loading) return

Loading {username}...

+ if (error) return

Error loading {username}'s data

+ + const user = data?.user + + if (!user) { + return

No data available for {username}

+ } + + const handleButtonClick = (user: User) => { + router.push(`/members/${user.login}`) + } + + return ( + , + label: 'View Profile', + onclick: () => handleButtonClick(user), + }} + className="h-64 w-40 bg-inherit" + company={user.company} + description={leaders[user.login]} + location={user.location} + name={user.name || username} + /> + ) +} + export default LeadersListBlock diff --git a/frontend/src/types/project.ts b/frontend/src/types/project.ts index 63d90415ec..fb5eb70f50 100644 --- a/frontend/src/types/project.ts +++ b/frontend/src/types/project.ts @@ -25,7 +25,7 @@ export type Project = { key: string languages: string[] leaders: string[] - leadersLogins: string[] + leadersLogins?: string[] level: string name: string openIssuesCount?: number From 9146e2c0559a6aecbfca91665f78c3890464cba7 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 24 Jul 2025 16:38:44 +0530 Subject: [PATCH 3/5] Add leaders presentation block in chapters --- frontend/src/app/chapters/[chapterKey]/page.tsx | 1 + frontend/src/server/queries/chapterQueries.ts | 1 + frontend/src/types/chapter.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/frontend/src/app/chapters/[chapterKey]/page.tsx b/frontend/src/app/chapters/[chapterKey]/page.tsx index d6a7f1a16c..893d9c495f 100644 --- a/frontend/src/app/chapters/[chapterKey]/page.tsx +++ b/frontend/src/app/chapters/[chapterKey]/page.tsx @@ -68,6 +68,7 @@ export default function ChapterDetailsPage() { socialLinks={chapter.relatedUrls} summary={chapter.summary} title={chapter.name} + leadersLogins={chapter.leadersLogins} topContributors={topContributors} type="chapter" /> diff --git a/frontend/src/server/queries/chapterQueries.ts b/frontend/src/server/queries/chapterQueries.ts index 2482c06beb..3b4add0c93 100644 --- a/frontend/src/server/queries/chapterQueries.ts +++ b/frontend/src/server/queries/chapterQueries.ts @@ -16,6 +16,7 @@ export const GET_CHAPTER_DATA = gql` summary updatedAt url + leadersLogins } topContributors(chapter: $key) { avatarUrl diff --git a/frontend/src/types/chapter.ts b/frontend/src/types/chapter.ts index c6ee7bbe70..fc77b68da2 100644 --- a/frontend/src/types/chapter.ts +++ b/frontend/src/types/chapter.ts @@ -13,6 +13,7 @@ export type Chapter = { relatedUrls: string[] suggestedLocation: string summary: string + leadersLogins?: string[] topContributors: Contributor[] updatedAt: number url: string From f4408f0de0dc8b4362e39c8ae94a744f2da952f7 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Fri, 25 Jul 2025 10:02:55 +0530 Subject: [PATCH 4/5] Add tests for LeadersListBlock --- .../e2e/pages/ChapterDetails.spec.ts | 4 + .../e2e/pages/ProjectDetails.spec.ts | 4 + .../unit/components/LeadersListBlock.test.tsx | 113 ++++++++++++++++++ .../unit/data/mockChapterDetailsData.ts | 1 + .../unit/data/mockProjectDetailsData.ts | 1 + 5 files changed, 123 insertions(+) create mode 100644 frontend/__tests__/unit/components/LeadersListBlock.test.tsx diff --git a/frontend/__tests__/e2e/pages/ChapterDetails.spec.ts b/frontend/__tests__/e2e/pages/ChapterDetails.spec.ts index e4bb465ea9..72a0dceffb 100644 --- a/frontend/__tests__/e2e/pages/ChapterDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/ChapterDetails.spec.ts @@ -55,4 +55,8 @@ test.describe('Chapter Details Page', () => { await page.getByRole('button', { name: 'Show less' }).click() await expect(page.getByRole('button', { name: 'Show more' })).toBeVisible() }) + + test('should have a leaders list block', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Leaders' })).toBeVisible() + }) }) diff --git a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts index af8de6b13d..52e6d7313c 100644 --- a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts @@ -123,4 +123,8 @@ test.describe('Project Details Page', () => { await expect(page.getByText('Forks Trend')).toBeVisible() await expect(page.getByText('Days Since Last Commit and Release')).toBeVisible() }) + + test('should have a leaders list block', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Leaders' })).toBeVisible() + }) }) diff --git a/frontend/__tests__/unit/components/LeadersListBlock.test.tsx b/frontend/__tests__/unit/components/LeadersListBlock.test.tsx new file mode 100644 index 0000000000..4cf0688476 --- /dev/null +++ b/frontend/__tests__/unit/components/LeadersListBlock.test.tsx @@ -0,0 +1,113 @@ +import { MockedProvider } from '@apollo/client/testing' +import { render, screen, fireEvent } from '@testing-library/react' +import { useRouter } from 'next/navigation' +import { GET_LEADER_DATA } from 'server/queries/userQueries' +import LeadersListBlock from 'components/LeadersListBlock' + +jest.mock('next/navigation', () => ({ + useRouter: jest.fn(), +})) + +const mockLeaders = { + leader1: 'Role 1', + leader2: 'Role 2', +} + +const mocks = [ + { + request: { + query: GET_LEADER_DATA, + variables: { key: 'leader1' }, + }, + result: { + data: { + user: { + login: 'leader1', + avatarUrl: 'https://avatars.githubusercontent.com/u/1', + company: 'Company 1', + location: 'Location 1', + name: 'Leader One', + }, + }, + }, + }, + { + request: { + query: GET_LEADER_DATA, + variables: { key: 'leader2' }, + }, + result: { + data: { + user: { + login: 'leader2', + avatarUrl: 'https://avatars.githubusercontent.com/u/2', + company: 'Company 2', + location: 'Location 2', + name: 'Leader Two', + }, + }, + }, + }, +] + +describe('LeadersListBlock', () => { + const push = jest.fn() + beforeEach(() => { + ;(useRouter as jest.Mock).mockReturnValue({ push }) + }) + + test('renders loading state initially', () => { + render( + + + + ) + + expect(screen.getByText('Loading leader1...')).toBeInTheDocument() + expect(screen.getByText('Loading leader2...')).toBeInTheDocument() + }) + + test('renders user cards on successful data fetch', async () => { + render( + + + + ) + + expect(await screen.findByText('Leader One')).toBeInTheDocument() + expect(await screen.findByText('Leader Two')).toBeInTheDocument() + }) + + test('navigates to the correct user profile on button click', async () => { + render( + + + + ) + + const viewProfileButton = await screen.findAllByText('View Profile') + fireEvent.click(viewProfileButton[0]) + + expect(push).toHaveBeenCalledWith('/members/leader1') + }) + + test('renders an error message if the query fails', async () => { + const errorMocks = [ + { + request: { + query: GET_LEADER_DATA, + variables: { key: 'leader1' }, + }, + error: new Error('An error occurred'), + }, + ] + + render( + + + + ) + + expect(await screen.findByText("Error loading leader1's data")).toBeInTheDocument() + }) +}) diff --git a/frontend/__tests__/unit/data/mockChapterDetailsData.ts b/frontend/__tests__/unit/data/mockChapterDetailsData.ts index f5963686d3..6c97890b8b 100644 --- a/frontend/__tests__/unit/data/mockChapterDetailsData.ts +++ b/frontend/__tests__/unit/data/mockChapterDetailsData.ts @@ -21,6 +21,7 @@ export const mockChapterDetailsData = { }, establishedYear: 2020, key: 'test-chapter', + leadersLogins: ['leader1', 'leader2'], }, topContributors: Array.from({ length: 15 }, (_, i) => ({ avatarUrl: `https://avatars.githubusercontent.com/avatar${i + 1}.jpg`, diff --git a/frontend/__tests__/unit/data/mockProjectDetailsData.ts b/frontend/__tests__/unit/data/mockProjectDetailsData.ts index 9d9c195022..6eea638302 100644 --- a/frontend/__tests__/unit/data/mockProjectDetailsData.ts +++ b/frontend/__tests__/unit/data/mockProjectDetailsData.ts @@ -20,6 +20,7 @@ export const mockProjectDetailsData = { key: 'example-project', languages: ['Python', 'GraphQL', 'JavaScript'], leaders: ['alice', 'bob'], + leadersLogins: ['alice', 'bob'], level: 'Lab', name: 'Test Project', recentIssues: [ From a0edae1d7dd4a30c1e008b65bf705fe191c685a0 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 31 Jul 2025 15:06:15 +0530 Subject: [PATCH 5/5] increase width --- frontend/src/components/LeadersListBlock.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/LeadersListBlock.tsx b/frontend/src/components/LeadersListBlock.tsx index 65ec2b2bc1..4b08827715 100644 --- a/frontend/src/components/LeadersListBlock.tsx +++ b/frontend/src/components/LeadersListBlock.tsx @@ -65,7 +65,7 @@ const LeaderData = ({ label: 'View Profile', onclick: () => handleButtonClick(user), }} - className="h-64 w-40 bg-inherit" + className="w-42 h-64 bg-inherit" company={user.company} description={leaders[user.login]} location={user.location}