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

const titleElement = screen.getByText('Test')
expect(titleElement).toHaveClass('flex', 'items-center', 'text-2xl', 'font-semibold')
expect(titleElement).toHaveAttribute('id', 'anchor-title')
expect(titleElement).toHaveAttribute('data-anchor-title', 'true')
})

it('renders FontAwesome link icon', () => {
Expand Down Expand Up @@ -372,7 +372,7 @@ describe('AnchorTitle Component', () => {
render(<AnchorTitle title="Heading Test" />)

const titleElement = screen.getByText('Heading Test')
expect(titleElement).toHaveAttribute('id', 'anchor-title')
expect(titleElement).toHaveAttribute('data-anchor-title', 'true')
expect(titleElement).toHaveClass('text-2xl', 'font-semibold')

const container = document.getElementById('heading-test')
Expand Down 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()
})
})
15 changes: 4 additions & 11 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 All @@ -50,7 +43,7 @@ const AnchorTitle: React.FC<AnchorTitleProps> = ({ title }) => {
return (
<div id={id} className="relative">
<div className="group relative flex items-center">
<div className="flex items-center text-2xl font-semibold" id="anchor-title">
<div className="flex items-center text-2xl font-semibold" data-anchor-title="true">
{title}
</div>
<a
Expand Down
17 changes: 10 additions & 7 deletions frontend/src/components/CardDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +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 { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'
Comment thread
divyanshu-vr marked this conversation as resolved.
import type { ExtendedSession } from 'types/auth'
Comment thread
divyanshu-vr marked this conversation as resolved.
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 @@ -96,9 +93,15 @@ const DetailsCard = ({
</button>
)}
{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 ?? 0} />
</a>
)}
</div>
{!isActive && (
Expand Down
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('[data-anchor-title]')
const headingHeight = (anchorElement instanceof HTMLElement) ? anchorElement.offsetHeight : 0
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
}
}
}