Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
499b093
feat(about): limit roadmap and timeline items with show more toggle
0xgaurav Dec 29, 2025
414b0b7
fix(about): clarify state naming and add timeline show more toggle
0xgaurav Dec 29, 2025
54c7925
refactor(about): reuse ShowMoreButton for timeline toggle
0xgaurav Dec 30, 2025
f93c4f6
Run make update
arkid15r Dec 28, 2025
72080d9
Update backend/Makefile
kasya Dec 28, 2025
3b37e88
refactor: remove redundant exception handling reported by SonarCloud …
anukalp2804 Dec 29, 2025
32c211a
Unify issues view between module issues page and Mentee page (#3050)
kasya Dec 29, 2025
6a11bfc
.
0xgaurav Dec 30, 2025
bca5921
test: add unit test for project timeline milestone limit
0xgaurav Jan 3, 2026
3ec8390
change in toggleablelist.tsx
0xgaurav Jan 3, 2026
0deb4b5
Merge branch 'main' into limit-about-roadmap-timeline
0xgaurav Jan 4, 2026
834ed36
fix: restore JSONDecodeError handling in owasp_sync_board_candidates
0xgaurav Jan 4, 2026
ddca058
Merge branch 'limit-about-roadmap-timeline' of https://github.com/0xg…
0xgaurav Jan 4, 2026
e85dfa0
changes in toggleablelist and it's test file
0xgaurav Jan 4, 2026
e01f3f2
error removal
0xgaurav Jan 4, 2026
db2a18c
changes in toggleablelist
0xgaurav Jan 4, 2026
81db77b
error fix
0xgaurav Jan 4, 2026
b5cb67f
Fix ToggleableList refactor and roadmap timeline rendering
0xgaurav Jan 8, 2026
18b9bcc
Merge branch 'main' into limit-about-roadmap-timeline
0xgaurav Jan 8, 2026
e992b86
Fix timeline limit, ToggleableList rendering, and tests
0xgaurav Jan 9, 2026
dc081b5
Merge branch 'limit-about-roadmap-timeline' of https://github.com/0xg…
0xgaurav Jan 9, 2026
d9285c7
Fix ToggleableList render contract, timeline limit, and related tests
0xgaurav Jan 9, 2026
63a41d5
Fix ToggleableList rendering, timeline limit, and test alignment
0xgaurav Jan 9, 2026
74d50e8
Fix timeline ToggleableList rendering and keys
0xgaurav Jan 9, 2026
f94c35c
fixes
0xgaurav Jan 9, 2026
5b129ec
Merge branch 'main' into limit-about-roadmap-timeline
0xgaurav Jan 9, 2026
99b9536
Merge branch 'main' into limit-about-roadmap-timeline
0xgaurav Jan 10, 2026
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
152 changes: 51 additions & 101 deletions frontend/__tests__/unit/components/ToggleableList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FaUser } from 'react-icons/fa'
import ToggleableList from 'components/ToggleableList'

const mockPush = jest.fn()

jest.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
Expand All @@ -24,170 +25,119 @@ jest.mock('wrappers/IconWrapper', () => ({
icon: Icon,
className,
...props
}: { icon: React.ComponentType<{ className?: string }> } & React.SVGProps<SVGSVGElement>) => (
<Icon className={className} data-testid="react-icon" {...props} />
),
}: {
icon: React.ComponentType<{ className?: string }>
className?: string
}) => <Icon className={className} data-testid="react-icon" {...props} />,
}))

