Skip to content
17 changes: 11 additions & 6 deletions frontend/__tests__/unit/components/ModuleList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import ModuleList from 'components/ModuleList'

// Mock FontAwesome icons
jest.mock('@fortawesome/react-fontawesome', () => ({
FontAwesomeIcon: ({ icon, className }: { icon: unknown; className?: string }) => (
<span
data-testid={`icon-${icon === faChevronDown ? 'chevron-down' : icon === faChevronUp ? 'chevron-up' : 'unknown'}`}
className={className}
/>
),
FontAwesomeIcon: ({ icon, className }: { icon: unknown; className?: string }) => {
let iconName = 'unknown'

if (icon === faChevronDown) {
iconName = 'chevron-down'
} else if (icon === faChevronUp) {
iconName = 'chevron-up'
}

return <span data-testid={`icon-${iconName}`} className={className} />
},
}))

// Mock HeroUI Button component
Expand Down
17 changes: 11 additions & 6 deletions frontend/__tests__/unit/components/ProgramCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import type { Program } from 'types/mentorship'
import ProgramCard from 'components/ProgramCard'

jest.mock('@fortawesome/react-fontawesome', () => ({
FontAwesomeIcon: ({ icon, className }: { icon: unknown; className?: string }) => (
<span
data-testid={`icon-${icon === faEye ? 'eye' : icon === faEdit ? 'edit' : 'unknown'}`}
className={className}
/>
),
FontAwesomeIcon: ({ icon, className }: { icon: unknown; className?: string }) => {
let iconName = 'unknown'

if (icon === faEye) {
iconName = 'eye'
} else if (icon === faEdit) {
iconName = 'edit'
}

return <span data-testid={`icon-${iconName}`} className={className} />
},
}))

jest.mock('components/ActionButton', () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ describe('MetricsPage', () => {
})
})

test('SortableColumnHeader applies correct alignment classes', async () => {
render(<MetricsPage />)
const sortButton = await screen.findByTitle('Sort by Stars')
const wrapperDiv = sortButton.closest('div')
expect(wrapperDiv).not.toBeNull()
expect(wrapperDiv).toHaveClass('justify-center')
expect(sortButton).toHaveClass('text-center')
})

