diff --git a/frontend/__tests__/unit/pages/Community.test.tsx b/frontend/__tests__/unit/pages/Community.test.tsx new file mode 100644 index 0000000000..e89f4ee23c --- /dev/null +++ b/frontend/__tests__/unit/pages/Community.test.tsx @@ -0,0 +1,155 @@ +import { render, screen } from '@testing-library/react' +import React from 'react' +import CommunityPage from 'app/community/page' + +jest.mock('react-icons/fa', () => ({ + FaArrowRight: () => , + FaSlack: () => , + FaChevronRight: () => , +})) + +jest.mock('react-icons/fa6', () => ({ + FaLocationDot: () => , + FaFolder: () => , + FaPeopleGroup: () => , + FaBuilding: () => , + FaUsers: () => , + FaHandshakeAngle: () => , +})) + +jest.mock('components/SecondaryCard', () => ({ + __esModule: true, + default: ({ + title, + children, + className, + }: { + title?: React.ReactNode + children: React.ReactNode + className?: string + }) => ( +
+ {title &&
{title}
} + {children} +
+ ), +})) + +jest.mock('components/AnchorTitle', () => ({ + __esModule: true, + default: ({ title }: { title: string }) => {title}, +})) + +jest.mock('wrappers/IconWrapper', () => ({ + IconWrapper: ({ icon: Icon, className }: { icon: React.ElementType; className: string }) => ( +
+ +
+ ), +})) + +jest.mock('utils/communityData', () => ({ + exploreCards: [ + { + title: 'Test Chapter', + description: 'Test Description', + href: '/test-chapter', + icon: () => , + color: 'text-red-500', + }, + ], + engagementWays: [ + { + title: 'Test Engagement', + description: 'Test Engagement Description', + }, + ], + journeySteps: [ + { + label: 'Test Step 1', + description: 'Test Step 1 Description', + }, + { + label: 'Test Step 2', + description: 'Test Step 2 Description', + }, + ], +})) + +describe('CommunityPage', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test('renders the main page title and intro', () => { + render() + + expect(screen.getByRole('heading', { level: 1, name: /OWASP Community/i })).toBeInTheDocument() + expect(screen.getByText(/Explore the vibrant OWASP community/i)).toBeInTheDocument() + expect(screen.getByRole('link', { name: /Chapters/i })).toHaveAttribute('href', '/chapters') + expect(screen.getByRole('link', { name: /Members/i })).toHaveAttribute('href', '/members') + expect(screen.getByRole('link', { name: /Organizations/i })).toHaveAttribute( + 'href', + '/organizations' + ) + }) + + test('renders Explore Resources section correctly', () => { + render() + + expect(screen.getByTestId('anchor-title')).toHaveTextContent('Explore Resources') + expect(screen.getByText('Test Chapter')).toBeInTheDocument() + expect(screen.getByText('Test Description')).toBeInTheDocument() + const link = screen.getByRole('link', { name: /Test Chapter/i }) + expect(link).toHaveAttribute('href', '/test-chapter') + expect(screen.getByTestId('icon-chevron-right')).toBeInTheDocument() + }) + + test('renders Ways to Engage section correctly', () => { + render() + + expect(screen.getByText('Ways to Engage')).toBeInTheDocument() + expect(screen.getByText('Test Engagement')).toBeInTheDocument() + expect(screen.getByText('Test Engagement Description')).toBeInTheDocument() + }) + + test('renders Community Journey section correctly', () => { + render() + + expect(screen.getByText('Your Community Journey')).toBeInTheDocument() + + expect(screen.getAllByText('Test Step 1')).toHaveLength(2) + expect(screen.getAllByText('Test Step 1 Description')).toHaveLength(2) + expect(screen.getAllByText('Test Step 2')).toHaveLength(2) + + expect(screen.getAllByText('1')).toHaveLength(2) + expect(screen.getAllByText('2')).toHaveLength(2) + }) + + test('renders Join the Community section correctly', () => { + render() + + expect(screen.getByText('Join the Community')).toBeInTheDocument() + expect(screen.getByTestId('icon-slack')).toBeInTheDocument() + expect(screen.getByText(/Connect with fellow security professionals/i)).toBeInTheDocument() + + const slackLink = screen.getByRole('link', { + name: /Join OWASP Community Slack workspace/i, + }) + expect(slackLink).toHaveAttribute('href', 'https://owasp.org/slack/invite') + expect(screen.getByTestId('icon-arrow-right')).toBeInTheDocument() + }) + + test('renders Final Call to Action section correctly', () => { + render() + + expect( + screen.getByRole('heading', { level: 2, name: /Ready to Get Involved\?/i }) + ).toBeInTheDocument() + expect(screen.getByText(/Join thousands of security professionals/i)).toBeInTheDocument() + expect(screen.getByRole('link', { name: /contributing to a project/i })).toHaveAttribute( + 'href', + '/contribute' + ) + }) +}) diff --git a/frontend/src/app/community/layout.tsx b/frontend/src/app/community/layout.tsx new file mode 100644 index 0000000000..6d92d0ddc0 --- /dev/null +++ b/frontend/src/app/community/layout.tsx @@ -0,0 +1,8 @@ +import type { ReactNode } from 'react' +import { getStaticMetadata } from 'utils/metaconfig' + +export const metadata = getStaticMetadata('community', '/community') + +export default function CommunityLayout({ children }: { children: ReactNode }) { + return children +} diff --git a/frontend/src/app/community/page.tsx b/frontend/src/app/community/page.tsx new file mode 100644 index 0000000000..b0d18e3ec9 --- /dev/null +++ b/frontend/src/app/community/page.tsx @@ -0,0 +1,160 @@ +'use client' + +import Link from 'next/link' +import { FaArrowRight, FaSlack, FaChevronRight } from 'react-icons/fa' +import { IconWrapper } from 'wrappers/IconWrapper' +import { exploreCards as NAV_SECTIONS, engagementWays, journeySteps } from 'utils/communityData' +import AnchorTitle from 'components/AnchorTitle' +import SecondaryCard from 'components/SecondaryCard' + +export default function CommunityPage() { + return ( +
+
+
+

+ OWASP Community +

+

+ Explore the vibrant OWASP community. Connect with{' '} + + Chapters + + ,{' '} + + Members + + , and{' '} + + Organizations + {' '} + around the world at your fingertips. Discover opportunities to engage, learn, and + contribute. +

+
+ + {/* Explore Community Section */} + + +
+ } + className="overflow-hidden" + > +
+ {NAV_SECTIONS.map((section) => ( + +
+ +
+
+

+ {section.title} + +

+

+ {section.description} +

+
+ + ))} +
+ + + {/* Ways to Engage Section */} + +
+ {engagementWays.map((way) => ( +
+

+ {way.title} +

+

{way.description}

+
+ ))} +
+
+ + {/* Community Journey Section */} + +
+ {/* Desktop view - horizontal */} +
+ {journeySteps.map((step, idx) => ( +
+
+
+ {idx + 1} +
+

{step.label}

+

{step.description}

+
+ {idx < journeySteps.length - 1 && ( +
+ )} +
+ ))} +
+ + {/* Mobile view - vertical */} +
+ {journeySteps.map((step, idx) => ( +
+
+ {idx + 1} +
+
+

{step.label}

+

{step.description}

+
+
+ ))} +
+
+ + + {/* Join the Community Section */} + +
+
+
+

+ Connect with fellow security professionals, ask questions, share ideas, and + collaborate with the global OWASP community on Slack. +

+ + Join Slack + +
+
+ + {/* Final Call to Action */} + +

Ready to Get Involved?

+

+ Join thousands of security professionals making a difference in the OWASP community. + Whether you're just starting out or you're an experienced contributor, + there's a place for you here. Start by{' '} + + contributing to a project + + . +

+
+
+
+ ) +} diff --git a/frontend/src/utils/communityData.ts b/frontend/src/utils/communityData.ts new file mode 100644 index 0000000000..e23bfbe6ef --- /dev/null +++ b/frontend/src/utils/communityData.ts @@ -0,0 +1,83 @@ +import { + FaBuilding, + FaFolder, + FaHandshakeAngle, + FaLocationDot, + FaPeopleGroup, + FaUsers, +} from 'react-icons/fa6' + +export const exploreCards = [ + { + title: 'Chapters', + description: 'Find local OWASP chapters and connect with your community.', + href: '/chapters', + icon: FaLocationDot, + color: 'text-gray-900 dark:text-gray-100', + }, + { + title: 'Projects', + description: 'Explore open-source security projects and contribute.', + href: '/projects', + icon: FaFolder, + color: 'text-gray-900 dark:text-gray-100', + }, + { + title: 'Committees', + description: 'OWASP committees driving security governance.', + href: '/committees', + icon: FaPeopleGroup, + color: 'text-gray-900 dark:text-gray-100', + }, + { + title: 'Organizations', + description: 'Browse OWASP organizations and their work.', + href: '/organizations', + icon: FaBuilding, + color: 'text-gray-900 dark:text-gray-100', + }, + { + title: 'Members', + description: 'Meet the people behind OWASP.', + href: '/members', + icon: FaUsers, + color: 'text-gray-900 dark:text-gray-100', + }, + { + title: 'Contribute', + description: 'Find issues and start contributing today.', + href: '/contribute', + icon: FaHandshakeAngle, + color: 'text-gray-900 dark:text-gray-100', + }, +] + +export const engagementWays = [ + { + title: 'Join Local Chapters', + description: + 'Find and connect with OWASP chapters in your area to participate in local events and meetups.', + }, + { + title: 'Connect with Members', + description: + 'Network with security professionals, developers, and enthusiasts from around the world.', + }, + { + title: 'Contribute to Projects', + description: + 'Share your expertise by contributing to open-source security projects and initiatives.', + }, + { + title: 'Participate in Discussions', + description: + 'Join conversations, share knowledge, and learn from the global cybersecurity community.', + }, +] + +export const journeySteps = [ + { label: 'Discover', description: 'Explore chapters, members, and organizations' }, + { label: 'Connect', description: 'Build relationships within the community' }, + { label: 'Participate', description: 'Join events and contribute to discussions' }, + { label: 'Contribute', description: 'Share your knowledge and expertise' }, +] diff --git a/frontend/src/utils/metadata.ts b/frontend/src/utils/metadata.ts index d174adf440..503c39d48e 100644 --- a/frontend/src/utils/metadata.ts +++ b/frontend/src/utils/metadata.ts @@ -36,6 +36,12 @@ export const METADATA_CONFIG = { pageTitle: 'Contribute', type: 'website', }, + community: { + description: 'Explore the OWASP community: chapters, members, organizations, and snapshots.', + keywords: ['OWASP community', 'chapters', 'members', 'organizations', 'security community'], + pageTitle: 'Community', + type: 'website', + }, snapshots: { description: 'Snapshots of OWASP', keywords: ['OWASP snapshot', 'contribute'],