Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 React from 'react'
import { getStaticMetadata } from 'utils/metaconfig'

export const metadata = getStaticMetadata('community', '/community')

export default function CommunityLayout({ children }: { children: React.ReactNode }) {
return children
}
251 changes: 251 additions & 0 deletions frontend/src/app/community/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
'use client'

import { useQuery } from '@apollo/client/react'
import millify from 'millify'
import Link from 'next/link'
import { FaMapMarkerAlt } from 'react-icons/fa'
import {
FaBuilding,
FaChevronRight,
FaFolder,
FaHandshakeAngle,
FaPeopleGroup,
FaUsers,
FaTag,
} from 'react-icons/fa6'
import { HiUserGroup } from 'react-icons/hi'
import { IconWrapper } from 'wrappers/IconWrapper'
import { GetCommunityPageDataDocument } from 'types/__generated__/communityQueries.generated'
import AnchorTitle from 'components/AnchorTitle'
import ContributorsList from 'components/ContributorsList'
import LoadingSpinner from 'components/LoadingSpinner'
import Release from 'components/Release'
import SecondaryCard from 'components/SecondaryCard'

const NAV_SECTIONS = [
{
title: 'Chapters',
description: 'Find local OWASP chapters and connect with your community.',
href: '/chapters',
icon: FaMapMarkerAlt,
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',
},
]

const CommunityPage = () => {
const { data, loading, error } = useQuery(GetCommunityPageDataDocument, {
variables: {
distinct: true,
},
})

if (loading) return <LoadingSpinner />
if (error)
return <div className="p-8 text-center text-red-500">Error loading community data.</div>

const statsOverview = data?.statsOverview

const stats = [
{
icon: FaUsers,
value: millify(statsOverview?.contributorsStats || 0),
label: 'Contributors',
color: 'text-gray-900 dark:text-gray-100',
bg: 'bg-gray-100 dark:bg-gray-800',
},
{
icon: FaMapMarkerAlt,
value: millify(statsOverview?.activeChaptersStats || 0),
label: 'Active Chapters',
color: 'text-gray-900 dark:text-gray-100',
bg: 'bg-gray-100 dark:bg-gray-800',
},
{
icon: FaFolder,
value: millify(statsOverview?.activeProjectsStats || 0),
label: 'Active Projects',
color: 'text-gray-900 dark:text-gray-100',
bg: 'bg-gray-100 dark:bg-gray-800',
},
]

return (
<div className="mt-2 min-h-screen p-8 text-gray-600 dark:bg-[#212529] dark:text-gray-300">
<div className="mx-auto max-w-6xl">
{/* Hero */}
<div className="pt-2 pb-8 text-center">
<div className="flex flex-col items-center gap-2">
<h1 className="text-3xl font-medium tracking-tighter sm:text-5xl md:text-6xl">
Community Hub
</h1>
<p className="text-muted-foreground max-w-[700px] md:text-xl">
Your gateway into the OWASP community — explore chapters, projects, and people.
</p>
</div>
</div>

{/* Explore the Community (Resources) */}
<div className="mb-12">
<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-50 dark:bg-gray-700 dark:hover:bg-blue-950"
>
<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 group-hover:text-blue-500 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>
</div>

{/* Stats Grid */}
<div className="mb-8 grid grid-cols-1 gap-6 sm:grid-cols-3">
{stats.map((stat) => (
<div
key={stat.label}
className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
>
<div className="flex items-center gap-4">
<div
className={`flex h-12 w-12 items-center justify-center rounded-full ${stat.bg} ${stat.color}`}
>
<stat.icon className="h-6 w-6" />
</div>
<div>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
{stat.label}
</p>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white">{stat.value}</h3>
</div>
</div>
</div>
))}
</div>

<div className="grid grid-cols-1 gap-6 md:grid-cols-3 lg:grid-cols-3">
{/* Main Content: Recent Releases (Renamed from Activity Feed) */}
<div className="md:col-span-2">
<SecondaryCard
icon={FaTag}
title={<AnchorTitle title="Recent Releases" />}
className="!mb-0 h-full"
>
<div className="mb-4">
<p className="text-sm text-gray-500 dark:text-gray-400">
Latest updates from across the OWASP ecosystem.
</p>
</div>
<div className="flex flex-col gap-4">
{data?.recentReleases?.map((release, index) => (
<Release key={release.id} release={release} index={index} showAvatar={true} />
))}
</div>
<div className="mt-4 text-center">
<Link
href="/projects"
className="font-medium text-blue-500 hover:text-blue-600 hover:underline"
>
View all recent releases &rarr;
</Link>
</div>
</SecondaryCard>
</div>

{/* Sidebar: Top Contributors (Spotlight Removed) */}
<div className="md:col-span-1">
<ContributorsList
contributors={data?.topContributors || []}
icon={HiUserGroup}
maxInitialDisplay={10}
title="Top Contributors"
getUrl={(login: string) => `/members/${login}`}
className="!mb-0 h-full"
gridClassName="grid-cols-1 gap-4"
/>
</div>
</div>

{/* Quick Links */}
<div className="border-t border-gray-200 pt-5 text-center dark:border-gray-800">
<p className="mb-1 text-gray-500">Want to get more involved?</p>
<div className="flex justify-center gap-2 text-sm font-medium text-gray-500">
<Link
href="https://github.com/owasp"
className="hover:text-blue-500 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
Github
</Link>
<span>&middot;</span>
<Link href="/members" className="hover:text-blue-500 hover:underline">
Members
</Link>
<span>&middot;</span>
<Link href="/organizations" className="hover:text-blue-500 hover:underline">
Organizations
</Link>
</div>
</div>
</div>
</div>
)
}

