Skip to content
155 changes: 155 additions & 0 deletions frontend/__tests__/unit/pages/Community.test.tsx
Original file line number Diff line number Diff line change
@@ -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: () => <span data-testid="icon-arrow-right" />,
FaSlack: () => <span data-testid="icon-slack" />,
FaChevronRight: () => <span data-testid="icon-chevron-right" />,
}))

jest.mock('react-icons/fa6', () => ({
FaLocationDot: () => <span data-testid="icon-location" />,
FaFolder: () => <span data-testid="icon-folder" />,
FaPeopleGroup: () => <span data-testid="icon-people" />,
FaBuilding: () => <span data-testid="icon-building" />,
FaUsers: () => <span data-testid="icon-users" />,
FaHandshakeAngle: () => <span data-testid="icon-handshake" />,
}))

jest.mock('components/SecondaryCard', () => ({
__esModule: true,
default: ({
title,
children,
className,
}: {
title?: React.ReactNode
children: React.ReactNode
className?: string
}) => (
<div data-testid="secondary-card" className={className}>
{title && <div data-testid="card-title">{title}</div>}
{children}
</div>
),
}))

jest.mock('components/AnchorTitle', () => ({
__esModule: true,
default: ({ title }: { title: string }) => <span data-testid="anchor-title">{title}</span>,
}))

jest.mock('wrappers/IconWrapper', () => ({
IconWrapper: ({ icon: Icon, className }: { icon: React.ElementType; className: string }) => (
<div data-testid="icon-wrapper" className={className}>
<Icon />
</div>
),
}))

jest.mock('utils/communityData', () => ({
exploreCards: [
{
title: 'Test Chapter',
description: 'Test Description',
href: '/test-chapter',
icon: () => <span data-testid="mock-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(<CommunityPage />)

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(<CommunityPage />)

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(<CommunityPage />)

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(<CommunityPage />)

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(<CommunityPage />)

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(<CommunityPage />)

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'
)
})
})
8 changes: 8 additions & 0 deletions frontend/src/app/community/layout.tsx
Original file line number Diff line number Diff line change
@@ -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
}
160 changes: 160 additions & 0 deletions frontend/src/app/community/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen w-full px-8 pt-24 pb-8 text-gray-600 dark:bg-[#212529] dark:text-gray-300">
<div className="mx-auto max-w-6xl">
<div className="mb-12 text-center">
<h1 className="mb-4 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
OWASP Community
</h1>
<p className="mx-auto mb-8 max-w-2xl text-lg text-gray-600 dark:text-gray-400">
Explore the vibrant OWASP community. Connect with{' '}
<Link href="/chapters" className="text-blue-500 hover:underline">
Chapters
</Link>
,{' '}
<Link href="/members" className="text-blue-500 hover:underline">
Members
</Link>
, and{' '}
<Link href="/organizations" className="text-blue-500 hover:underline">
Organizations
</Link>{' '}
around the world at your fingertips. Discover opportunities to engage, learn, and
contribute.
</p>
</div>

{/* Explore Community Section */}
<SecondaryCard
title={
<div className="flex items-center gap-2">
<AnchorTitle title="Explore Resources" />
</div>
}
className="overflow-hidden"
>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{NAV_SECTIONS.map((section) => (
<Link
key={section.title}
href={section.href}
className="group flex items-start gap-4 rounded-lg bg-gray-200 p-5 transition-all duration-300 hover:bg-blue-100 dark:bg-gray-700 dark:hover:bg-gray-600"
>
<div className="flex-shrink-0 pt-1">
<IconWrapper icon={section.icon} className={`h-6 w-6 ${section.color}`} />
</div>
<div className="min-w-0 flex-1">
<h3 className="flex items-center gap-2 text-lg font-semibold text-gray-800 dark:text-gray-200">
{section.title}
<FaChevronRight className="h-3 w-3 transform transition-transform group-hover:translate-x-1" />
</h3>
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
{section.description}
</p>
</div>
</Link>
))}
</div>
</SecondaryCard>

{/* Ways to Engage Section */}
<SecondaryCard title="Ways to Engage">
<div className="grid gap-6 md:grid-cols-2">
{engagementWays.map((way) => (
<div key={way.title} className="rounded-lg bg-gray-200 p-5 dark:bg-gray-700">
<h3 className="mb-2 text-lg font-semibold text-gray-900 dark:text-gray-100">
{way.title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">{way.description}</p>
</div>
))}
</div>
</SecondaryCard>

{/* Community Journey Section */}
<SecondaryCard title="Your Community Journey">
<div className="relative">
{/* Desktop view - horizontal */}
<div className="hidden items-center justify-between md:flex">
{journeySteps.map((step, idx) => (
<div key={step.label} className="flex flex-1 items-center">
<div className="flex flex-col items-center text-center">
<div className="mb-3 flex h-16 w-16 items-center justify-center rounded-full bg-blue-500 text-xl font-bold text-white">
{idx + 1}
</div>
<h3 className="mb-1 text-lg font-semibold">{step.label}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">{step.description}</p>
</div>
{idx < journeySteps.length - 1 && (
<div className="mx-4 flex-1 border-t-2 border-dashed border-gray-400" />
)}
</div>
))}
</div>

{/* Mobile view - vertical */}
<div className="flex flex-col gap-6 md:hidden">
{journeySteps.map((step, idx) => (
<div key={step.label} className="flex items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-blue-500 text-lg font-bold text-white">
{idx + 1}
</div>
<div className="flex-1">
<h3 className="mb-1 text-lg font-semibold">{step.label}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">{step.description}</p>
</div>
</div>
))}
</div>
</div>
</SecondaryCard>

{/* Join the Community Section */}
<SecondaryCard title="Join the Community">
<div className="mx-auto max-w-2xl text-center">
<div className="mb-6 inline-flex rounded-full bg-blue-50 p-4 dark:bg-blue-900/20">
<FaSlack className="h-8 w-8 text-blue-500" aria-hidden="true" />
</div>
<p className="mb-8 text-lg text-gray-600 dark:text-gray-400">
Connect with fellow security professionals, ask questions, share ideas, and
collaborate with the global OWASP community on Slack.
</p>
<a
href="https://owasp.org/slack/invite"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-lg bg-blue-500 px-8 py-3 text-lg font-semibold text-white transition-colors hover:bg-blue-600 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none dark:focus:ring-offset-gray-800"
aria-label="Join OWASP Community Slack workspace"
>
Join Slack
<FaArrowRight className="h-5 w-5" aria-hidden="true" />
</a>
</div>
</SecondaryCard>

{/* Final Call to Action */}
<SecondaryCard className="text-center">
<h2 className="mb-4 text-3xl font-bold">Ready to Get Involved?</h2>
<p className="mb-8 text-lg text-gray-600 dark:text-gray-400">
Join thousands of security professionals making a difference in the OWASP community.
Whether you&apos;re just starting out or you&apos;re an experienced contributor,
there&apos;s a place for you here. Start by{' '}
<Link href="/contribute" className="text-blue-500 hover:underline">
contributing to a project
</Link>
.
</p>
</SecondaryCard>
</div>
</div>
)
}
Loading