|
1 | | -'use client' |
2 | | -import { useQuery } from '@apollo/client' |
3 | | -import { |
4 | | - faCircleCheck, |
5 | | - faClock, |
6 | | - faUserGear, |
7 | | - faMapSigns, |
8 | | - faScroll, |
9 | | - faUsers, |
10 | | - faTools, |
11 | | - faArrowUpRightFromSquare, |
12 | | -} from '@fortawesome/free-solid-svg-icons' |
13 | | -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
14 | | -import { Tooltip } from '@heroui/tooltip' |
15 | | -import upperFirst from 'lodash/upperFirst' |
16 | | -import Image from 'next/image' |
17 | | -import Link from 'next/link' |
18 | | -import { useRouter } from 'next/navigation' |
19 | | -import { useEffect, useState } from 'react' |
20 | | -import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper' |
21 | | -import { ErrorDisplay, handleAppError } from 'app/global-error' |
22 | | -import { GET_PROJECT_METADATA, GET_TOP_CONTRIBUTORS } from 'server/queries/projectQueries' |
23 | | -import { GET_LEADER_DATA } from 'server/queries/userQueries' |
24 | | -import type { Contributor } from 'types/contributor' |
25 | | -import type { Project } from 'types/project' |
26 | | -import type { User } from 'types/user' |
27 | | -import { aboutText, technologies } from 'utils/aboutData' |
28 | | -import AnchorTitle from 'components/AnchorTitle' |
29 | | -import AnimatedCounter from 'components/AnimatedCounter' |
30 | | -import LoadingSpinner from 'components/LoadingSpinner' |
31 | | -import Markdown from 'components/MarkdownWrapper' |
32 | | -import SecondaryCard from 'components/SecondaryCard' |
33 | | -import TopContributorsList from 'components/TopContributorsList' |
34 | | -import UserCard from 'components/UserCard' |
35 | | - |
36 | | -const leaders = { |
37 | | - arkid15r: 'CCSP, CISSP, CSSLP', |
38 | | - kasya: 'CC', |
39 | | - mamicidal: 'CISSP', |
40 | | -} |
41 | | -const projectKey = 'nest' |
42 | | - |
43 | | -const About = () => { |
44 | | - const { data: projectMetadataResponse, error: projectMetadataRequestError } = useQuery( |
45 | | - GET_PROJECT_METADATA, |
46 | | - { |
47 | | - variables: { key: projectKey }, |
48 | | - } |
49 | | - ) |
50 | | - |
51 | | - const { data: topContributorsResponse, error: topContributorsRequestError } = useQuery( |
52 | | - GET_TOP_CONTRIBUTORS, |
53 | | - { |
54 | | - variables: { |
55 | | - excludedUsernames: Object.keys(leaders), |
56 | | - hasFullName: true, |
57 | | - key: projectKey, |
58 | | - limit: 24, |
59 | | - }, |
60 | | - } |
61 | | - ) |
62 | | - |
63 | | - const [projectMetadata, setProjectMetadata] = useState<Project | null>(null) |
64 | | - const [topContributors, setTopContributors] = useState<Contributor[]>([]) |
65 | | - |
66 | | - useEffect(() => { |
67 | | - if (projectMetadataResponse?.project) { |
68 | | - setProjectMetadata(projectMetadataResponse.project) |
69 | | - } |
70 | | - |
71 | | - if (projectMetadataRequestError) { |
72 | | - handleAppError(projectMetadataRequestError) |
73 | | - } |
74 | | - }, [projectMetadataResponse, projectMetadataRequestError]) |
75 | | - |
76 | | - useEffect(() => { |
77 | | - if (topContributorsResponse?.topContributors) { |
78 | | - setTopContributors(topContributorsResponse.topContributors) |
79 | | - } |
80 | | - |
81 | | - if (topContributorsRequestError) { |
82 | | - handleAppError(topContributorsRequestError) |
83 | | - } |
84 | | - }, [topContributorsResponse, topContributorsRequestError]) |
85 | | - |
86 | | - const isLoading = |
87 | | - !projectMetadataResponse || |
88 | | - !topContributorsResponse || |
89 | | - (projectMetadataRequestError && !projectMetadata) || |
90 | | - (topContributorsRequestError && !topContributors) |
91 | | - |
92 | | - if (isLoading) { |
93 | | - return <LoadingSpinner /> |
94 | | - } |
95 | | - |
96 | | - if (!projectMetadata || !topContributors) { |
97 | | - return ( |
98 | | - <ErrorDisplay |
99 | | - statusCode={404} |
100 | | - title="Data not found" |
101 | | - message="Sorry, the page you're looking for doesn't exist" |
102 | | - /> |
103 | | - ) |
104 | | - } |
105 | | - |
| 1 | +export default function AboutPage() { |
106 | 2 | return ( |
107 | | - <div className="min-h-screen p-8 text-gray-600 dark:bg-[#212529] dark:text-gray-300"> |
108 | | - <div className="mx-auto max-w-6xl"> |
109 | | - <h1 className="mb-6 mt-4 text-4xl font-bold">About</h1> |
110 | | - <SecondaryCard icon={faScroll} title={<AnchorTitle title="History" />}> |
111 | | - {aboutText.map((text) => ( |
112 | | - <div key={text} className="mb-4"> |
113 | | - <div key={text}> |
114 | | - <Markdown content={text} /> |
115 | | - </div> |
116 | | - </div> |
117 | | - ))} |
118 | | - </SecondaryCard> |
119 | | - |
120 | | - <SecondaryCard icon={faArrowUpRightFromSquare} title={<AnchorTitle title="Leaders" />}> |
121 | | - <div className="flex w-full flex-col items-center justify-around overflow-hidden md:flex-row"> |
122 | | - {Object.keys(leaders).map((username) => ( |
123 | | - <div key={username}> |
124 | | - <LeaderData username={username} /> |
125 | | - </div> |
126 | | - ))} |
127 | | - </div> |
128 | | - </SecondaryCard> |
129 | | - |
130 | | - {topContributors && ( |
131 | | - <TopContributorsList |
132 | | - contributors={topContributors} |
133 | | - icon={faUsers} |
134 | | - maxInitialDisplay={12} |
135 | | - /> |
136 | | - )} |
137 | | - |
138 | | - <SecondaryCard icon={faTools} title={<AnchorTitle title="Technologies & Tools" />}> |
139 | | - <div className="w-full"> |
140 | | - <div className="grid w-full grid-cols-1 justify-center sm:grid-cols-2 lg:grid-cols-4 lg:pl-8"> |
141 | | - {technologies.map((tech) => ( |
142 | | - <div key={tech.section} className="mb-2"> |
143 | | - <h3 className="mb-3 font-semibold text-blue-400">{tech.section}</h3> |
144 | | - <ul className="space-y-3"> |
145 | | - {Object.entries(tech.tools).map(([name, details]) => ( |
146 | | - <li key={name} className="flex flex-row items-center gap-2"> |
147 | | - <Image |
148 | | - alt={`${name} icon`} |
149 | | - className="inline-block align-middle grayscale" |
150 | | - height={24} |
151 | | - src={details.icon} |
152 | | - width={24} |
153 | | - /> |
154 | | - <Link |
155 | | - href={details.url} |
156 | | - className="text-gray-600 hover:underline dark:text-gray-300" |
157 | | - target="_blank" |
158 | | - rel="noopener noreferrer" |
159 | | - > |
160 | | - {upperFirst(name)} |
161 | | - </Link> |
162 | | - </li> |
163 | | - ))} |
164 | | - </ul> |
165 | | - </div> |
166 | | - ))} |
167 | | - </div> |
168 | | - </div> |
169 | | - </SecondaryCard> |
170 | | - |
171 | | - {projectMetadata.recentMilestones.length > 0 && ( |
172 | | - <SecondaryCard icon={faMapSigns} title={<AnchorTitle title="Roadmap" />}> |
173 | | - <div className="grid gap-4"> |
174 | | - {[...projectMetadata.recentMilestones] |
175 | | - .filter((milestone) => milestone.state !== 'closed') |
176 | | - .sort((a, b) => (a.title > b.title ? 1 : -1)) |
177 | | - .map((milestone, index) => ( |
178 | | - <div |
179 | | - key={milestone.url || milestone.title || index} |
180 | | - className="flex items-center gap-4 overflow-hidden rounded-lg bg-gray-200 p-6 dark:bg-gray-700" |
181 | | - > |
182 | | - <div className="flex-1"> |
183 | | - <Link |
184 | | - href={milestone.url} |
185 | | - target="_blank" |
186 | | - rel="noopener noreferrer" |
187 | | - className="inline-block" |
188 | | - > |
189 | | - <h3 className="mb-2 text-xl font-semibold text-blue-400"> |
190 | | - {milestone.title} |
191 | | - <Tooltip |
192 | | - closeDelay={100} |
193 | | - content={ |
194 | | - milestone.progress === 100 |
195 | | - ? 'Completed' |
196 | | - : milestone.progress > 0 |
197 | | - ? 'In Progress' |
198 | | - : 'Not Started' |
199 | | - } |
200 | | - id={`tooltip-state-${index}`} |
201 | | - delay={100} |
202 | | - placement="top" |
203 | | - showArrow |
204 | | - > |
205 | | - <span className="ml-4 inline-block text-gray-400"> |
206 | | - <FontAwesomeIcon |
207 | | - icon={ |
208 | | - milestone.progress === 100 |
209 | | - ? faCircleCheck |
210 | | - : milestone.progress > 0 |
211 | | - ? faUserGear |
212 | | - : faClock |
213 | | - } |
214 | | - /> |
215 | | - </span> |
216 | | - </Tooltip> |
217 | | - </h3> |
218 | | - </Link> |
219 | | - <p className="text-gray-600 dark:text-gray-300">{milestone.body}</p> |
220 | | - </div> |
221 | | - </div> |
222 | | - ))} |
223 | | - </div> |
224 | | - </SecondaryCard> |
225 | | - )} |
226 | | - |
227 | | - <div className="grid gap-6 md:grid-cols-4"> |
228 | | - {[ |
229 | | - { label: 'Forks', value: projectMetadata.forksCount }, |
230 | | - { label: 'Stars', value: projectMetadata.starsCount }, |
231 | | - { label: 'Contributors', value: projectMetadata.contributorsCount }, |
232 | | - { label: 'Open Issues', value: projectMetadata.issuesCount }, |
233 | | - ].map((stat, index) => ( |
234 | | - <div key={index}> |
235 | | - <SecondaryCard className="text-center"> |
236 | | - <div className="mb-2 text-3xl font-bold text-blue-400"> |
237 | | - <AnimatedCounter end={Math.floor(stat.value / 10) * 10} duration={2} />+ |
238 | | - </div> |
239 | | - <div className="text-gray-600 dark:text-gray-300">{stat.label}</div> |
240 | | - </SecondaryCard> |
241 | | - </div> |
242 | | - ))} |
243 | | - </div> |
244 | | - </div> |
| 3 | + <div style={{ padding: "2rem", fontFamily: "sans-serif" }}> |
| 4 | + <h1>About OWASP Nest</h1> |
| 5 | + <p> |
| 6 | + OWASP Nest is a platform to discover, engage, and contribute to OWASP |
| 7 | + projects and initiatives. It serves as a central hub for the community. |
| 8 | + </p> |
245 | 9 | </div> |
246 | | - ) |
247 | | -} |
248 | | - |
249 | | -const LeaderData = ({ username }: { username: string }) => { |
250 | | - const { data, loading, error } = useQuery(GET_LEADER_DATA, { |
251 | | - variables: { key: username }, |
252 | | - }) |
253 | | - const router = useRouter() |
254 | | - |
255 | | - if (loading) return <p>Loading {username}...</p> |
256 | | - if (error) return <p>Error loading {username}'s data</p> |
257 | | - |
258 | | - const user = data?.user |
259 | | - |
260 | | - if (!user) { |
261 | | - return <p>No data available for {username}</p> |
262 | | - } |
263 | | - |
264 | | - const handleButtonClick = (user: User) => { |
265 | | - router.push(`/members/${user.login}`) |
266 | | - } |
267 | | - |
268 | | - return ( |
269 | | - <UserCard |
270 | | - avatar={user.avatarUrl} |
271 | | - button={{ |
272 | | - icon: <FontAwesomeIconWrapper icon="fa-solid fa-right-to-bracket" />, |
273 | | - label: 'View Profile', |
274 | | - onclick: () => handleButtonClick(user), |
275 | | - }} |
276 | | - className="h-64 w-40 bg-inherit" |
277 | | - company={user.company} |
278 | | - description={leaders[user.login]} |
279 | | - location={user.location} |
280 | | - name={user.name || username} |
281 | | - /> |
282 | | - ) |
| 10 | + ); |
283 | 11 | } |
284 | | - |
285 | | -export default About |
0 commit comments