diff --git a/frontend/__tests__/unit/components/ModuleList.test.tsx b/frontend/__tests__/unit/components/ModuleList.test.tsx index ca151c466d..290e9ea4d8 100644 --- a/frontend/__tests__/unit/components/ModuleList.test.tsx +++ b/frontend/__tests__/unit/components/ModuleList.test.tsx @@ -5,12 +5,17 @@ import ModuleList from 'components/ModuleList' // Mock FontAwesome icons jest.mock('@fortawesome/react-fontawesome', () => ({ - FontAwesomeIcon: ({ icon, className }: { icon: unknown; className?: string }) => ( - - ), + FontAwesomeIcon: ({ icon, className }: { icon: unknown; className?: string }) => { + let iconName = 'unknown' + + if (icon === faChevronDown) { + iconName = 'chevron-down' + } else if (icon === faChevronUp) { + iconName = 'chevron-up' + } + + return + }, })) // Mock HeroUI Button component diff --git a/frontend/__tests__/unit/components/ProgramCard.test.tsx b/frontend/__tests__/unit/components/ProgramCard.test.tsx index b7fb66cc9e..cc4a6def7a 100644 --- a/frontend/__tests__/unit/components/ProgramCard.test.tsx +++ b/frontend/__tests__/unit/components/ProgramCard.test.tsx @@ -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 }) => ( - - ), + FontAwesomeIcon: ({ icon, className }: { icon: unknown; className?: string }) => { + let iconName = 'unknown' + + if (icon === faEye) { + iconName = 'eye' + } else if (icon === faEdit) { + iconName = 'edit' + } + + return + }, })) jest.mock('components/ActionButton', () => ({ diff --git a/frontend/__tests__/unit/pages/ProjectsHealthDashboardMetrics.test.tsx b/frontend/__tests__/unit/pages/ProjectsHealthDashboardMetrics.test.tsx index c2323e1a7b..d9f47c0aa7 100644 --- a/frontend/__tests__/unit/pages/ProjectsHealthDashboardMetrics.test.tsx +++ b/frontend/__tests__/unit/pages/ProjectsHealthDashboardMetrics.test.tsx @@ -148,6 +148,15 @@ describe('MetricsPage', () => { }) }) + test('SortableColumnHeader applies correct alignment classes', async () => { + render() + 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') diff --git a/frontend/__tests__/unit/utils/milestoneProgress.test.ts b/frontend/__tests__/unit/utils/milestoneProgress.test.ts new file mode 100644 index 0000000000..e08e3b7935 --- /dev/null +++ b/frontend/__tests__/unit/utils/milestoneProgress.test.ts @@ -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) + }) + }) +}) diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx index 0f0b9549fb..5c8e8891ad 100644 --- a/frontend/src/app/about/page.tsx +++ b/frontend/src/app/about/page.tsx @@ -3,7 +3,6 @@ import { useQuery } from '@apollo/client/react' import { faCircleCheck, faClock, - faUserGear, faMapSigns, faScroll, faUsers, @@ -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' @@ -218,28 +218,14 @@ const About = () => { 0 - ? 'In Progress' - : 'Not Started' - } + content={getMilestoneProgressText(milestone.progress)} id={`tooltip-state-${index}`} delay={100} placement="top" showArrow > - 0 - ? faUserGear - : faClock - } - /> + diff --git a/frontend/src/app/global-error.tsx b/frontend/src/app/global-error.tsx index e984ccf804..69d7b766fc 100644 --- a/frontend/src/app/global-error.tsx +++ b/frontend/src/app/global-error.tsx @@ -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) diff --git a/frontend/src/app/projects/dashboard/metrics/page.tsx b/frontend/src/app/projects/dashboard/metrics/page.tsx index af4048040a..22b9bb864b 100644 --- a/frontend/src/app/projects/dashboard/metrics/page.tsx +++ b/frontend/src/app/projects/dashboard/metrics/page.tsx @@ -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 (
@@ -93,7 +118,7 @@ const SortableColumnHeader: FC<{ > {label} diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index ca2055cebb..99fce967e3 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -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 (
@@ -122,13 +133,7 @@ const DetailsCard = ({ } - 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' ? ( diff --git a/frontend/src/components/ProgramCard.tsx b/frontend/src/components/ProgramCard.tsx index d6b335e3d6..e5fe1c46b5 100644 --- a/frontend/src/components/ProgramCard.tsx +++ b/frontend/src/components/ProgramCard.tsx @@ -31,6 +31,17 @@ const ProgramCard: React.FC = ({ 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 (
@@ -47,13 +58,7 @@ const ProgramCard: React.FC = ({ program, onEdit, onView, acce )}
-
- {program.startedAt && program.endedAt - ? `${formatDate(program.startedAt)} – ${formatDate(program.endedAt)}` - : program.startedAt - ? `Started: ${formatDate(program.startedAt)}` - : 'No dates set'} -
+
{dateInfo}

{description}

diff --git a/frontend/src/hooks/useSearchPage.ts b/frontend/src/hooks/useSearchPage.ts index dd92aa9545..0b37d6bd3b 100644 --- a/frontend/src/hooks/useSearchPage.ts +++ b/frontend/src/hooks/useSearchPage.ts @@ -82,10 +82,19 @@ export function useSearchPage({ 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( - sortBy && sortBy !== 'default' && sortBy[0] !== 'default' - ? `${indexName}_${sortBy}${order && order !== '' ? `_${order}` : ''}` - : indexName, + computedIndexName, searchQuery, currentPage, hitsPerPage diff --git a/frontend/src/utils/milestoneProgress.ts b/frontend/src/utils/milestoneProgress.ts new file mode 100644 index 0000000000..544c547149 --- /dev/null +++ b/frontend/src/utils/milestoneProgress.ts @@ -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 + } +}