Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 3 additions & 3 deletions frontend/__tests__/unit/components/AnchorTitle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ describe('AnchorTitle Component', () => {
fireEvent.click(link)

expect(mockScrollTo).toHaveBeenCalledWith({
top: 520,
top: 20,
behavior: 'smooth',
})

Expand Down Expand Up @@ -562,8 +562,8 @@ describe('AnchorTitle Component', () => {
fireEvent.click(link)
const secondCall = (mockScrollTo.mock.calls[1][0] as unknown as { top: number }).top

expect(firstCall).not.toBe(secondCall)
expect(Math.abs(firstCall - secondCall)).toBe(30)
expect(firstCall).toBe(secondCall)
expect(Math.abs(firstCall - secondCall)).toBe(0)

mockScrollTo.mockRestore()
mockGetElementById.mockRestore()
Expand Down
72 changes: 70 additions & 2 deletions frontend/__tests__/unit/components/MetricsScoreCircle.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react'
import { render, screen, fireEvent } from '@testing-library/react'
import React from 'react'
import '@testing-library/jest-dom'
import MetricsScoreCircle from 'components/MetricsScoreCircle'
Expand Down Expand Up @@ -218,4 +218,72 @@ describe('MetricsScoreCircle', () => {
'Current Project Health Score'
)
})
})

// Test Click handling functionality
describe('click handling', () => {
it('calls onClick when provided and component is clicked', () => {
const mockOnClick = jest.fn()
render(<MetricsScoreCircle score={75} onClick={mockOnClick} />)

// Find the clickable circle element by looking for the element with rounded-full class
// that also has the group class (this is the actual clickable div)
const circleElement = screen.getByText('75').closest('.group')
expect(circleElement).toBeInTheDocument()

if (circleElement) {
fireEvent.click(circleElement)
}
expect(mockOnClick).toHaveBeenCalledTimes(1)
})

it('does not call onClick when not provided', () => {
render(<MetricsScoreCircle score={75} />)

const circleElement = screen.getByText('75').closest('.group')
if (circleElement) {
fireEvent.click(circleElement)
}
// Should not throw any errors - test passes if no exception is thrown
expect(circleElement).toBeInTheDocument()
})

it('adds cursor-pointer class when onClick is provided', () => {
const mockOnClick = jest.fn()
const { container } = render(<MetricsScoreCircle score={75} onClick={mockOnClick} />)

const circleElement = container.querySelector('[class*="cursor-pointer"]')
expect(circleElement).toBeInTheDocument()
})

it('does not add cursor-pointer class when onClick is not provided', () => {
const { container } = render(<MetricsScoreCircle score={75} />)

const circleElement = container.querySelector('[class*="cursor-pointer"]')
expect(circleElement).not.toBeInTheDocument()
})
})

// Test Maintains existing functionality with onClick
it('maintains all existing functionality when onClick is provided', () => {
const mockOnClick = jest.fn()
const { container } = render(<MetricsScoreCircle score={25} onClick={mockOnClick} />)

// Should still have red styling
expect(container.querySelector('[class*="bg-red"]')).toBeInTheDocument()

// Should still have pulse animation
expect(container.querySelector('[class*="animate-pulse"]')).toBeInTheDocument()

// Should still display correct score
expect(screen.getByText('25')).toBeInTheDocument()

// Should still have tooltip
expect(screen.getByTestId('tooltip-wrapper')).toHaveAttribute(
'data-content',
'Current Project Health Score'
)

// Should still have hover effects
expect(container.querySelector('[class*="hover:"]')).toBeInTheDocument()
})
})
13 changes: 3 additions & 10 deletions frontend/src/components/AnchorTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { faLink } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React, { useEffect, useCallback } from 'react'
import { scrollToAnchor, scrollToAnchorWithHistory } from 'utils/scrollToAnchor'
import slugify from 'utils/slugify'

