diff --git a/frontend/__tests__/unit/components/ProgramCard.test.tsx b/frontend/__tests__/unit/components/ProgramCard.test.tsx index a6c97a9d9f..505a518426 100644 --- a/frontend/__tests__/unit/components/ProgramCard.test.tsx +++ b/frontend/__tests__/unit/components/ProgramCard.test.tsx @@ -24,6 +24,14 @@ jest.mock('components/ActionButton', () => ({ ), })) +jest.mock('@heroui/tooltip', () => ({ + Tooltip: ({ children, content }: { children: React.ReactNode; content: string }) => ( +
+ {children} +
+ ), +})) + describe('ProgramCard', () => { const mockOnEdit = jest.fn() const mockOnView = jest.fn() @@ -178,23 +186,31 @@ describe('ProgramCard', () => { }) describe('Description Handling', () => { - it('truncates long descriptions to 100 characters', () => { - const longDescription = 'A'.repeat(150) + it('renders long descriptions with line-clamp-6 CSS class', () => { + const longDescription = 'A'.repeat(300) // Long enough to trigger line clamping const longDescProgram = { ...baseMockProgram, description: longDescription } render() - const expectedText = 'A'.repeat(100) + '...' - expect(screen.getByText(expectedText)).toBeInTheDocument() + // Check that the full description is rendered (CSS handles the visual truncation) + expect(screen.getByText(longDescription)).toBeInTheDocument() + + // Check that the paragraph has the line-clamp-6 class + const descriptionElement = screen.getByText(longDescription) + expect(descriptionElement).toHaveClass('line-clamp-6') }) - it('shows full description when under 100 characters', () => { + it('shows full description when short', () => { const shortDescription = 'Short description' const shortDescProgram = { ...baseMockProgram, description: shortDescription } render() expect(screen.getByText('Short description')).toBeInTheDocument() + + // Check that it still has line-clamp-6 class for consistency + const descriptionElement = screen.getByText('Short description') + expect(descriptionElement).toHaveClass('line-clamp-6') }) it('shows fallback text when description is empty', () => { diff --git a/frontend/__tests__/unit/components/SingleModuleCard.test.tsx b/frontend/__tests__/unit/components/SingleModuleCard.test.tsx index 8f7861fd59..d5a9d56412 100644 --- a/frontend/__tests__/unit/components/SingleModuleCard.test.tsx +++ b/frontend/__tests__/unit/components/SingleModuleCard.test.tsx @@ -1,10 +1,9 @@ import { faUsers } from '@fortawesome/free-solid-svg-icons' -import { fireEvent, screen, waitFor } from '@testing-library/react' +import { screen } from '@testing-library/react' import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' import React from 'react' import { render } from 'wrappers/testUtil' -import type { ExtendedSession } from 'types/auth' import type { Module } from 'types/mentorship' import { ExperienceLevelEnum, ProgramStatusEnum } from 'types/mentorship' import SingleModuleCard from 'components/SingleModuleCard' @@ -111,16 +110,6 @@ const mockModule: Module = { const mockAdmins = [{ login: 'admin1' }, { login: 'admin2' }] -const mockSessionData: ExtendedSession = { - user: { - login: 'admin1', - isLeader: true, - email: 'admin@example.com', - image: 'https://example.com/admin-avatar.jpg', - }, - expires: '2024-12-31T23:59:59Z', -} - describe('SingleModuleCard', () => { beforeEach(() => { jest.clearAllMocks() @@ -146,7 +135,6 @@ describe('SingleModuleCard', () => { expect(screen.getByText('Test Module')).toBeInTheDocument() expect(screen.getByText('This is a test module description')).toBeInTheDocument() expect(screen.getByTestId('icon-users')).toBeInTheDocument() - expect(screen.getByTestId('icon-ellipsis')).toBeInTheDocument() }) it('renders module details correctly', () => { @@ -183,128 +171,31 @@ describe('SingleModuleCard', () => { }) }) - describe('Dropdown Menu', () => { - it('opens dropdown when ellipsis button is clicked', () => { - render() - - const ellipsisButton = screen.getByRole('button') - fireEvent.click(ellipsisButton) - - expect(screen.getByText('View Module')).toBeInTheDocument() - }) - - it('closes dropdown when clicking outside', async () => { - render() - - const ellipsisButton = screen.getByRole('button') - fireEvent.click(ellipsisButton) - - expect(screen.getByText('View Module')).toBeInTheDocument() - - // Click outside the dropdown - fireEvent.mouseDown(document.body) - - await waitFor(() => { - expect(screen.queryByText('View Module')).not.toBeInTheDocument() - }) - }) - - it('navigates to view module when View Module is clicked', () => { + describe('Simplified Interface', () => { + it('focuses on content display only', () => { render() - const ellipsisButton = screen.getByRole('button') - fireEvent.click(ellipsisButton) - - const viewButton = screen.getByText('View Module') - fireEvent.click(viewButton) - - expect(mockPush).toHaveBeenCalledWith('//modules/test-module') - }) - - it('shows only View Module option for non-admin users', () => { - render( - - ) - - const ellipsisButton = screen.getByRole('button') - fireEvent.click(ellipsisButton) + // Should display core content + expect(screen.getByText('Test Module')).toBeInTheDocument() + expect(screen.getByText('This is a test module description')).toBeInTheDocument() + expect(screen.getByText('Experience Level:')).toBeInTheDocument() - expect(screen.getByText('View Module')).toBeInTheDocument() - expect(screen.queryByText('Edit Module')).not.toBeInTheDocument() - expect(screen.queryByText('Create Module')).not.toBeInTheDocument() + // Should have clickable title for navigation + const moduleLink = screen.getByTestId('module-link') + expect(moduleLink).toHaveAttribute('href', '//modules/test-module') }) }) - describe('Admin Functionality', () => { - beforeEach(() => { - mockUseSession.mockReturnValue({ - data: mockSessionData, - status: 'authenticated', - update: jest.fn(), - }) - }) - - it('shows Edit Module option for admin users when showEdit is true', () => { - render( - - ) - - const ellipsisButton = screen.getByRole('button') - fireEvent.click(ellipsisButton) - - expect(screen.getByText('View Module')).toBeInTheDocument() - expect(screen.getByText('Edit Module')).toBeInTheDocument() - expect(screen.getByText('Create Module')).toBeInTheDocument() - }) - - it('does not show Edit Module option when showEdit is false', () => { - render( - - ) - - const ellipsisButton = screen.getByRole('button') - fireEvent.click(ellipsisButton) - - expect(screen.getByText('View Module')).toBeInTheDocument() - expect(screen.queryByText('Edit Module')).not.toBeInTheDocument() - expect(screen.getByText('Create Module')).toBeInTheDocument() - }) - - it('navigates to edit module when Edit Module is clicked', () => { - render( - - ) - - const ellipsisButton = screen.getByRole('button') - fireEvent.click(ellipsisButton) - - const editButton = screen.getByText('Edit Module') - fireEvent.click(editButton) + describe('Props Handling', () => { + it('renders correctly with minimal props', () => { + render() - expect(mockPush).toHaveBeenCalledWith('//modules/test-module/edit') + expect(screen.getByText('Test Module')).toBeInTheDocument() + expect(screen.getByText('This is a test module description')).toBeInTheDocument() }) - it('navigates to create module when Create Module is clicked', () => { + it('ignores admin-related props since menu is removed', () => { + // These props are now ignored but should not cause errors render( { /> ) - const ellipsisButton = screen.getByRole('button') - fireEvent.click(ellipsisButton) - - const createButton = screen.getByText('Create Module') - fireEvent.click(createButton) - - expect(mockPush).toHaveBeenCalledWith('//modules/create') + expect(screen.getByText('Test Module')).toBeInTheDocument() }) }) @@ -338,67 +223,31 @@ describe('SingleModuleCard', () => { expect(screen.queryByTestId('top-contributors-list')).not.toBeInTheDocument() }) - it('handles undefined admins array', () => { + it('handles undefined admins array gracefully', () => { render() - const ellipsisButton = screen.getByRole('button') - fireEvent.click(ellipsisButton) - - expect(screen.getByText('View Module')).toBeInTheDocument() - expect(screen.queryByText('Edit Module')).not.toBeInTheDocument() - }) - - it('handles null session data', () => { - mockUseSession.mockReturnValue({ - data: null, - status: 'unauthenticated', - update: jest.fn(), - }) - - render( - - ) - - const ellipsisButton = screen.getByRole('button') - fireEvent.click(ellipsisButton) - - expect(screen.getByText('View Module')).toBeInTheDocument() - expect(screen.queryByText('Edit Module')).not.toBeInTheDocument() + // Should render without errors even with admin props + expect(screen.getByText('Test Module')).toBeInTheDocument() }) }) describe('Accessibility', () => { - it('has proper button roles and interactions', () => { + it('has accessible link for module navigation', () => { render() - const ellipsisButton = screen.getByRole('button') - expect(ellipsisButton).toBeInTheDocument() - - fireEvent.click(ellipsisButton) - - const viewButton = screen.getByText('View Module') - expect(viewButton.closest('button')).toBeInTheDocument() + const moduleLink = screen.getByTestId('module-link') + expect(moduleLink).toBeInTheDocument() + expect(moduleLink).toHaveAttribute('href', '//modules/test-module') + expect(moduleLink).toHaveAttribute('target', '_blank') + expect(moduleLink).toHaveAttribute('rel', 'noopener noreferrer') }) - it('supports keyboard navigation', () => { + it('has proper heading structure', () => { render() - const ellipsisButton = screen.getByRole('button') - - // Focus the button - ellipsisButton.focus() - expect(ellipsisButton).toHaveFocus() - - // Press Enter to open dropdown - fireEvent.keyDown(ellipsisButton, { key: 'Enter', code: 'Enter' }) - fireEvent.click(ellipsisButton) // Simulate the click that would happen - - expect(screen.getByText('View Module')).toBeInTheDocument() + const moduleTitle = screen.getByRole('heading', { level: 1 }) + expect(moduleTitle).toBeInTheDocument() + expect(moduleTitle).toHaveTextContent('Test Module') }) }) diff --git a/frontend/__tests__/unit/pages/ProgramDetails.test.tsx b/frontend/__tests__/unit/pages/ProgramDetails.test.tsx index 4231f31836..cfb16deeaa 100644 --- a/frontend/__tests__/unit/pages/ProgramDetails.test.tsx +++ b/frontend/__tests__/unit/pages/ProgramDetails.test.tsx @@ -67,7 +67,7 @@ describe('ProgramDetailsPage', () => { expect(screen.getByText('Jan 1, 2025')).toBeInTheDocument() expect(screen.getByText('Dec 31, 2025')).toBeInTheDocument() expect(screen.getByText('20')).toBeInTheDocument() - expect(screen.getByText('beginner, intermediate')).toBeInTheDocument() + expect(screen.getByText('Beginner, Intermediate')).toBeInTheDocument() }) }) }) diff --git a/frontend/__tests__/unit/pages/ProgramDetailsMentorship.test.tsx b/frontend/__tests__/unit/pages/ProgramDetailsMentorship.test.tsx index 33e457e0d3..6cbe6e4f54 100644 --- a/frontend/__tests__/unit/pages/ProgramDetailsMentorship.test.tsx +++ b/frontend/__tests__/unit/pages/ProgramDetailsMentorship.test.tsx @@ -67,7 +67,7 @@ describe('ProgramDetailsPage', () => { expect(screen.getByText('Jan 1, 2025')).toBeInTheDocument() expect(screen.getByText('Dec 31, 2025')).toBeInTheDocument() expect(screen.getByText('20')).toBeInTheDocument() - expect(screen.getByText('beginner, intermediate')).toBeInTheDocument() + expect(screen.getByText('Beginner, Intermediate')).toBeInTheDocument() }) }) }) diff --git a/frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx b/frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx index c2f4e2b1c6..57e7b1b84c 100644 --- a/frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx +++ b/frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx @@ -1,12 +1,12 @@ 'use client' import { useQuery } from '@apollo/client' -import upperFirst from 'lodash/upperFirst' import { useParams } from 'next/navigation' import { useEffect, useState } from 'react' import { ErrorDisplay, handleAppError } from 'app/global-error' import { GET_PROGRAM_ADMINS_AND_MODULES } from 'server/queries/moduleQueries' import type { Module } from 'types/mentorship' +import { titleCaseWord } from 'utils/capitalize' import { formatDate } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' @@ -49,7 +49,7 @@ const ModuleDetailsPage = () => { } const moduleDetails = [ - { label: 'Experience Level', value: upperFirst(module.experienceLevel) }, + { label: 'Experience Level', value: titleCaseWord(module.experienceLevel) }, { label: 'Start Date', value: formatDate(module.startedAt) }, { label: 'End Date', value: formatDate(module.endedAt) }, { diff --git a/frontend/src/app/mentorship/programs/[programKey]/page.tsx b/frontend/src/app/mentorship/programs/[programKey]/page.tsx index ccd7bbd687..3b064dc52c 100644 --- a/frontend/src/app/mentorship/programs/[programKey]/page.tsx +++ b/frontend/src/app/mentorship/programs/[programKey]/page.tsx @@ -1,12 +1,12 @@ 'use client' import { useQuery } from '@apollo/client' -import upperFirst from 'lodash/upperFirst' import { useParams, useSearchParams, useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import { ErrorDisplay } from 'app/global-error' import { GET_PROGRAM_AND_MODULES } from 'server/queries/programsQueries' import type { Module, Program } from 'types/mentorship' +import { titleCaseWord } from 'utils/capitalize' import { formatDate } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' @@ -70,13 +70,13 @@ const ProgramDetailsPage = () => { } const programDetails = [ - { label: 'Status', value: upperFirst(program.status) }, + { label: 'Status', value: titleCaseWord(program.status) }, { label: 'Start Date', value: formatDate(program.startedAt) }, { label: 'End Date', value: formatDate(program.endedAt) }, { label: 'Mentees Limit', value: String(program.menteesLimit) }, { label: 'Experience Levels', - value: program.experienceLevels?.join(', ') || 'N/A', + value: program.experienceLevels?.map((level) => titleCaseWord(level)).join(', ') || 'N/A', }, ] diff --git a/frontend/src/app/mentorship/programs/page.tsx b/frontend/src/app/mentorship/programs/page.tsx index 66f78a897e..6fd1a9552d 100644 --- a/frontend/src/app/mentorship/programs/page.tsx +++ b/frontend/src/app/mentorship/programs/page.tsx @@ -50,7 +50,7 @@ const ProgramsPage = () => { searchQuery={searchQuery} totalPages={totalPages} > -
+
{programs && programs.filter((p) => p.status === 'published').map(renderProgramCard)}
diff --git a/frontend/src/app/my/mentorship/page.tsx b/frontend/src/app/my/mentorship/page.tsx index 183a3a8ecb..2248eb54d9 100644 --- a/frontend/src/app/my/mentorship/page.tsx +++ b/frontend/src/app/my/mentorship/page.tsx @@ -129,7 +129,7 @@ const MyMentorshipPage: React.FC = () => { searchPlaceholder="Search your programs" indexName="my-programs" > -
+
{programs.length === 0 ? (

Program not found

diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx index a28ef94602..96497bf9e9 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx @@ -12,6 +12,7 @@ import { GET_PROGRAM_AND_MODULES } from 'server/queries/programsQueries' import type { ExtendedSession } from 'types/auth' import type { Module, Program } from 'types/mentorship' import { ProgramStatusEnum } from 'types/mentorship' +import { titleCaseWord } from 'utils/capitalize' import { formatDate } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' @@ -128,13 +129,13 @@ const ProgramDetailsPage = () => { } const programDetails = [ - { label: 'Status', value: upperFirst(program.status) }, + { label: 'Status', value: titleCaseWord(program.status) }, { label: 'Start Date', value: formatDate(program.startedAt) }, { label: 'End Date', value: formatDate(program.endedAt) }, { label: 'Mentees Limit', value: String(program.menteesLimit) }, { label: 'Experience Levels', - value: program.experienceLevels?.join(', ') || 'N/A', + value: program.experienceLevels?.map((level) => titleCaseWord(level)).join(', ') || 'N/A', }, ] diff --git a/frontend/src/components/ActionButton.tsx b/frontend/src/components/ActionButton.tsx index 3acdb09e54..c31d74d1c1 100644 --- a/frontend/src/components/ActionButton.tsx +++ b/frontend/src/components/ActionButton.tsx @@ -12,7 +12,7 @@ interface ActionButtonProps { const ActionButton: React.FC = ({ url, onClick, tooltipLabel, children }) => { const baseStyles = - 'flex items-center gap-2 px-2 py-2 rounded-md border border-[#1D7BD7] transition-all whitespace-nowrap justify-center bg-transparent text-blue-600 hover:bg-[#1D7BD7] hover:text-white dark:hover:text-white' + 'flex items-center gap-2 px-2 py-2 rounded-md border border-[#1D7BD7] transition-all whitespace-nowrap justify-center bg-transparent text-[#1D7BD7] hover:bg-[#1D7BD7] hover:text-white dark:hover:text-white' return url ? ( diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 5f872a0ed5..abd87f136a 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -85,7 +85,7 @@ const DetailsCard = ({ ) && ( + {loading ? 'Saving...' : submitText} diff --git a/frontend/src/components/ProgramCard.tsx b/frontend/src/components/ProgramCard.tsx index d6b335e3d6..719c83f8aa 100644 --- a/frontend/src/components/ProgramCard.tsx +++ b/frontend/src/components/ProgramCard.tsx @@ -1,6 +1,7 @@ import { faEye } from '@fortawesome/free-regular-svg-icons' import { faEdit } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Tooltip } from '@heroui/tooltip' import type React from 'react' import { Program } from 'types/mentorship' import ActionButton from 'components/ActionButton' @@ -26,19 +27,26 @@ const ProgramCard: React.FC = ({ program, onEdit, onView, acce default: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200', } - const description = - program.description?.length > 100 - ? `${program.description.slice(0, 100)}...` - : program.description || 'No description available.' + const description = program.description || 'No description available.' return ( -
-
-
+
+
+
-

- {program.name} -

+ 50 ? false : true} + > +

+ {program.name} +

+
{accessLevel === 'admin' && ( = ({ program, onEdit, onView, acce )}
-
+
{program.startedAt && program.endedAt ? `${formatDate(program.startedAt)} – ${formatDate(program.endedAt)}` : program.startedAt ? `Started: ${formatDate(program.startedAt)}` : 'No dates set'}
-

{description}

+
+

+ {description} +

+
-
+
{accessLevel === 'admin' ? ( <> onView(program.key)}> diff --git a/frontend/src/components/ProgramForm.tsx b/frontend/src/components/ProgramForm.tsx index fe2ce19def..b03d93ee85 100644 --- a/frontend/src/components/ProgramForm.tsx +++ b/frontend/src/components/ProgramForm.tsx @@ -218,7 +218,7 @@ const ProgramForm = ({ diff --git a/frontend/src/components/SingleModuleCard.tsx b/frontend/src/components/SingleModuleCard.tsx index 60a7127f0a..ade79a3e8d 100644 --- a/frontend/src/components/SingleModuleCard.tsx +++ b/frontend/src/components/SingleModuleCard.tsx @@ -35,11 +35,6 @@ const SingleModuleCard: React.FC = ({ accessLevel === 'admin' && admins?.some((admin) => admin.login === ((data as ExtendedSession)?.user?.login as string)) - const handleView = () => { - setDropdownOpen(false) - router.push(`${window.location.pathname}/modules/${module.key}`) - } - const handleEdit = () => { setDropdownOpen(false) router.push(`${window.location.pathname}/modules/${module.key}/edit`) @@ -89,41 +84,37 @@ const SingleModuleCard: React.FC = ({
-
- + {isAdmin && ( +
+ - {dropdownOpen && ( -
- - {showEdit && isAdmin && ( - - )} - {isAdmin && ( - - )} -
- )} -
+ {dropdownOpen && ( +
+ {showEdit && isAdmin && ( + + )} + {isAdmin && ( + + )} +
+ )} +
+ )}
{/* Description */}