Skip to content
3 changes: 2 additions & 1 deletion frontend/__tests__/e2e/pages/ProjectDetails.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ test.describe('Project Details Page', () => {
})

test('should have project repositories', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Repositories' })).toBeVisible()
const repositoriesSection = page.locator('section#repositories')
await expect(repositoriesSection.getByRole('heading', { name: 'Repositories' })).toBeVisible()
await expect(page.getByText('Repo One')).toBeVisible()
await expect(page.getByText('Stars95')).toBeVisible()
await expect(page.getByText('Forks12')).toBeVisible()
Expand Down
22 changes: 14 additions & 8 deletions frontend/__tests__/unit/pages/ProjectDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQuery } from '@apollo/client'
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
import { act, screen, waitFor, within } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import { mockProjectDetailsData } from '@unit/data/mockProjectDetailsData'
import { toast } from 'hooks/useToast'
import { ProjectDetailsPage } from 'pages'
Expand Down Expand Up @@ -91,24 +92,29 @@ describe('ProjectDetailsPage', () => {

test('toggles contributors list when show more/less is clicked', async () => {
render(<ProjectDetailsPage />)

await waitFor(() => {
expect(screen.getByText('Contributor 6')).toBeInTheDocument()
expect(screen.queryByText('Contributor 7')).not.toBeInTheDocument()
})

const contributorsSection = screen
.getByRole('heading', { name: /Top Contributors/i })
.closest('div')
const showMoreButton = within(contributorsSection!).getByRole('button', { name: /Show more/i })
fireEvent.click(showMoreButton)
const heading = screen.getByRole('heading', { name: /Top Contributors/i })
expect(heading).toBeInTheDocument()

const contributorsSection = heading.closest('div')
expect(contributorsSection).not.toBeNull()
if (!contributorsSection) return

const showMoreButton = within(contributorsSection).getByRole('button', { name: /Show more/i })
await userEvent.click(showMoreButton)

await waitFor(() => {
expect(screen.getByText('Contributor 7')).toBeInTheDocument()
expect(screen.getByText('Contributor 8')).toBeInTheDocument()
})

const showLessButton = within(contributorsSection!).getByRole('button', { name: /Show less/i })
fireEvent.click(showLessButton)
const showLessButton = within(contributorsSection).getByRole('button', { name: /Show less/i })
await userEvent.click(showLessButton)

await waitFor(() => {
expect(screen.queryByText('Contributor 7')).not.toBeInTheDocument()
Expand Down
31 changes: 19 additions & 12 deletions frontend/__tests__/unit/pages/RepositoryDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQuery } from '@apollo/client'
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
import { act, screen, waitFor, within } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import { mockRepositoryData } from '@unit/data/mockRepositoryData'
import { toast } from 'hooks/useToast'
import { RepositoryDetailsPage } from 'pages'
Expand Down Expand Up @@ -92,24 +93,30 @@ describe('RepositoryDetailsPage', () => {

test('toggles contributors list when show more/less is clicked', async () => {
render(<RepositoryDetailsPage />)
await waitFor(() => {
expect(screen.getByText('Contributor 6')).toBeInTheDocument()
expect(screen.queryByText('Contributor 7')).not.toBeInTheDocument()
})
await waitFor(
() => {
expect(screen.queryByText('Contributor 6')).toBeInTheDocument()
expect(screen.queryByText('Contributor 7')).not.toBeInTheDocument()
},
{ timeout: 1000 }
)

const heading = screen.getByRole('heading', { name: /Top Contributors/i })
expect(heading).toBeInTheDocument()

const contributorsSection = screen
.getByRole('heading', { name: /Top Contributors/i })
.closest('div')
const contributorsSection = heading.closest('div')
expect(contributorsSection).not.toBeNull()
if (!contributorsSection) return
const showMoreButton = within(contributorsSection!).getByRole('button', { name: /Show more/i })
fireEvent.click(showMoreButton)
await userEvent.click(showMoreButton)

await waitFor(() => {
expect(screen.getByText('Contributor 7')).toBeInTheDocument()
expect(screen.getByText('Contributor 8')).toBeInTheDocument()
expect(screen.queryByText('Contributor 7')).toBeInTheDocument()
expect(screen.queryByText('Contributor 8')).toBeInTheDocument()
})

const showLessButton = within(contributorsSection!).getByRole('button', { name: /Show less/i })
fireEvent.click(showLessButton)
await userEvent.click(showLessButton)

await waitFor(() => {
expect(screen.queryByText('Contributor 7')).not.toBeInTheDocument()
Expand Down
39 changes: 27 additions & 12 deletions frontend/src/components/RepositoriesCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,47 @@ import {
IconDefinition,
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useExpandableList } from 'hooks/useExpandableList'
import millify from 'millify'
import type React from 'react'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { RepositoriesCardProps } from 'types/project'

const RepositoriesCard: React.FC<RepositoriesCardProps> = ({ repositories }) => {
const [showAllRepositories, setShowAllRepositories] = useState(false)

const displayedRepositories = showAllRepositories ? repositories : repositories.slice(0, 4)
const { visibleItems, showAll, animatingOut, toggleShowAll } = useExpandableList(repositories, 4)

return (
<div>
<div className="rounded-lg bg-gray-100 p-6 shadow-md dark:bg-gray-800">
<section id="repositories"></section>
<h2 className="mb-4 text-2xl font-semibold">Repositories</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{displayedRepositories.map((repository, index) => {
return <RepositoryItem key={index} details={repository} />
})}
{visibleItems.map((repository, index) => (
<div
key={index}
className={`transition-all duration-700 ease-in-out ${
index >= 4
? showAll
? 'animate-fadeIn'
: animatingOut
? 'animate-fadeOut'
: 'hidden'
: ''
}`}
>
<RepositoryItem details={repository} />
</div>
))}
</div>
<section />

{repositories.length > 4 && (
<div className="mt-6 flex items-center justify-center text-center">
<button

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rishyym0927 please use action button here, we have it in components, or if it does not fit with your requirements please create a separate button component that is modular and reusable, and one more thing we are using Chakra UI so if possible try to use components from the chakra ui library whenever possible to maintain consistency across the code base.
Thank you :)

onClick={() => setShowAllRepositories(!showAllRepositories)}
className="mt-4 flex items-center justify-center text-[#1d7bd7] hover:underline dark:text-sky-600"
onClick={toggleShowAll}
disabled={animatingOut}
className="mt-4 flex items-center justify-center text-[#1d7bd7] transition-all duration-300 hover:underline dark:text-sky-600"
>
{showAllRepositories ? (
{showAll || animatingOut ? (
<>
Show less <FontAwesomeIcon icon={faChevronUp} className="ml-1" />
</>
Expand Down Expand Up @@ -61,7 +77,6 @@ const RepositoryItem = ({ details }) => {
>
{details?.name}
</button>

<div className="space-y-2 text-sm">
<InfoItem
icon={faStar}
Expand Down
29 changes: 14 additions & 15 deletions frontend/src/components/ToggleContributors.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Button } from '@chakra-ui/react'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useState } from 'react'
import { useExpandableList } from 'hooks/useExpandableList'
import { useNavigate } from 'react-router-dom'
import { TopContributorsTypeGraphql } from 'types/contributor'

const TopContributors = ({
contributors,
label = 'Top Contributors',
Expand All @@ -16,25 +17,23 @@ const TopContributors = ({
className?: string
}) => {
const navigate = useNavigate()
const [showAllContributors, setShowAllContributors] = useState(false)

const toggleContributors = () => setShowAllContributors(!showAllContributors)

const displayContributors = showAllContributors
? contributors
: contributors.slice(0, maxInitialDisplay)
const { visibleItems, showAll, animatingOut, toggleShowAll } = useExpandableList(
contributors,
maxInitialDisplay
)

if (contributors.length === 0) {
return
return null
}

return (
<div className={`mb-8 rounded-lg bg-gray-100 p-6 shadow-md dark:bg-gray-800 ${className}`}>
<h2 className="mb-4 text-2xl font-semibold">{label}</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
{displayContributors.map((contributor, index) => (
{visibleItems.map((contributor, index) => (
<div
key={index}
className="flex cursor-pointer items-center space-x-3 rounded-lg p-3 hover:bg-gray-200 dark:hover:bg-gray-700"
className={`flex cursor-pointer items-center space-x-3 rounded-lg p-3 transition-all duration-700 ease-in-out hover:bg-gray-200 dark:hover:bg-gray-700 ${index >= maxInitialDisplay ? (showAll ? 'animate-fadeIn' : animatingOut ? 'animate-fadeOut' : 'hidden') : ''}`}
>
<img
src={`${contributor?.avatarUrl}&s=60`}
Expand All @@ -49,7 +48,6 @@ const TopContributors = ({
>
{contributor.name || contributor.login}
</button>

{contributor?.projectName ? (
<p className="text-sm text-gray-600 dark:text-gray-400">
<a
Expand All @@ -72,10 +70,11 @@ const TopContributors = ({
</div>
{contributors.length > maxInitialDisplay && (
<Button
onClick={toggleContributors}
className="mt-4 flex items-center text-[#1d7bd7] hover:underline dark:text-sky-600"
onClick={toggleShowAll}
disabled={animatingOut}
className={`mt-4 flex items-center text-[#1d7bd7] transition-all duration-300 hover:underline dark:text-sky-600 ${animatingOut ? 'opacity-70' : 'opacity-100'}`}
>
{showAllContributors ? (
{showAll || animatingOut ? (
<>
Show less <FontAwesomeIcon icon={faChevronUp} className="ml-1" />
</>
Expand Down
23 changes: 15 additions & 8 deletions frontend/src/components/ToggleableList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button } from '@chakra-ui/react'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useState } from 'react'
import { useExpandableList } from 'hooks/useExpandableList'

const ToggleableList = ({
items,
Expand All @@ -12,18 +12,24 @@ const ToggleableList = ({
label: string
limit?: number
}) => {
const [showAll, setShowAll] = useState(false)

const toggleShowAll = () => setShowAll(!showAll)
const { visibleItems, showAll, animatingOut, toggleShowAll } = useExpandableList(items, limit)

return (
<div className="rounded-lg bg-gray-100 p-6 shadow-md dark:bg-gray-800">
<h2 className="mb-4 text-2xl font-semibold">{label}</h2>
<div className="flex flex-wrap gap-2">
{(showAll ? items : items.slice(0, limit)).map((item, index) => (
{visibleItems.map((item, index) => (
<span
key={index}
className="rounded-lg border border-gray-400 px-2 py-1 text-sm dark:border-gray-300"
className={`rounded-lg border border-gray-400 px-2 py-1 text-sm transition-all duration-700 ease-in-out dark:border-gray-300 ${
index >= limit
? showAll
? 'animate-fadeIn'
: animatingOut
? 'animate-fadeOut'
: 'hidden'
: ''
}`}
>
{item}
</span>
Expand All @@ -32,9 +38,10 @@ const ToggleableList = ({
{items.length > limit && (
<Button
onClick={toggleShowAll}
className="mt-4 flex items-center text-[#1d7bd7] hover:underline dark:text-sky-600"
disabled={animatingOut}
className="mt-4 flex items-center text-[#1d7bd7] transition-all duration-300 hover:underline dark:text-sky-600"
>
{showAll ? (
{showAll || animatingOut ? (
<>
Show less <FontAwesomeIcon icon={faChevronUp} className="ml-1" />
</>
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/hooks/useExpandableList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useState, useEffect } from 'react'

export const useExpandableList = <T>(items: T[], maxInitialDisplay: number) => {
const [showAll, setShowAll] = useState(false)
const [animatingOut, setAnimatingOut] = useState(false)
const [visibleItems, setVisibleItems] = useState<T[]>(items.slice(0, maxInitialDisplay))

useEffect(() => {
if (showAll) {
setVisibleItems(items)
setAnimatingOut(false)
} else if (animatingOut) {
const timer = setTimeout(() => {
setVisibleItems(items.slice(0, maxInitialDisplay))
setAnimatingOut(false)
}, 500)
return () => clearTimeout(timer)
}
}, [showAll, animatingOut, items, maxInitialDisplay])

const toggleShowAll = () => {
if (showAll) {
setAnimatingOut(true)
setTimeout(() => setShowAll(false), 50)
} else {
setShowAll(true)
}
}

return { visibleItems, showAll, animatingOut, toggleShowAll }
}
40 changes: 40 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,43 @@ a {
left: 50%;
opacity: 0.6;
}

@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes fadeOut {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(10px);
}
}

@keyframes bounceSmall {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-4px);
}
}

.animate-fadeIn {
animation: fadeIn 0.5s ease-out;
}

.animate-fadeOut {
animation: fadeOut 0.5s ease-in;
}
Comment on lines +296 to +334

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Duplicate animation definitions with inconsistent timing

There are duplicate fadeIn and fadeOut keyframes in the file. The original animations (lines 121-141) have a duration of 0.3s while these new ones use 0.5s.

Suggested improvements:

  1. Remove the duplicates and enhance the existing animations with the translateY property
  2. Standardize the animation durations across the application
  3. Consider using CSS variables for animation durations to maintain consistency
- @keyframes fadeIn {
-   from {
-     opacity: 0;
-     transform: translateY(10px);
-   }
-   to {
-     opacity: 1;
-     transform: translateY(0);
-   }
- }
- 
- @keyframes fadeOut {
-   from {
-     opacity: 1;
-     transform: translateY(0);
-   }
-   to {
-     opacity: 0;
-     transform: translateY(10px);
-   }
- }

/* Modify existing animations at lines 121-151 instead */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeOut {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(10px);
}
}
@keyframes bounceSmall {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-4px);
}
}
.animate-fadeIn {
animation: fadeIn 0.5s ease-out;
}
.animate-fadeOut {
animation: fadeOut 0.5s ease-in;
}
/* Duplicate fadeIn and fadeOut keyframes removed.
Modify existing animations at lines 121-151 instead */
@keyframes bounceSmall {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-4px);
}
}
.animate-fadeIn {
animation: fadeIn 0.5s ease-out;
}
.animate-fadeOut {
animation: fadeOut 0.5s ease-in;
}