Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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 @@ -218,28 +218,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'
}
})()

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
15 changes: 12 additions & 3 deletions frontend/src/hooks/useSearchPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,19 @@ export function useSearchPage<T>({

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

// check if valid sort option is selected
const hasValidSort = sortBy && sortBy !== 'default' && sortBy[0] !== '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
}
}