test('handles sorting state and URL updates', async () => {
const mockReplace = jest.fn()
const { useRouter, useSearchParams } = jest.requireMock('next/navigation')
Expand Down
33 changes: 33 additions & 0 deletions frontend/__tests__/unit/utils/milestoneProgress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { faCircleCheck, faClock, faUserGear } from '@fortawesome/free-solid-svg-icons'

import { getMilestoneProgressIcon, getMilestoneProgressText } from 'utils/milestoneProgress'

describe('milestone progress helpers', () => {
describe('getMilestoneProgressText', () => {
test('returns "Completed" when progress is 100', () => {
expect(getMilestoneProgressText(100)).toBe('Completed')
})

test('returns "In Progress" when progress is between 1 and 99', () => {
expect(getMilestoneProgressText(50)).toBe('In Progress')
})

test('returns "Not Started" when progress is 0', () => {
expect(getMilestoneProgressText(0)).toBe('Not Started')
})
})

describe('getMilestoneProgressIcon', () => {
test('returns faCircleCheck when progress is 100', () => {
expect(getMilestoneProgressIcon(100)).toBe(faCircleCheck)
})

test('returns faUserGear when progress is between 1 and 99', () => {
expect(getMilestoneProgressIcon(50)).toBe(faUserGear)
})

test('returns faClock when progress is 0', () => {
expect(getMilestoneProgressIcon(0)).toBe(faClock)
})
})
})
20 changes: 3 additions & 17 deletions frontend/src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useQuery } from '@apollo/client/react'
import {
faCircleCheck,
faClock,
faUserGear,
faMapSigns,
faScroll,
faUsers,
Expand Down Expand Up @@ -34,6 +33,7 @@ import {
projectTimeline,
projectStory,
} from 'utils/aboutData'
import { getMilestoneProgressIcon, getMilestoneProgressText } from 'utils/milestoneProgress'
import AnchorTitle from 'components/AnchorTitle'
import AnimatedCounter from 'components/AnimatedCounter'
import Leaders from 'components/Leaders'
Expand Down Expand Up @@ -222,28 +222,14 @@ const About = () => {
</Link>
<Tooltip
closeDelay={100}
content={
milestone.progress === 100
? 'Completed'
: milestone.progress > 0
? 'In Progress'
: 'Not Started'
}
content={getMilestoneProgressText(milestone.progress)}
id={`tooltip-state-${index}`}
delay={100}
placement="top"
showArrow
>
<span className="absolute top-0 right-0 text-xl text-gray-400">
<FontAwesomeIcon
icon={
milestone.progress === 100
? faCircleCheck
: milestone.progress > 0
? faUserGear
: faClock
}
/>
<FontAwesomeIcon icon={getMilestoneProgressIcon(milestone.progress)} />
</span>
</Tooltip>
</div>
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/app/global-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,14 @@ export class AppError extends Error {
}

export const handleAppError = (error: unknown) => {
const appError =
error instanceof AppError
? error
: new AppError(500, error instanceof Error ? error.message : ERROR_CONFIGS['500'].message)
let appError: AppError

if (error instanceof AppError) {
appError = error
} else {
const message = error instanceof Error ? error.message : ERROR_CONFIGS['500'].message
appError = new AppError(500, message)
}

if (appError.statusCode >= 500) {
Sentry.captureException(error instanceof Error ? error : appError)
Expand Down
35 changes: 30 additions & 5 deletions frontend/src/app/projects/dashboard/metrics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,35 @@ const SortableColumnHeader: FC<{
}
}

const alignmentClass =
align === 'center' ? 'justify-center' : align === 'right' ? 'justify-end' : 'justify-start'
const textAlignClass =
align === 'center' ? 'text-center' : align === 'right' ? 'text-right' : 'text-left'
const alignmentClass = (() => {
if (align === 'center') {
return 'justify-center'
} else if (align === 'right') {
return 'justify-end'
} else {
return 'justify-start'
}
})()

const textAlignClass = (() => {
if (align === 'center') {
return 'text-center'
} else if (align === 'right') {
return 'text-right'
} else {
return 'text-left'
}
})()
Copy link
Collaborator

Choose a reason for hiding this comment

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

@ishanBahuguna I think this would be more clean and readable if we switch to using maps here. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Map would be great , more readable code , avoid repeated if/else logic and easier to extend

 const justifyMap = {
  left: 'justify-start',
  center: 'justify-center',
  right: 'justify-end',
}
const alignmentClass = justifyMap[align] || justifyMap.left

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kasya should I go ahead and make the changes?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@ishanBahuguna yes, let's do it!


const fontAwesomeIconType = (() => {
if (isActiveSortDesc) {
return faSortDown
} else if (isActiveSortAsc) {
return faSortUp
} else {
return faSort
}
})()

return (
<div className={`flex items-center gap-1 ${alignmentClass}`}>
Expand All @@ -93,7 +118,7 @@ const SortableColumnHeader: FC<{
>
<span className="truncate">{label}</span>
<FontAwesomeIcon
icon={isActiveSortDesc ? faSortDown : isActiveSortAsc ? faSortUp : faSort}
icon={fontAwesomeIconType}
className={`h-3 w-3 ${isActive ? 'text-blue-600' : 'text-gray-400'}`}
/>
</button>
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/components/CardDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ const DetailsCard = ({
}: DetailsCardProps) => {
const { data } = useSession()
const router = useRouter()

// compute styles based on type prop
const secondaryCardStyles = (() => {
if (type === 'program' || type === 'module') {
return 'gap-2 md:col-span-7'
} else if (type === 'chapter') {
return 'gap-2 md:col-span-3'
}
return 'gap-2 md:col-span-5'
})()

return (
<div className="min-h-screen bg-white p-8 text-gray-600 dark:bg-[#212529] dark:text-gray-300">
<div className="mx-auto max-w-6xl">
Expand Down Expand Up @@ -122,13 +133,7 @@ const DetailsCard = ({
<SecondaryCard
icon={faRectangleList}
title={<AnchorTitle title={`${upperFirst(type)} Details`} />}
className={
type === 'program' || type === 'module'
? 'gap-2 md:col-span-7'
: type !== 'chapter'
? 'gap-2 md:col-span-5'
: 'gap-2 md:col-span-3'
}
className={secondaryCardStyles}
>
{details?.map((detail) =>
detail?.label === 'Leaders' ? (
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/components/ProgramCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ const ProgramCard: React.FC<ProgramCardProps> = ({ program, onEdit, onView, acce
? `${program.description.slice(0, 100)}...`
: program.description || 'No description available.'

// computes a formatted date string for the program based on its start and end dates.
const dateInfo = (() => {
if (program.startedAt && program.endedAt) {
return `${formatDate(program.startedAt)} – ${formatDate(program.endedAt)}`
} else if (program.startedAt) {
return `Started: ${formatDate(program.startedAt)}`
} else {
return 'No dates set'
}
})()

return (
<div className="h-64 w-80 rounded-[5px] border border-gray-400 bg-white p-6 text-left transition-transform duration-300 hover:scale-[1.02] hover:brightness-105 dark:border-gray-600 dark:bg-gray-800">
<div className="flex h-full flex-col justify-between">
Expand All @@ -47,13 +58,7 @@ const ProgramCard: React.FC<ProgramCardProps> = ({ program, onEdit, onView, acce
</span>
)}
</div>
<div className="mb-2 text-xs text-gray-600 dark:text-gray-400">
{program.startedAt && program.endedAt
? `${formatDate(program.startedAt)} – ${formatDate(program.endedAt)}`
: program.startedAt
? `Started: ${formatDate(program.startedAt)}`
: 'No dates set'}
</div>
<div className="mb-2 text-xs text-gray-600 dark:text-gray-400">{dateInfo}</div>
<p className="mb-4 text-sm text-gray-700 dark:text-gray-300">{description}</p>
</div>

Expand Down
19 changes: 14 additions & 5 deletions frontend/src/hooks/useSearchPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ export function useSearchPage<T>({
if (searchQuery) params.set('q', searchQuery)
if (currentPage > 1) params.set('page', currentPage.toString())

if (sortBy && sortBy !== 'default' && sortBy[0] !== 'default' && sortBy !== '') {
if (sortBy && sortBy !== 'default' && sortBy !== '') {
params.set('sortBy', sortBy)
}

if (sortBy !== 'default' && sortBy[0] !== 'default' && order && order !== '') {
if (sortBy !== 'default' && order && order !== '') {
params.set('order', order)
}

Expand All @@ -84,10 +84,19 @@ export function useSearchPage<T>({

const fetchData = async () => {
try {
let computedIndexName = indexName

// Check if valid sort option is selected
const hasValidSort = sortBy && sortBy !== 'default'

if (hasValidSort) {
// if sorting is active then appends the sort field and order to the base index name.
const orderSuffix = order && order !== '' ? `_${order}` : ''
computedIndexName = `${indexName}_${sortBy}${orderSuffix}`
}

const response = await fetchAlgoliaData<T>(
sortBy && sortBy !== 'default' && sortBy[0] !== 'default'
? `${indexName}_${sortBy}${order && order !== '' ? `_${order}` : ''}`
: indexName,
computedIndexName,
searchQuery,
currentPage,
hitsPerPage
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/utils/milestoneProgress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { faCircleCheck, faClock, faUserGear } from '@fortawesome/free-solid-svg-icons'

// helper functions used in about/page.tsx
export const getMilestoneProgressText = (progress: number): string => {
if (progress === 100) {
return 'Completed'
} else if (progress > 0) {
return 'In Progress'
} else {
return 'Not Started'
}
}

export const getMilestoneProgressIcon = (progress: number) => {
if (progress === 100) {
return faCircleCheck
} else if (progress > 0) {
return faUserGear
} else {
return faClock
}
}
Loading