export default CommunityPage
14 changes: 13 additions & 1 deletion frontend/src/components/ContributorsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Image from 'next/image'
import Link from 'next/link'
import { useState } from 'react'
import type { IconType } from 'react-icons'
import { twMerge } from 'tailwind-merge'
import type { Contributor } from 'types/contributor'
import AnchorTitle from 'components/AnchorTitle'
import SecondaryCard from 'components/SecondaryCard'
Expand All @@ -14,6 +15,8 @@ interface ContributorsListProps {
maxInitialDisplay?: number
icon?: IconType
getUrl: (login: string) => string
className?: string
gridClassName?: string
}

const ContributorsList = ({
Expand All @@ -22,6 +25,8 @@ const ContributorsList = ({
maxInitialDisplay = 12,
icon,
getUrl,
className,
gridClassName,
}: ContributorsListProps) => {
const [showAllContributors, setShowAllContributors] = useState(false)

Expand All @@ -43,8 +48,15 @@ const ContributorsList = ({
<AnchorTitle title={label} />
</div>
}
className={className}
>
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-3 lg:grid-cols-4">
<div
className={twMerge(
'grid gap-4',
!gridClassName && 'sm:grid-cols-1 md:grid-cols-3 lg:grid-cols-4',
gridClassName
)}
>
{displayContributors.map((item) => (
<div
key={item.login}
Expand Down
38 changes: 38 additions & 0 deletions frontend/src/server/queries/communityQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { gql } from '@apollo/client'

export const GET_COMMUNITY_PAGE_DATA = gql`
query GetCommunityPageData(
$distinct: Boolean
$recentReleasesLimit: Int = 6
$topContributorsLimit: Int = 10
) {
topContributors(hasFullName: true, limit: $topContributorsLimit) {
id
avatarUrl
login
name
}
recentReleases(limit: $recentReleasesLimit, distinct: $distinct) {
id
author {
id
avatarUrl
login
name
}
name
organizationName
publishedAt
repositoryName
tagName
url
}
statsOverview {
activeChaptersStats
activeProjectsStats
contributorsStats
countriesStats
slackWorkspaceStats
}
}
`
10 changes: 7 additions & 3 deletions frontend/src/server/queries/homeQueries.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { gql } from '@apollo/client'

export const GET_MAIN_PAGE_DATA = gql`
query GetMainPageData($distinct: Boolean) {
query GetMainPageData(
$distinct: Boolean
$recentReleasesLimit: Int = 5
$topContributorsLimit: Int = 40
) {
recentProjects(limit: 3) {
id
createdAt
Expand All @@ -28,7 +32,7 @@ export const GET_MAIN_PAGE_DATA = gql`
name
suggestedLocation
}
topContributors(hasFullName: true, limit: 40) {
topContributors(hasFullName: true, limit: $topContributorsLimit) {
id
avatarUrl
login
Expand Down Expand Up @@ -62,7 +66,7 @@ export const GET_MAIN_PAGE_DATA = gql`
title
url
}
recentReleases(limit: 5, distinct: $distinct) {
recentReleases(limit: $recentReleasesLimit, distinct: $distinct) {
id
author {
id
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/types/__generated__/communityQueries.generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading