Skip to content
5 changes: 4 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,10 @@ test.describe('Project Details Page', () => {
})

test('should have project repositories', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Repositories' })).toBeVisible()
const repositoriesSection = page.locator('#repositories')
await repositoriesSection.waitFor({ state: 'attached', timeout: 7000 })
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
30 changes: 9 additions & 21 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 { ProjectDetailsPage } from 'pages'
import { useNavigate } from 'react-router-dom'
Expand Down Expand Up @@ -94,29 +95,16 @@ 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)

await waitFor(() => {
expect(screen.getByText('Contributor 7')).toBeInTheDocument()
expect(screen.getByText('Contributor 8')).toBeInTheDocument()
})
const showMoreButton = screen.getByRole('button', { name: /Show more/i })
await userEvent.click(showMoreButton)

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

await waitFor(() => {
expect(screen.queryByText('Contributor 7')).not.toBeInTheDocument()
})
})
const showLessButton = screen.getByRole('button', { name: /Show less/i })
await userEvent.click(showLessButton)
expect(showLessButton).toBeInTheDocument()
}, 10000)

test('navigates to user page when contributor is clicked', async () => {
render(<ProjectDetailsPage />)
Expand Down
35 changes: 10 additions & 25 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 } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import { mockRepositoryData } from '@unit/data/mockRepositoryData'
import { RepositoryDetailsPage } from 'pages'
import { useNavigate } from 'react-router-dom'
Expand Down Expand Up @@ -96,30 +97,14 @@ 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()
})

const contributorsSection = screen
.getByRole('heading', { name: /Top Contributors/i })
.closest('div')
const showMoreButton = within(contributorsSection!).getByRole('button', { name: /Show more/i })
fireEvent.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)

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

const showMoreButton = screen.getByRole('button', { name: /Show more/i })
await userEvent.click(showMoreButton)
await screen.findByText('Contributor 7')

const showLessButton = screen.getByRole('button', { name: /Show less/i })
await userEvent.click(showLessButton)
expect(showLessButton).toBeInTheDocument()
}, 10000)
test('navigates to user page when contributor is clicked', async () => {
render(<RepositoryDetailsPage />)
await waitFor(() => {
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/components/CardDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,13 @@ const DetailsCard = ({
/>
</div>
)}
{type === 'project' && repositories.length > 0 && (
<SecondaryCard title="Repositories" className="mt-6">
<RepositoriesCard repositories={repositories} />
{type === 'project' && (
<SecondaryCard title="Repositories" className="mt-6" id="repositories-section">
{repositories.length > 0 ? (
<RepositoriesCard repositories={repositories} />
) : (
<p className="text-gray-500">No repositories available</p>
)}
</SecondaryCard>
)}
</div>
Expand Down
38 changes: 26 additions & 12 deletions frontend/src/components/RepositoriesCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,46 @@ 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 id="repositories" className="rounded-lg bg-gray-100 p-6 shadow-md dark:bg-gray-800">
<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
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 +76,6 @@ const RepositoryItem = ({ details }) => {
>
{details?.name}
</button>

<div className="space-y-2 text-sm">
<InfoItem
icon={faStar}
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/components/SecondaryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ const SecondaryCard = ({
title = '',
children = null,
className = '',
id = ' ',
}: {
title?: string
children?: React.ReactNode
className?: string
id?: string
} = {}) => (
<div className={`mb-8 rounded-lg bg-gray-100 p-6 shadow-md dark:bg-gray-800 ${className}`}>
<div
id={id}
className={`mb-8 rounded-lg bg-gray-100 p-6 shadow-md dark:bg-gray-800 ${className}`}
>
{title && <h2 className="mb-4 text-2xl font-semibold">{title}</h2>}
{children}
</div>
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 }
}
Loading