describe('ToggleableList', () => {
const mockItems = Array.from({ length: 15 }, (_, i) => `Item ${i + 1}`)
const renderItem = (item: string) => <span>{item}</span>

beforeEach(() => {
jest.clearAllMocks()
})

it('renders with limited props initially', () => {
render(<ToggleableList items={mockItems} label="test-label" />)
it('renders with limited items initially', () => {
render(<ToggleableList items={mockItems} label="test-label" renderItem={renderItem} />)

// First 10 items should be visible
for (const item of mockItems.slice(0, 10)) {
mockItems.slice(0, 10).forEach((item) => {
expect(screen.getByText(item)).toBeInTheDocument()
}
})

// Remaining items should be hidden
for (const item of mockItems.slice(10)) {
mockItems.slice(10).forEach((item) => {
expect(screen.queryByText(item)).not.toBeInTheDocument()
}
})
})

it('renders with an icon', () => {
render(<ToggleableList items={mockItems} label="test-label" icon={FaUser} />)
render(
<ToggleableList items={mockItems} label="test-label" icon={FaUser} renderItem={renderItem} />
)

const iconElement = screen.getByTestId('react-icon')
expect(iconElement).toBeInTheDocument()
expect(iconElement).toHaveClass('mr-2', 'h-5', 'w-5')
const icon = screen.getByTestId('react-icon')
expect(icon).toBeInTheDocument()
expect(icon).toHaveClass('mr-2', 'h-5', 'w-5')
})

it('respects custom limit prop', () => {
render(<ToggleableList items={mockItems} label="test-label" limit={3} />)
render(
<ToggleableList items={mockItems} label="test-label" limit={3} renderItem={renderItem} />
)

expect(screen.getByText('Item 1')).toBeInTheDocument()
expect(screen.getByText('Item 2')).toBeInTheDocument()
expect(screen.getByText('Item 3')).toBeInTheDocument()
expect(screen.queryByText('Item 4')).not.toBeInTheDocument()
})

it('does not show Show More button when item count is less than the limit', () => {
const limitedItems = mockItems.slice(0, 5)
render(<ToggleableList items={limitedItems} label="test-label" />)
it('does not show Show More button when item count is less than limit', () => {
render(
<ToggleableList items={mockItems.slice(0, 5)} label="test-label" renderItem={renderItem} />
)

expect(screen.queryByTestId('show-more-button')).not.toBeInTheDocument()
})

it('shows Show More button when items exceed limit', () => {
render(<ToggleableList items={mockItems} label="test-label" limit={5} />)
render(
<ToggleableList items={mockItems} label="test-label" limit={5} renderItem={renderItem} />
)

expect(screen.getByTestId('show-more-button')).toBeInTheDocument()
})

it('expands to show all items when ShowMoreButton is clicked', () => {
render(<ToggleableList items={mockItems} label="Expandable Items" limit={5} />)
it('expands to show all items when Show More is clicked', () => {
render(
<ToggleableList
items={mockItems}
label="Expandable Items"
limit={5}
renderItem={renderItem}
/>
)

// Initially hidden items
expect(screen.queryByText('Item 6')).not.toBeInTheDocument()
expect(screen.queryByText('Item 15')).not.toBeInTheDocument()

// Click Show More
fireEvent.click(screen.getByTestId('show-more-button'))

// All items should now be visible
expect(screen.getByText('Item 6')).toBeInTheDocument()
expect(screen.getByText('Item 15')).toBeInTheDocument()
})

it('collapses back to limited view when ShowMoreButton is clicked again', () => {
render(<ToggleableList items={mockItems} label="Collapsible Items" limit={5} />)
it('collapses back when Show More is clicked again', () => {
render(
<ToggleableList
items={mockItems}
label="Collapsible Items"
limit={5}
renderItem={renderItem}
/>
)

// Expand
fireEvent.click(screen.getByTestId('show-more-button'))
expect(screen.getByText('Item 10')).toBeInTheDocument()

// Collapse
fireEvent.click(screen.getByTestId('show-more-button'))
expect(screen.queryByText('Item 6')).not.toBeInTheDocument()
expect(screen.getByText('Item 5')).toBeInTheDocument()
})

it('navigates on item button click', () => {
render(<ToggleableList items={['React', 'Next.js', 'FastAPI']} label="Tags" limit={2} />)
const button = screen.getByText('React')
fireEvent.click(button)
expect(mockPush).toHaveBeenCalledWith('/projects?q=React')
})

it('handles empty items array', () => {
render(<ToggleableList items={[]} label="Empty List" />)
render(<ToggleableList items={[]} label="Empty List" renderItem={renderItem} />)

expect(screen.getByText('Empty List')).toBeInTheDocument()
expect(screen.queryByTestId('show-more-button')).not.toBeInTheDocument()
})

it('handles single item', () => {
render(<ToggleableList items={['Single Item']} label="Single" />)

expect(screen.getByText('Single Item')).toBeInTheDocument()
expect(screen.queryByTestId('show-more-button')).not.toBeInTheDocument()
})

it('handles items exactly equal to limit', () => {
const exactItems = Array.from({ length: 5 }, (_, i) => `Item ${i + 1}`)
render(<ToggleableList items={exactItems} label="Exact Items" limit={5} />)

render(
<ToggleableList items={exactItems} label="Exact Items" limit={5} renderItem={renderItem} />
)

expect(screen.getByText('Item 5')).toBeInTheDocument()
expect(screen.queryByTestId('show-more-button')).not.toBeInTheDocument()
})

it('handles limit of 0', () => {
render(<ToggleableList items={mockItems} label="test-label" limit={0} />)
// Should show ShowMoreButton since limit is exceeded
expect(screen.getByTestId('show-more-button')).toBeInTheDocument()
for (const item of mockItems) {
expect(screen.queryByText(item)).not.toBeInTheDocument()
}
})

it('properly encodes special character in item names', () => {
const itemsWithSpecialChars = ['C++', 'C#', 'Node.js & Express']
render(<ToggleableList items={itemsWithSpecialChars} label="Special Items" />)
const specialButton = screen.getByText('C++')
fireEvent.click(specialButton)

expect(mockPush).toHaveBeenCalledWith('/projects?q=C%2B%2B')
})

it('applies correct CSS classes to main container', () => {
const { container } = render(<ToggleableList items={mockItems} label="Styled List" />)
const mainDiv = container.firstChild
expect(mainDiv).toHaveClass('rounded-lg', 'bg-gray-100', 'p-6', 'shadow-md', 'dark:bg-gray-800')
})

it('applies correct CSS classes to header', () => {
render(<ToggleableList items={mockItems} label="Styled header" />)
const header = screen.getByRole('heading', { level: 2 })
expect(header).toHaveClass('mb-4', 'text-2xl', 'font-semibold')
})

it('applies correct CSS to button items (no underline, no transition, only hover background)', () => {
const randomItems = ['React', 'Vue', 'Angular']
render(<ToggleableList items={randomItems} label="Styled Buttons" />)
const button = screen.getByText('React')
expect(button).toHaveClass(
'rounded-lg',
'border',
'border-gray-400',
'px-3',
'py-1',
'text-sm',
'hover:bg-gray-200',
'dark:border-gray-300',
'dark:hover:bg-gray-700'
)
expect(button).not.toHaveClass(
'hover:underline',
'transition-all',
'duration-200',
'ease-in-out',
'hover:scale-105'
)
})
})
14 changes: 12 additions & 2 deletions frontend/__tests__/unit/pages/About.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,12 @@ jest.mock('utils/aboutData', () => ({
},
projectStory: ['Test story paragraph 1', 'Test story paragraph 2'],
projectTimeline: [
{ title: 'Timeline Event 1', description: 'Timeline description 1', year: '2023' },
{ title: 'Timeline Event 2', description: 'Timeline description 2', year: '2024' },
{ title: 'Timeline Event 1', description: 'Timeline description 1', year: '2020' },
{ title: 'Timeline Event 2', description: 'Timeline description 2', year: '2021' },
{ title: 'Timeline Event 3', description: 'Timeline description 3', year: '2022' },
{ title: 'Timeline Event 4', description: 'Timeline description 4', year: '2023' },
{ title: 'Timeline Event 5', description: 'Timeline description 5', year: '2024' },
{ title: 'Timeline Event 6', description: 'Timeline description 6', year: '2025' },
],
technologies: [
{
Expand Down Expand Up @@ -681,4 +685,10 @@ describe('About Component', () => {
})
})
})
test('limits project timeline milestones to 4 items by default', async () => {
render(<About />)

const milestones = await screen.findAllByText(/Timeline Event/)
expect(milestones.length).toBeLessThanOrEqual(4)
})
})
63 changes: 39 additions & 24 deletions frontend/src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import upperFirst from 'lodash/upperFirst'
import millify from 'millify'
import Image from 'next/image'
import Link from 'next/link'
import { useEffect } from 'react'
import { FaMapSigns, FaTools } from 'react-icons/fa'
import { FaCircleCheck, FaClock, FaScroll, FaBullseye, FaUser, FaUsersGear } from 'react-icons/fa6'
import { useState, useEffect } from 'react'
import { FaMapSigns,FaTools,FaClock } from 'react-icons/fa'
import { FaCircleCheck, FaScroll, FaBullseye, FaUser, FaUsersGear } from 'react-icons/fa6'
import { HiUserGroup } from 'react-icons/hi'
import { IconWrapper } from 'wrappers/IconWrapper'
import { ErrorDisplay, handleAppError } from 'app/global-error'
Expand All @@ -28,7 +28,9 @@ import AnchorTitle from 'components/AnchorTitle'
import Leaders from 'components/Leaders'
import Markdown from 'components/MarkdownWrapper'
import SecondaryCard from 'components/SecondaryCard'
import ShowMoreButton from 'components/ShowMoreButton'
import AboutSkeleton from 'components/skeletons/AboutSkeleton'
import ToggleableList from 'components/ToggleableList'
import TopContributorsList from 'components/TopContributorsList'

const leaders = {
Expand Down Expand Up @@ -60,6 +62,8 @@ const getMilestoneIcon = (progress: number) => {
}

const About = () => {
const [storyExpanded, setStoryExpanded] = useState(false)

const {
data: projectMetadataResponse,
loading: projectMetadataLoading,
Expand Down Expand Up @@ -240,34 +244,45 @@ const About = () => {
</SecondaryCard>
)}
<SecondaryCard icon={FaScroll} title={<AnchorTitle title="Our Story" />}>
{projectStory.map((text) => (
{projectStory.slice(0, storyExpanded ? projectStory.length : 3).map((text) => (
<div key={`story-${text.substring(0, 50).replaceAll(' ', '-')}`} className="mb-4">
<div>
<Markdown content={text} />
</div>
</div>
))}

{projectStory.length > 3 && (
<ShowMoreButton
expanded={storyExpanded}
onToggle={() => setStoryExpanded((v) => !v)}
/>
)}
</SecondaryCard>
<SecondaryCard icon={FaClock} title={<AnchorTitle title="Project Timeline" />}>
<div className="space-y-6">
{[...projectTimeline].reverse().map((milestone, index) => (
<div key={`${milestone.year}-${milestone.title}`} className="relative pl-10">
{index !== projectTimeline.length - 1 && (
<div className="absolute top-5 left-[5px] h-full w-0.5 bg-gray-400"></div>
)}
<div
aria-hidden="true"
className="absolute top-2.5 left-0 h-3 w-3 rounded-full bg-gray-400"
></div>
<div>
<h3 className="text-lg font-semibold text-blue-400">{milestone.title}</h3>
<h4 className="mb-1 font-medium text-gray-400">{milestone.year}</h4>
<p className="text-gray-600 dark:text-gray-300">{milestone.description}</p>
</div>
</div>
))}
</div>
</SecondaryCard>

<ToggleableList
items={[...projectTimeline].reverse()}
limit={4}
label="Project Timeline"
icon={FaClock}
keyExtractor={(item) => `${item.year}-${item.title}`}
renderItem={(milestone, index, visibleCount) => (
<div className="relative pl-10">
{index !== visibleCount - 1 && (
<div className="absolute top-5 left-[5px] h-full w-0.5 bg-gray-400" />
)}

<div
aria-hidden="true"
className="absolute top-2.5 left-0 h-3 w-3 rounded-full bg-gray-400"
/>

<h3 className="text-lg font-semibold text-blue-400">{milestone.title}</h3>
<h4 className="mb-1 font-medium text-gray-400">{milestone.year}</h4>
<p className="text-gray-600 dark:text-gray-300">{milestone.description}</p>
</div>
)}
/>

<div className="grid gap-0 md:grid-cols-4 md:gap-6">
{[
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/components/CardDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,10 +216,16 @@ const DetailsCard = ({
items={languages}
icon={FaCode}
label={<AnchorTitle title="Languages" />}
renderItem={(item) => <span>{item}</span>}
/>
)}
{topics.length !== 0 && (
<ToggleableList items={topics} icon={FaTags} label={<AnchorTitle title="Topics" />} />
<ToggleableList
items={topics}
icon={FaTags}
label={<AnchorTitle title="Topics" />}
renderItem={(item) => <span>{item}</span>}
/>
)}
</div>
)}
Expand All @@ -235,6 +241,7 @@ const DetailsCard = ({
icon={FaTags}
label={<AnchorTitle title="Tags" />}
isDisabled={true}
renderItem={(item) => <span>{item}</span>}
/>
)}
{domains?.length > 0 && (
Expand All @@ -243,6 +250,7 @@ const DetailsCard = ({
icon={FaChartPie}
label={<AnchorTitle title="Domains" />}
isDisabled={true}
renderItem={(item) => <span>{item}</span>}
/>
)}
</div>
Expand All @@ -254,6 +262,7 @@ const DetailsCard = ({
icon={FaTags}
label={<AnchorTitle title="Labels" />}
isDisabled={true}
renderItem={(item) => <span>{item}</span>}
/>
</div>
)}
Expand Down
Loading
Loading