Skip to content

Commit a2521f3

Browse files
shining-bluemoon-11kasyaarkid15r
authored
adding feature of anchor links (OWASP#960)
* adding feature of anchor links * fix update * update asked changes * fix checks * remove unwanted file * fix checks * fix * update fix * resolve conversation * fix checks * fix required updates * fix * fix e2e test * Fix icon/title alignments. Resolve conflicts * Fix tests * Fix tests * Update code --------- Co-authored-by: Kate Golovanova <[email protected]> Co-authored-by: Arkadii Yakovets <[email protected]> Co-authored-by: Arkadii Yakovets <[email protected]>
1 parent 51d7bb5 commit a2521f3

16 files changed

+262
-57
lines changed

frontend/__tests__/e2e/pages/Home.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ test.describe('Home Page', () => {
3737
await page.getByRole('link', { name: 'Chapter 1' }).click()
3838
await expect(page).toHaveURL('chapters/chapter_1')
3939
})
40+
4041
test('should have new projects', async ({ page }) => {
4142
await expect(page.getByRole('heading', { name: 'New Projects' })).toBeVisible()
4243
await expect(page.getByRole('link', { name: 'Project 1', exact: true })).toBeVisible()

frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ test.describe('Repository Details Page', () => {
5252
await expect(page.getByText('web', { exact: true })).toBeVisible()
5353
await expect(page.getByText('security', { exact: true })).toBeVisible()
5454
})
55+
5556
test('should have top contributors', async ({ page }) => {
5657
await expect(page.getByRole('heading', { name: 'Top Contributors' })).toBeVisible()
5758
await expect(page.getByRole('img', { name: 'Contributor 1' })).toBeVisible()

frontend/__tests__/unit/pages/About.test.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,18 +197,15 @@ describe('About Component', () => {
197197
expect(screen.queryByText('Contributor 10')).not.toBeInTheDocument()
198198
})
199199

200-
const contributorsSection = screen
201-
.getByRole('heading', { name: /Top Contributors/i })
202-
.closest('div')
203-
const showMoreButton = within(contributorsSection!).getByRole('button', { name: /Show more/i })
200+
const showMoreButton = screen.getByRole('button', { name: /Show more/i })
204201
fireEvent.click(showMoreButton)
205202

206203
await waitFor(() => {
207204
expect(screen.getByText('Contributor 7')).toBeInTheDocument()
208205
expect(screen.getByText('Contributor 8')).toBeInTheDocument()
209206
})
210207

211-
const showLessButton = within(contributorsSection!).getByRole('button', { name: /Show less/i })
208+
const showLessButton = screen.getByRole('button', { name: /Show less/i })
212209
fireEvent.click(showLessButton)
213210

214211
await waitFor(() => {

frontend/__tests__/unit/pages/ProjectDetails.test.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,18 +103,15 @@ describe('ProjectDetailsPage', () => {
103103
expect(screen.queryByText('Contributor 10')).not.toBeInTheDocument()
104104
})
105105

106-
const contributorsSection = screen
107-
.getByRole('heading', { name: /Top Contributors/i })
108-
.closest('div')
109-
const showMoreButton = within(contributorsSection!).getByRole('button', { name: /Show more/i })
106+
const showMoreButton = screen.getByRole('button', { name: /Show more/i })
110107
fireEvent.click(showMoreButton)
111108

112109
await waitFor(() => {
113110
expect(screen.getByText('Contributor 7')).toBeInTheDocument()
114111
expect(screen.getByText('Contributor 8')).toBeInTheDocument()
115112
})
116113

117-
const showLessButton = within(contributorsSection!).getByRole('button', { name: /Show less/i })
114+
const showLessButton = screen.getByRole('button', { name: /Show less/i })
118115
fireEvent.click(showLessButton)
119116

120117
await waitFor(() => {

frontend/__tests__/unit/pages/RepositoryDetails.test.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useQuery } from '@apollo/client'
22
import { addToast } from '@heroui/toast'
3-
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
3+
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
44
import { mockRepositoryData } from '@unit/data/mockRepositoryData'
55
import { render } from 'wrappers/testUtil'
66
import RepositoryDetailsPage from 'app/organizations/[organizationKey]/repositories/[repositoryKey]/page'
@@ -105,18 +105,15 @@ describe('RepositoryDetailsPage', () => {
105105
expect(screen.queryByText('Contributor 10')).not.toBeInTheDocument()
106106
})
107107

108-
const contributorsSection = screen
109-
.getByRole('heading', { name: /Top Contributors/i })
110-
.closest('div')
111-
const showMoreButton = within(contributorsSection!).getByRole('button', { name: /Show more/i })
108+
const showMoreButton = screen.getByRole('button', { name: /Show more/i })
112109
fireEvent.click(showMoreButton)
113110

114111
await waitFor(() => {
115112
expect(screen.getByText('Contributor 7')).toBeInTheDocument()
116113
expect(screen.getByText('Contributor 8')).toBeInTheDocument()
117114
})
118115

119-
const showLessButton = within(contributorsSection!).getByRole('button', { name: /Show less/i })
116+
const showLessButton = screen.getByRole('button', { name: /Show less/i })
120117
fireEvent.click(showLessButton)
121118

122119
await waitFor(() => {

frontend/src/app/globals.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,24 @@ a {
294294
opacity: 0.6;
295295
}
296296

297+
html {
298+
scroll-behavior: smooth;
299+
}
300+
301+
.inherit-color {
302+
color: inherit;
303+
}
304+
305+
.custom-icon {
306+
display: inline-flex;
307+
align-items: center;
308+
vertical-align: middle;
309+
}
310+
311+
section {
312+
scroll-margin-top: 100px;
313+
}
314+
297315
/* Dropdown container */
298316
.dropdown {
299317
@apply relative;

frontend/src/app/page.tsx

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { EventType } from 'types/event'
2727
import { MainPageData } from 'types/home'
2828
import { capitalize } from 'utils/capitalize'
2929
import { formatDate, formatDateRange } from 'utils/dateFormatter'
30+
import AnchorTitle from 'components/AnchorTitle'
3031
import AnimatedCounter from 'components/AnimatedCounter'
3132
import ChapterMapWrapper from 'components/ChapterMapWrapper'
3233
import LeadersList from 'components/LeadersList'
@@ -147,7 +148,19 @@ export default function Home() {
147148
/>
148149
</div>
149150
</div>
150-
<SecondaryCard icon={faCalendarAlt} title="Upcoming Events" className="overflow-hidden">
151+
<SecondaryCard
152+
icon={faCalendarAlt}
153+
title={
154+
<div className="flex items-center gap-2">
155+
<AnchorTitle
156+
href="#upcoming-events"
157+
title="Upcoming Events"
158+
className="flex items-center leading-none"
159+
/>
160+
</div>
161+
}
162+
className="overflow-hidden"
163+
>
151164
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
152165
{data?.upcomingEvents?.map((event: EventType, index: number) => (
153166
<div key={`card-${event.name}`} className="overflow-hidden">
@@ -185,7 +198,19 @@ export default function Home() {
185198
</div>
186199
</SecondaryCard>
187200
<div className="grid gap-4 md:grid-cols-2">
188-
<SecondaryCard icon={faMapMarkerAlt} title="New Chapters" className="overflow-hidden">
201+
<SecondaryCard
202+
icon={faMapMarkerAlt}
203+
title={
204+
<div className="flex items-center gap-2">
205+
<AnchorTitle
206+
href="#new-chapters"
207+
title="New Chapters"
208+
className="flex items-center leading-none"
209+
/>
210+
</div>
211+
}
212+
className="overflow-hidden"
213+
>
189214
<div className="space-y-4">
190215
{data?.recentChapters?.map((chapter) => (
191216
<div key={chapter.key} className="rounded-lg bg-gray-200 p-4 dark:bg-gray-700">
@@ -219,7 +244,19 @@ export default function Home() {
219244
))}
220245
</div>
221246
</SecondaryCard>
222-
<SecondaryCard icon={faFolder} title="New Projects" className="overflow-hidden">
247+
<SecondaryCard
248+
icon={faFolder}
249+
title={
250+
<div className="flex items-center gap-2">
251+
<AnchorTitle
252+
href="#new-projects"
253+
title="New Projects"
254+
className="flex items-center leading-none"
255+
/>
256+
</div>
257+
}
258+
className="overflow-hidden"
259+
>
223260
<div className="space-y-4">
224261
{data?.recentProjects?.map((project) => (
225262
<div key={project.key} className="rounded-lg bg-gray-200 p-4 dark:bg-gray-700">
@@ -254,10 +291,18 @@ export default function Home() {
254291
</SecondaryCard>
255292
</div>
256293
<div className="mb-20">
257-
<h2 className="mb-4 text-2xl font-semibold">
258-
<FontAwesomeIcon icon={faGlobe} className="mr-2 h-5 w-5" />
259-
Chapters Worldwide
260-
</h2>
294+
<div className="mb-4 flex items-center gap-2">
295+
<FontAwesomeIcon
296+
icon={faGlobe}
297+
className="h-5 w-5"
298+
style={{ verticalAlign: 'middle' }}
299+
/>
300+
<AnchorTitle
301+
href="#chapters-worldwide"
302+
title="Chapters Worldwide"
303+
className="flex items-center leading-none"
304+
/>
305+
</div>
261306
<ChapterMapWrapper
262307
geoLocData={geoLocData}
263308
showLocal={false}
@@ -281,7 +326,19 @@ export default function Home() {
281326
<RecentPullRequests data={data?.recentPullRequests} />
282327
</div>
283328
<RecentReleases data={data?.recentReleases} />
284-
<SecondaryCard icon={faNewspaper} title="News & Opinions" className="overflow-hidden">
329+
<SecondaryCard
330+
icon={faNewspaper}
331+
title={
332+
<div className="flex items-center gap-2">
333+
<AnchorTitle
334+
href="#news-&-opinions"
335+
title="News & Opinions"
336+
className="flex items-center leading-none"
337+
/>
338+
</div>
339+
}
340+
className="overflow-hidden"
341+
>
285342
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-2">
286343
{data?.recentPosts.map((post) => (
287344
<div
@@ -314,12 +371,14 @@ export default function Home() {
314371
</SecondaryCard>
315372
<div className="grid gap-6 md:grid-cols-4">
316373
{counterData.map((stat, index) => (
317-
<SecondaryCard key={index} className="text-center">
318-
<div className="mb-2 text-3xl font-bold text-blue-400">
319-
<AnimatedCounter end={parseInt(stat.value)} duration={2} />+
320-
</div>
321-
<div className="text-gray-600 dark:text-gray-400">{stat.label}</div>
322-
</SecondaryCard>
374+
<div key={index}>
375+
<SecondaryCard className="text-center">
376+
<div className="mb-2 text-3xl font-bold text-blue-400">
377+
<AnimatedCounter end={parseInt(stat.value)} duration={2} />+
378+
</div>
379+
<div className="text-gray-600 dark:text-gray-400">{stat.label}</div>
380+
</SecondaryCard>
381+
</div>
323382
))}
324383
</div>
325384

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { faLink } from '@fortawesome/free-solid-svg-icons'
2+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3+
import React, { useEffect, useCallback } from 'react'
4+
5+
interface AnchorTitleProps {
6+
href: string
7+
title: string
8+
className?: string
9+
}
10+
11+
const AnchorTitle: React.FC<AnchorTitleProps> = ({ href, title }) => {
12+
const id = href.replace('#', '') // TODO(arkid15r): refactor get href from title automatically.
13+
14+
const scrollToElement = useCallback(() => {
15+
const element = document.getElementById(id)
16+
if (element) {
17+
const headingHeight =
18+
(element.querySelector('div#anchor-title') as HTMLElement)?.offsetHeight || 0
19+
const yOffset = -headingHeight - 50
20+
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset
21+
window.scrollTo({ top: y, behavior: 'smooth' })
22+
}
23+
}, [id])
24+
25+
const handleClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
26+
event.preventDefault()
27+
scrollToElement()
28+
window.history.pushState(null, '', href)
29+
}
30+
31+
useEffect(() => {
32+
const hash = window.location.hash.replace('#', '')
33+
if (hash === id) {
34+
requestAnimationFrame(() => scrollToElement())
35+
}
36+
}, [id, scrollToElement])
37+
38+
useEffect(() => {
39+
const handlePopState = () => {
40+
const hash = window.location.hash.replace('#', '')
41+
if (hash === id) {
42+
requestAnimationFrame(() => scrollToElement())
43+
}
44+
}
45+
window.addEventListener('popstate', handlePopState)
46+
return () => window.removeEventListener('popstate', handlePopState)
47+
}, [id, scrollToElement])
48+
49+
return (
50+
<div id={id} className="relative">
51+
<div className="group relative flex items-center">
52+
<div className="flex items-center text-2xl font-semibold" id="anchor-title">
53+
{title}
54+
</div>
55+
<a
56+
href={href}
57+
className="inherit-color ml-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
58+
onClick={handleClick}
59+
>
60+
<FontAwesomeIcon icon={faLink} className="custom-icon h-7 w-5" />
61+
</a>
62+
</div>
63+
</div>
64+
)
65+
}
66+
67+
export default AnchorTitle

0 commit comments

Comments
 (0)