interface AnchorTitleProps {
Expand All @@ -13,20 +14,12 @@ const AnchorTitle: React.FC<AnchorTitleProps> = ({ title }) => {
const href = `#${id}`

const scrollToElement = useCallback(() => {
const element = document.getElementById(id)
if (element) {
const headingHeight =
(element.querySelector('div#anchor-title') as HTMLElement)?.offsetHeight || 0
const yOffset = -headingHeight - 50
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset
window.scrollTo({ top: y, behavior: 'smooth' })
}
scrollToAnchor(id)
}, [id])

const handleClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
event.preventDefault()
scrollToElement()
window.history.pushState(null, '', href)
scrollToAnchorWithHistory(id)
}

useEffect(() => {
Expand Down
18 changes: 13 additions & 5 deletions frontend/src/components/CardDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import upperFirst from 'lodash/upperFirst'
import Link from 'next/link'
import type { DetailsCardProps } from 'types/card'
import { IS_PROJECT_HEALTH_ENABLED } from 'utils/credentials'
import { scrollToAnchor } from 'utils/scrollToAnchor'
import { getSocialIcon } from 'utils/urlIconMappings'
import AnchorTitle from 'components/AnchorTitle'
import ChapterMapWrapper from 'components/ChapterMapWrapper'
Expand Down Expand Up @@ -62,9 +62,17 @@ const DetailsCard = ({
<div className="flex w-full items-center justify-between">
<h1 className="text-4xl font-bold">{title}</h1>
{IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && (
<Link href="#issues-trend">
<MetricsScoreCircle score={healthMetricsData[0].score} />
</Link>
<a
Comment thread
kasya marked this conversation as resolved.
Outdated
href="#issues-trend"
onClick={(e) => {
e.preventDefault()
scrollToAnchor('issues-trend')
}}
>
<MetricsScoreCircle
score={healthMetricsData[0].score}
/>
Comment thread
divyanshu-vr marked this conversation as resolved.
Outdated
</a>
)}
</div>
{!isActive && (
Expand Down Expand Up @@ -250,4 +258,4 @@ const SocialLinks = ({ urls }) => {
</div>
</div>
)
}
}
19 changes: 17 additions & 2 deletions frontend/src/components/MetricsScoreCircle.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
import { Tooltip } from '@heroui/tooltip'
import { FC } from 'react'

const MetricsScoreCircle: FC<{ score: number }> = ({ score }) => {
interface MetricsScoreCircleProps {
score: number
onClick?: () => void
}

const MetricsScoreCircle: FC<MetricsScoreCircleProps> = ({ score, onClick }) => {
// Base colours with reduced opacity so colours remain but are less contrasting
let scoreStyle = 'bg-green-400/80 text-green-900/90'
if (score < 50) {
scoreStyle = 'bg-red-400/80 text-red-900/90'
} else if (score < 75) {
scoreStyle = 'bg-yellow-400/80 text-yellow-900/90'
}
const handleClick = () => {
if (onClick) {
onClick()
}
}

return (
<Tooltip content={'Current Project Health Score'} placement="top">
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
className={`group relative flex h-14 w-14 flex-col items-center justify-center rounded-full shadow-md transition-all duration-300 hover:scale-105 hover:shadow-lg ${scoreStyle}`}
className={`group relative flex h-14 w-14 flex-col items-center justify-center rounded-full shadow-md transition-all duration-300 hover:scale-105 hover:shadow-lg ${scoreStyle} ${
onClick ? 'cursor-pointer' : ''
}`}
onClick={handleClick}
Comment thread
kasya marked this conversation as resolved.
Outdated
>
<div className="absolute inset-0 rounded-full bg-gradient-to-br from-white/20 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"></div>
<div className="relative z-10 flex flex-col items-center text-center">
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/utils/scrollToAnchor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export function scrollToAnchor(targetId: string, additionalOffset = 80): void {
try {
const element = document.getElementById(targetId)

if (!element) {
return
}
const anchorElement = element.querySelector('div#anchor-title')
const headingHeight = (anchorElement instanceof HTMLElement) ? anchorElement.offsetHeight : 0
Comment thread
divyanshu-vr marked this conversation as resolved.
Outdated
const yOffset = -headingHeight - additionalOffset

// Use modern window.scrollY instead of deprecated pageYOffset
const y = element.getBoundingClientRect().top + window.scrollY + yOffset
window.scrollTo({
top: y,
behavior: 'smooth'
})
} catch {
// Silently handle scroll errors
}
}

export const scrollToAnchorWithHistory = (targetId: string, updateHistory = true): void => {
scrollToAnchor(targetId)

if (updateHistory) {
try {
const href = `#${targetId}`
window.history.pushState(null, '', href)
} catch {
// Silently handle history update errors
}
}
}