diff --git a/backend/apps/mentorship/api/internal/queries/mentorship.py b/backend/apps/mentorship/api/internal/queries/mentorship.py index 5f96abb035..cab13ed8d5 100644 --- a/backend/apps/mentorship/api/internal/queries/mentorship.py +++ b/backend/apps/mentorship/api/internal/queries/mentorship.py @@ -48,15 +48,13 @@ def is_mentor(self, login: str) -> bool: return Mentor.objects.filter(github_user=github_user).exists() @strawberry.field - def get_mentee_details( - self, program_key: str, module_key: str, mentee_handle: str - ) -> MenteeNode: + def get_mentee_details(self, program_key: str, module_key: str, mentee_key: str) -> MenteeNode: """Get detailed information about a mentee in a specific module.""" try: module = Module.objects.only("id").get(key=module_key, program__key=program_key) github_user = GithubUser.objects.only("login", "name", "avatar_url", "bio").get( - login=mentee_handle + login=mentee_key ) mentee = Mentee.objects.only("id", "experience_level", "domains", "tags").get( @@ -65,7 +63,7 @@ def get_mentee_details( is_enrolled = MenteeModule.objects.filter(mentee=mentee, module=module).exists() if not is_enrolled: - message = f"Mentee {mentee_handle} is not enrolled in module {module_key}" + message = f"Mentee {mentee_key} is not enrolled in module {module_key}" raise ObjectDoesNotExist(message) return MenteeNode( @@ -88,7 +86,7 @@ def get_mentee_module_issues( self, program_key: str, module_key: str, - mentee_handle: str, + mentee_key: str, limit: int = 20, offset: int = 0, ) -> list[IssueNode]: @@ -96,13 +94,13 @@ def get_mentee_module_issues( try: module = Module.objects.only("id").get(key=module_key, program__key=program_key) - github_user = GithubUser.objects.only("id").get(login=mentee_handle) + github_user = GithubUser.objects.only("id").get(login=mentee_key) mentee = Mentee.objects.only("id").get(github_user=github_user) is_enrolled = MenteeModule.objects.filter(mentee=mentee, module=module).exists() if not is_enrolled: - message = f"Mentee {mentee_handle} is not enrolled in module {module_key}" + message = f"Mentee {mentee_key} is not enrolled in module {module_key}" raise ObjectDoesNotExist(message) issues_qs = ( diff --git a/frontend/__tests__/unit/components/ProgramActions.test.tsx b/frontend/__tests__/unit/components/ProgramActions.test.tsx deleted file mode 100644 index 043b9d401d..0000000000 --- a/frontend/__tests__/unit/components/ProgramActions.test.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { fireEvent, screen } from '@testing-library/react' -import '@testing-library/jest-dom' -import { useSession as mockUseSession } from 'next-auth/react' -import { render } from 'wrappers/testUtil' -import { ProgramStatusEnum } from 'types/__generated__/graphql' -import ProgramActions from 'components/ProgramActions' - -const mockPush = jest.fn() -jest.mock('next/navigation', () => ({ - ...jest.requireActual('next/navigation'), - useRouter: () => ({ push: mockPush }), -})) - -jest.mock('next-auth/react', () => { - const actual = jest.requireActual('next-auth/react') - return { - ...actual, - useSession: jest.fn(), - } -}) - -describe('ProgramActions', () => { - let setStatus: jest.Mock - beforeEach(() => { - setStatus = jest.fn() - mockPush.mockClear() - }) - - beforeAll(async () => { - ;(mockUseSession as jest.Mock).mockReturnValue({ - data: { - user: { - name: 'Test User', - email: 'test@example.com', - login: 'testuser', - isLeader: true, - }, - expires: '2099-01-01T00:00:00.000Z', - }, - status: 'authenticated', - loading: false, - }) - }) - - test('renders and toggles dropdown', () => { - render() - const button = screen.getByTestId('program-actions-button') - fireEvent.click(button) - expect(screen.getByText('Add Module')).toBeInTheDocument() - expect(screen.getByText('Publish Program')).toBeInTheDocument() - fireEvent.click(button) - expect(screen.queryByText('Add Module')).not.toBeInTheDocument() - }) - - test('handles Add Module action', () => { - render() - const button = screen.getByTestId('program-actions-button') - fireEvent.click(button) - fireEvent.click(screen.getByRole('menuitem', { name: /add module/i })) - expect(mockPush).toHaveBeenCalled() - expect(setStatus).not.toHaveBeenCalled() - }) - - test('handles Publish action', () => { - render() - const button = screen.getByTestId('program-actions-button') - fireEvent.click(button) - fireEvent.click(screen.getByRole('menuitem', { name: /publish program/i })) - expect(setStatus).toHaveBeenCalledWith(ProgramStatusEnum.Published) - expect(mockPush).not.toHaveBeenCalled() - }) - - test('handles Move to Draft action', () => { - render() - const button = screen.getByTestId('program-actions-button') - fireEvent.click(button) - fireEvent.click(screen.getByRole('menuitem', { name: /move to draft/i })) - expect(setStatus).toHaveBeenCalledWith(ProgramStatusEnum.Draft) - }) - - test('handles Mark as Completed action', () => { - render() - const button = screen.getByTestId('program-actions-button') - fireEvent.click(button) - fireEvent.click(screen.getByRole('menuitem', { name: /mark as completed/i })) - expect(setStatus).toHaveBeenCalledWith(ProgramStatusEnum.Completed) - }) - - test('dropdown closes on outside click', () => { - render( -
- - -
- ) - const button = screen.getByTestId('program-actions-button') - fireEvent.click(button) - expect(screen.getByText('Add Module')).toBeInTheDocument() - fireEvent.mouseDown(screen.getByTestId('outside')) - expect(screen.queryByText('Add Module')).not.toBeInTheDocument() - }) -}) diff --git a/frontend/__tests__/unit/components/ProgramCard.test.tsx b/frontend/__tests__/unit/components/ProgramCard.test.tsx index 288f0074ef..3dfc2177d1 100644 --- a/frontend/__tests__/unit/components/ProgramCard.test.tsx +++ b/frontend/__tests__/unit/components/ProgramCard.test.tsx @@ -1,12 +1,16 @@ import { faEye } from '@fortawesome/free-regular-svg-icons' import { faEdit } from '@fortawesome/free-solid-svg-icons' -import { fireEvent, render, screen } from '@testing-library/react' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { useRouter } from 'next/navigation' import React from 'react' import { ProgramStatusEnum } from 'types/__generated__/graphql' import type { Program } from 'types/mentorship' import ProgramCard from 'components/ProgramCard' +jest.mock('next/navigation', () => ({ + useRouter: jest.fn(), +})) + jest.mock('@fortawesome/react-fontawesome', () => ({ FontAwesomeIcon: ({ icon, className }: { icon: unknown; className?: string }) => { let iconName = 'unknown' @@ -46,8 +50,28 @@ jest.mock('@heroui/tooltip', () => ({ ), })) +jest.mock('components/EntityActions', () => jest.requireActual('components/EntityActions')) + +jest.mock('next/link', () => { + return ({ children, href }: { children: React.ReactNode; href: string }) => { + return {children} + } +}) + describe('ProgramCard', () => { - const mockOnView = jest.fn() + const mockPush = jest.fn() + + beforeEach(() => { + jest.clearAllMocks() + ;(useRouter as jest.Mock).mockReturnValue({ + push: mockPush, + back: jest.fn(), + forward: jest.fn(), + refresh: jest.fn(), + replace: jest.fn(), + prefetch: jest.fn(), + }) + }) const baseMockProgram: Program = { id: '1', @@ -60,17 +84,13 @@ describe('ProgramCard', () => { userRole: 'admin', } - beforeEach(() => { - jest.clearAllMocks() - }) - describe('Basic Rendering', () => { it('renders program name correctly', () => { render( ) @@ -83,7 +103,7 @@ describe('ProgramCard', () => { ) @@ -97,7 +117,7 @@ describe('ProgramCard', () => { render( @@ -106,38 +126,45 @@ describe('ProgramCard', () => { expect(screen.getByText('admin')).toBeInTheDocument() }) - it('calls onView when Preview button is clicked', () => { - render( + it('renders Link with correct href', () => { + const { container } = render( ) - const previewButton = screen.getByText('Preview').closest('button') - fireEvent.click(previewButton!) - - expect(mockOnView).toHaveBeenCalledWith('test-program') + const link = container.querySelector('a[href="/my/mentorship/programs/test-program"]') + expect(link).toBeInTheDocument() }) - it('navigates to edit page when Edit Program is clicked', () => { - const router = useRouter() - + it('navigates to edit page when Edit is clicked', async () => { render( ) - fireEvent.click(screen.getByTestId('program-actions-button')) - fireEvent.click(screen.getByText('Edit Program')) + const actionsButton = screen.getByTestId('program-actions-button') + + await act(async () => { + fireEvent.click(actionsButton) + }) - expect(router.push).toHaveBeenCalledWith('/my/mentorship/programs/test-program/edit') + const editButton = await waitFor(() => { + return screen.getByText('Edit') + }) + + await act(async () => { + fireEvent.click(editButton) + }) + + expect(mockPush).toHaveBeenCalledWith('/my/mentorship/programs/test-program/edit') }) }) @@ -147,7 +174,7 @@ describe('ProgramCard', () => { ) @@ -155,35 +182,35 @@ describe('ProgramCard', () => { expect(screen.queryByText('admin')).not.toBeInTheDocument() }) - it('shows only View Details button for user access', () => { + it('shows clickable card for user access', () => { render( ) - expect(screen.getByText('View Details')).toBeInTheDocument() + const link = document.querySelector('a[href="/test/path"]') + expect(link).toBeInTheDocument() expect(screen.queryByText('Preview')).not.toBeInTheDocument() expect(screen.queryByText('Edit')).not.toBeInTheDocument() + expect(screen.queryByText('View Details')).not.toBeInTheDocument() }) - it('calls onView when View Details button is clicked', () => { - render( + it('renders Link with correct href', () => { + const { container } = render( ) - const viewButton = screen.getByText('View Details').closest('button') - fireEvent.click(viewButton!) - - expect(mockOnView).toHaveBeenCalledWith('test-program') + const link = container.querySelector('a[href="/mentorship/programs/test-program"]') + expect(link).toBeInTheDocument() }) }) @@ -191,12 +218,7 @@ describe('ProgramCard', () => { it('applies admin role styling', () => { const adminProgram = { ...baseMockProgram, userRole: 'admin' } render( - + ) const badge = screen.getByText('admin') @@ -206,12 +228,7 @@ describe('ProgramCard', () => { it('applies mentor role styling', () => { const mentorProgram = { ...baseMockProgram, userRole: 'mentor' } render( - + ) const badge = screen.getByText('mentor') @@ -224,7 +241,7 @@ describe('ProgramCard', () => { ) @@ -236,12 +253,7 @@ describe('ProgramCard', () => { it('applies default styling when userRole is undefined', () => { const noRoleProgram = { ...baseMockProgram, userRole: undefined } render( - + ) // Should not render badge when userRole is undefined @@ -250,7 +262,7 @@ describe('ProgramCard', () => { }) describe('Description Handling', () => { - it('renders long descriptions with line-clamp-6 CSS class', () => { + it('renders long descriptions with line-clamp-8 CSS class', () => { const longDescription = 'A'.repeat(300) // Long enough to trigger line clamping const longDescProgram = { ...baseMockProgram, description: longDescription } @@ -258,7 +270,7 @@ describe('ProgramCard', () => { ) @@ -266,7 +278,7 @@ describe('ProgramCard', () => { expect(screen.getByText(longDescription)).toBeInTheDocument() expect(screen.getByText(longDescription)).toBeInTheDocument() const descriptionElement = screen.getByText(longDescription) - expect(descriptionElement).toHaveClass('line-clamp-6') + expect(descriptionElement).toHaveClass('line-clamp-8') }) it('shows full description when short', () => { @@ -277,7 +289,7 @@ describe('ProgramCard', () => { ) @@ -285,7 +297,7 @@ describe('ProgramCard', () => { expect(screen.getByText('Short description')).toBeInTheDocument() const descriptionElement = screen.getByText('Short description') - expect(descriptionElement).toHaveClass('line-clamp-6') + expect(descriptionElement).toHaveClass('line-clamp-8') }) it('shows fallback text when description is empty', () => { @@ -295,7 +307,7 @@ describe('ProgramCard', () => { ) @@ -308,12 +320,7 @@ describe('ProgramCard', () => { const noDescProgram = { ...baseMockProgram, description: undefined as any } render( - + ) expect(screen.getByText('No description available.')).toBeInTheDocument() @@ -326,7 +333,7 @@ describe('ProgramCard', () => { ) @@ -342,7 +349,7 @@ describe('ProgramCard', () => { ) @@ -357,7 +364,7 @@ describe('ProgramCard', () => { ) @@ -372,7 +379,7 @@ describe('ProgramCard', () => { ) @@ -382,59 +389,32 @@ describe('ProgramCard', () => { }) describe('Icons', () => { - it('renders eye icon for Preview button', () => { - render( - - ) - - expect(screen.getByTestId('icon-eye')).toBeInTheDocument() - }) - it('renders actions button for admin menu', () => { render( ) expect(screen.getByTestId('program-actions-button')).toBeInTheDocument() }) - - it('renders eye icon for View Details button', () => { - render( - - ) - - expect(screen.getByTestId('icon-eye')).toBeInTheDocument() - }) }) describe('Edge Cases', () => { - it('shows Edit Program in actions menu for admin access', () => { + it('shows actions button for admin access', () => { render( ) - fireEvent.click(screen.getByTestId('program-actions-button')) - expect(screen.getByText('Edit Program')).toBeInTheDocument() + expect(screen.getByTestId('program-actions-button')).toBeInTheDocument() }) it('handles program with minimal data', () => { @@ -452,7 +432,7 @@ describe('ProgramCard', () => { ) diff --git a/frontend/__tests__/unit/components/SingleModuleCard.test.tsx b/frontend/__tests__/unit/components/SingleModuleCard.test.tsx index d56a026ca6..92fdd0e6f7 100644 --- a/frontend/__tests__/unit/components/SingleModuleCard.test.tsx +++ b/frontend/__tests__/unit/components/SingleModuleCard.test.tsx @@ -1,6 +1,6 @@ import { faUsers } from '@fortawesome/free-solid-svg-icons' import { screen } from '@testing-library/react' -import { useRouter } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' import React from 'react' import { render } from 'wrappers/testUtil' @@ -11,6 +11,7 @@ import SingleModuleCard from 'components/SingleModuleCard' // Mock dependencies jest.mock('next/navigation', () => ({ useRouter: jest.fn(), + usePathname: jest.fn(), })) jest.mock('next-auth/react', () => ({ @@ -80,6 +81,7 @@ jest.mock('components/TopContributorsList', () => ({ const mockPush = jest.fn() const mockUseRouter = useRouter as jest.MockedFunction +const mockUsePathname = usePathname as jest.MockedFunction const mockUseSession = useSession as jest.MockedFunction // Test data @@ -122,6 +124,7 @@ describe('SingleModuleCard', () => { replace: jest.fn(), prefetch: jest.fn(), }) + mockUsePathname.mockReturnValue('/my/mentorship/programs/test-program') mockUseSession.mockReturnValue({ data: null, status: 'unauthenticated', @@ -166,7 +169,10 @@ describe('SingleModuleCard', () => { render() const moduleLink = screen.getByTestId('module-link') - expect(moduleLink).toHaveAttribute('href', '//modules/test-module') + expect(moduleLink).toHaveAttribute( + 'href', + '/my/mentorship/programs/test-program/modules/test-module' + ) expect(moduleLink).toHaveAttribute('target', '_blank') expect(moduleLink).toHaveAttribute('rel', 'noopener noreferrer') }) @@ -183,7 +189,10 @@ describe('SingleModuleCard', () => { // Should have clickable title for navigation const moduleLink = screen.getByTestId('module-link') - expect(moduleLink).toHaveAttribute('href', '//modules/test-module') + expect(moduleLink).toHaveAttribute( + 'href', + '/my/mentorship/programs/test-program/modules/test-module' + ) }) }) @@ -197,14 +206,7 @@ describe('SingleModuleCard', () => { it('ignores admin-related props since menu is removed', () => { // These props are now ignored but should not cause errors - render( - - ) + render() expect(screen.getByText('Test Module')).toBeInTheDocument() }) @@ -225,7 +227,7 @@ describe('SingleModuleCard', () => { }) it('handles undefined admins array gracefully', () => { - render() + render() // Should render without errors even with admin props expect(screen.getByText('Test Module')).toBeInTheDocument() @@ -238,7 +240,10 @@ describe('SingleModuleCard', () => { const moduleLink = screen.getByTestId('module-link') expect(moduleLink).toBeInTheDocument() - expect(moduleLink).toHaveAttribute('href', '//modules/test-module') + expect(moduleLink).toHaveAttribute( + 'href', + '/my/mentorship/programs/test-program/modules/test-module' + ) expect(moduleLink).toHaveAttribute('target', '_blank') expect(moduleLink).toHaveAttribute('rel', 'noopener noreferrer') }) diff --git a/frontend/__tests__/unit/pages/CreateModule.test.tsx b/frontend/__tests__/unit/pages/CreateModule.test.tsx index 6368b8645e..a334de0790 100644 --- a/frontend/__tests__/unit/pages/CreateModule.test.tsx +++ b/frontend/__tests__/unit/pages/CreateModule.test.tsx @@ -66,7 +66,7 @@ describe('CreateModulePage', () => { render() // Fill all inputs - fireEvent.change(screen.getByLabelText(/Module Name/i), { + fireEvent.change(screen.getByLabelText('Name *'), { target: { value: 'My Test Module' }, }) fireEvent.change(screen.getByLabelText(/Description/i), { diff --git a/frontend/__tests__/unit/pages/CreateProgram.test.tsx b/frontend/__tests__/unit/pages/CreateProgram.test.tsx index b32394cec8..80ae5db284 100644 --- a/frontend/__tests__/unit/pages/CreateProgram.test.tsx +++ b/frontend/__tests__/unit/pages/CreateProgram.test.tsx @@ -60,7 +60,7 @@ describe('CreateProgramPage (comprehensive tests)', () => { render() - expect(screen.queryByLabelText('Program Name *')).not.toBeInTheDocument() + expect(screen.queryByLabelText('Name *')).not.toBeInTheDocument() }) test('redirects with toast if not a project leader', async () => { @@ -103,7 +103,7 @@ describe('CreateProgramPage (comprehensive tests)', () => { render() - expect(await screen.findByLabelText('Program Name *')).toBeInTheDocument() + expect(await screen.findByLabelText('Name *')).toBeInTheDocument() }) test('submits form and redirects on success', async () => { @@ -127,7 +127,7 @@ describe('CreateProgramPage (comprehensive tests)', () => { render() - fireEvent.change(screen.getByLabelText('Program Name *'), { + fireEvent.change(screen.getByLabelText('Name *'), { target: { value: 'Test Program' }, }) fireEvent.change(screen.getByLabelText('Description *'), { @@ -186,7 +186,7 @@ describe('CreateProgramPage (comprehensive tests)', () => { render() - fireEvent.change(screen.getByLabelText('Program Name *'), { + fireEvent.change(screen.getByLabelText('Name *'), { target: { value: 'Test Program' }, }) fireEvent.change(screen.getByLabelText('Description *'), { diff --git a/frontend/__tests__/unit/pages/EditModule.test.tsx b/frontend/__tests__/unit/pages/EditModule.test.tsx index b535b00259..e93e30bdd7 100644 --- a/frontend/__tests__/unit/pages/EditModule.test.tsx +++ b/frontend/__tests__/unit/pages/EditModule.test.tsx @@ -86,8 +86,8 @@ describe('EditModulePage', () => { expect(await screen.findByDisplayValue('Existing Module')).toBeInTheDocument() // Modify values - fireEvent.change(screen.getByLabelText(/Module Name/i), { - target: { value: 'Updated Module Name' }, + fireEvent.change(screen.getByLabelText('Name *'), { + target: { value: 'Updated Name' }, }) fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: 'Updated description' }, diff --git a/frontend/__tests__/unit/pages/EditProgram.test.tsx b/frontend/__tests__/unit/pages/EditProgram.test.tsx index a82e7a7bf6..ff4cc020ab 100644 --- a/frontend/__tests__/unit/pages/EditProgram.test.tsx +++ b/frontend/__tests__/unit/pages/EditProgram.test.tsx @@ -88,7 +88,7 @@ describe('EditProgramPage', () => { render() - expect(await screen.findByLabelText('Program Name *')).toBeInTheDocument() + expect(await screen.findByLabelText('Name *')).toBeInTheDocument() expect(screen.getByDisplayValue('Test')).toBeInTheDocument() }) }) diff --git a/frontend/__tests__/unit/pages/Program.test.tsx b/frontend/__tests__/unit/pages/Program.test.tsx index 68aef54469..f46b6107ab 100644 --- a/frontend/__tests__/unit/pages/Program.test.tsx +++ b/frontend/__tests__/unit/pages/Program.test.tsx @@ -61,7 +61,7 @@ describe('ProgramsPage Component', () => { }) expect(screen.getByText('This is a summary of Program 1.')).toBeInTheDocument() - expect(screen.getByText('View Details')).toBeInTheDocument() + // Card is now clickable, no separate "View Details" button }) test('shows empty message when no programs found', async () => { @@ -91,14 +91,14 @@ describe('ProgramsPage Component', () => { }) }) - test('navigates to program detail page on View Details click', async () => { + test('navigates to program detail page on card click', async () => { render() await waitFor(() => { - const viewButton = screen.getByText('View Details') - fireEvent.click(viewButton) + expect(screen.getByText('Program 1')).toBeInTheDocument() }) - expect(mockRouter.push).toHaveBeenCalledWith('/mentorship/programs/program_1') + const card = screen.getByRole('link', { name: /Program 1/i }) + expect(card).toHaveAttribute('href', '/mentorship/programs/program_1') }) }) 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 c986ddf5ce..c6a7ad7b00 100644 --- a/frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx +++ b/frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useQuery } from '@apollo/client/react' -import upperFirst from 'lodash/upperFirst' +import capitalize from 'lodash/capitalize' import { useParams } from 'next/navigation' import { useEffect, useState } from 'react' import { ErrorDisplay, handleAppError } from 'app/global-error' @@ -49,7 +49,7 @@ const ModuleDetailsPage = () => { } const moduleDetails = [ - { label: 'Experience Level', value: upperFirst(module.experienceLevel) }, + { label: 'Experience Level', value: capitalize(module.experienceLevel) }, { label: 'Start Date', value: formatDate(module.startedAt) }, { label: 'End Date', value: formatDate(module.endedAt) }, { @@ -63,7 +63,6 @@ const ModuleDetailsPage = () => { admins={admins} details={moduleDetails} domains={module.domains} - labels={module.labels} mentors={module.mentors} summary={module.description} tags={module.tags} diff --git a/frontend/src/app/mentorship/programs/[programKey]/page.tsx b/frontend/src/app/mentorship/programs/[programKey]/page.tsx index a80f54e179..23f25d0451 100644 --- a/frontend/src/app/mentorship/programs/[programKey]/page.tsx +++ b/frontend/src/app/mentorship/programs/[programKey]/page.tsx @@ -13,7 +13,7 @@ import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' const ProgramDetailsPage = () => { - const { programKey } = useParams() as { programKey: string } + const { programKey } = useParams<{ programKey: string }>() const searchParams = useSearchParams() const router = useRouter() const shouldRefresh = searchParams.get('refresh') === 'true' diff --git a/frontend/src/app/mentorship/programs/page.tsx b/frontend/src/app/mentorship/programs/page.tsx index d02205b9b3..56ac5c4cd5 100644 --- a/frontend/src/app/mentorship/programs/page.tsx +++ b/frontend/src/app/mentorship/programs/page.tsx @@ -1,7 +1,6 @@ 'use client' import { useSearchPage } from 'hooks/useSearchPage' -import { useRouter } from 'next/navigation' import { ProgramStatusEnum } from 'types/__generated__/graphql' import { Program } from 'types/mentorship' import ProgramCard from 'components/ProgramCard' @@ -22,19 +21,13 @@ const ProgramsPage = () => { hitsPerPage: 24, }) - const router = useRouter() - const renderProgramCard = (program: Program) => { - const handleButtonClick = () => { - router.push(`/mentorship/programs/${program.key}`) - } - return ( ) @@ -54,7 +47,9 @@ const ProgramsPage = () => { >
{programs && - programs.filter((p) => p.status === ProgramStatusEnum.Published).map(renderProgramCard)} + programs + .filter((p) => p.status?.toUpperCase() === ProgramStatusEnum.Published) + .map(renderProgramCard)}
) diff --git a/frontend/src/app/my/mentorship/page.tsx b/frontend/src/app/my/mentorship/page.tsx index 8a93993fa7..04bb8a0093 100644 --- a/frontend/src/app/my/mentorship/page.tsx +++ b/frontend/src/app/my/mentorship/page.tsx @@ -81,7 +81,6 @@ const MyMentorshipPage: React.FC = () => { }, [error]) const handleCreate = () => router.push('/my/mentorship/programs/create') - const handleView = (key: string) => router.push(`/my/mentorship/programs/${key}`) if (!username) { return @@ -139,7 +138,7 @@ const MyMentorshipPage: React.FC = () => { accessLevel="admin" isAdmin={p?.userRole === 'admin'} key={p.id} - onView={handleView} + href={`/my/mentorship/programs/${p.key}`} program={p} /> )) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx index d4d0b8bc0b..d323bb943b 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx @@ -17,7 +17,7 @@ import LoadingSpinner from 'components/LoadingSpinner' import ProgramForm from 'components/ProgramForm' const EditProgramPage = () => { const router = useRouter() - const { programKey } = useParams() as { programKey: string } + const { programKey } = useParams<{ programKey: string }>() const { data: session, status: sessionStatus } = useSession() const [updateProgram, { loading: mutationLoading }] = useMutation(UpdateProgramDocument) const { diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/edit/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/edit/page.tsx index 18d30c86c3..d1b294d107 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/edit/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/edit/page.tsx @@ -16,7 +16,7 @@ import LoadingSpinner from 'components/LoadingSpinner' import ModuleForm from 'components/ModuleForm' const EditModulePage = () => { - const { programKey, moduleKey } = useParams() as { programKey: string; moduleKey: string } + const { programKey, moduleKey } = useParams<{ programKey: string; moduleKey: string }>() const router = useRouter() const { data: sessionData, status: sessionStatus } = useSession() diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index cb406b4704..322ef9f27b 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -24,7 +24,7 @@ import SecondaryCard from 'components/SecondaryCard' import { TruncatedText } from 'components/TruncatedText' const ModuleIssueDetailsPage = () => { - const params = useParams() as { programKey: string; moduleKey: string; issueId: string } + const params = useParams<{ programKey: string; moduleKey: string; issueId: string }>() const { programKey, moduleKey, issueId } = params const formatDeadline = (deadline: string | null) => { @@ -266,7 +266,7 @@ const ModuleIssueDetailsPage = () => { className="flex items-center justify-between gap-2 rounded-lg bg-gray-200 p-3 dark:bg-gray-700" > {a.avatarUrl ? ( diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index d24ea54c66..d5583561f3 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -16,7 +16,7 @@ const LABEL_ALL = 'all' const MAX_VISIBLE_LABELS = 5 const IssuesPage = () => { - const { programKey, moduleKey } = useParams() as { programKey: string; moduleKey: string } + const { programKey, moduleKey } = useParams<{ programKey: string; moduleKey: string }>() const router = useRouter() const searchParams = useSearchParams() const [selectedLabel, setSelectedLabel] = useState(searchParams.get('label') || LABEL_ALL) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeHandle]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeKey]/page.tsx similarity index 95% rename from frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeHandle]/page.tsx rename to frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeKey]/page.tsx index 50eb297b0d..5a8864ea97 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeHandle]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeKey]/page.tsx @@ -13,11 +13,11 @@ import LoadingSpinner from 'components/LoadingSpinner' import SecondaryCard from 'components/SecondaryCard' const MenteeProfilePage = () => { - const { programKey, moduleKey, menteeHandle } = useParams() as { + const { programKey, moduleKey, menteeKey } = useParams<{ programKey: string moduleKey: string - menteeHandle: string - } + menteeKey: string + }>() const [menteeDetails, setMenteeDetails] = useState(null) const [menteeIssues, setMenteeIssues] = useState([]) @@ -29,9 +29,9 @@ const MenteeProfilePage = () => { variables: { programKey, moduleKey, - menteeHandle, + menteeKey, }, - skip: !programKey || !moduleKey || !menteeHandle, + skip: !programKey || !moduleKey || !menteeKey, fetchPolicy: 'cache-and-network', }) @@ -63,8 +63,12 @@ const MenteeProfilePage = () => { const openIssues = menteeIssues.filter((issue) => issue.state.toLowerCase() === 'open') const closedIssues = menteeIssues.filter((issue) => issue.state.toLowerCase() === 'closed') - const filteredIssues = - statusFilter === 'all' ? menteeIssues : statusFilter === 'open' ? openIssues : closedIssues + const issueMap: Record = { + all: menteeIssues, + open: openIssues, + closed: closedIssues, + } + const filteredIssues = issueMap[statusFilter] || closedIssues return (
diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx index 957aba1023..989c30deae 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx @@ -12,7 +12,7 @@ import LoadingSpinner from 'components/LoadingSpinner' import { getSimpleDuration } from 'components/ModuleCard' const ModuleDetailsPage = () => { - const { programKey, moduleKey } = useParams() as { programKey: string; moduleKey: string } + const { programKey, moduleKey } = useParams<{ programKey: string; moduleKey: string }>() const [module, setModule] = useState(null) const [admins, setAdmins] = useState(null) const [isLoading, setIsLoading] = useState(true) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/create/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/create/page.tsx index 57b385b09e..a0666f8603 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/create/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/create/page.tsx @@ -18,7 +18,7 @@ import ModuleForm from 'components/ModuleForm' const CreateModulePage = () => { const router = useRouter() - const { programKey } = useParams() as { programKey: string } + const { programKey } = useParams<{ programKey: string }>() const { data: sessionData, status: sessionStatus } = useSession() const [createModule, { loading: mutationLoading }] = useMutation(CreateModuleDocument) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx index a605c27597..e25859a092 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx @@ -17,7 +17,7 @@ import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' const ProgramDetailsPage = () => { - const { programKey } = useParams() as { programKey: string } + const { programKey } = useParams<{ programKey: string }>() const { data: session } = useSession() const username = (session as ExtendedSession)?.user?.login diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 811c6b0f16..ab5d4a2314 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -9,7 +9,6 @@ import { } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import upperFirst from 'lodash/upperFirst' -import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' import type { ExtendedSession } from 'types/auth' import type { DetailsCardProps } from 'types/card' @@ -18,6 +17,7 @@ import { scrollToAnchor } from 'utils/scrollToAnchor' import { getSocialIcon } from 'utils/urlIconMappings' import AnchorTitle from 'components/AnchorTitle' import ChapterMapWrapper from 'components/ChapterMapWrapper' +import EntityActions from 'components/EntityActions' import HealthMetrics from 'components/HealthMetrics' import InfoBlock from 'components/InfoBlock' import Leaders from 'components/Leaders' @@ -26,7 +26,6 @@ import MenteeContributorsList from 'components/MenteeContributorsList' import MetricsScoreCircle from 'components/MetricsScoreCircle' import Milestones from 'components/Milestones' import ModuleCard from 'components/ModuleCard' -import ProgramActions from 'components/ProgramActions' import RecentIssues from 'components/RecentIssues' import RecentPullRequests from 'components/RecentPullRequests' import RecentReleases from 'components/RecentReleases' @@ -76,7 +75,6 @@ const DetailsCard = ({ userSummary, }: DetailsCardProps) => { const { data } = useSession() - const router = useRouter() // compute styles based on type prop const secondaryCardStyles = (() => { @@ -94,36 +92,20 @@ const DetailsCard = ({

{title}

- {type === 'program' && accessLevel === 'admin' && canUpdateStatus && ( - - )} - {type === 'module' && - accessLevel === 'admin' && - admins?.some( - (admin) => admin.login === ((data as ExtendedSession)?.user?.login as string) - ) && ( -
- - -
- )}
+ {type === 'program' && accessLevel === 'admin' && canUpdateStatus && ( + + )} + {type === 'module' && + accessLevel === 'admin' && + admins?.some( + (admin) => admin.login === ((data as ExtendedSession)?.user?.login as string) + ) && } {!isActive && } {isArchived && type === 'repository' && } {IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && ( @@ -223,26 +205,28 @@ const DetailsCard = ({ )} {(type === 'program' || type === 'module') && ( <> -
- {tags?.length > 0 && ( - } - isDisabled={true} - /> - )} - {domains?.length > 0 && ( - } - isDisabled={true} - /> - )} -
+ {((tags?.length || 0) > 0 || (domains?.length || 0) > 0) && ( +
+ {tags?.length > 0 && ( + } + isDisabled={true} + /> + )} + {domains?.length > 0 && ( + } + isDisabled={true} + /> + )} +
+ )} {labels?.length > 0 && (
void +} + +const EntityActions: React.FC = ({ + type, + programKey, + moduleKey, + status, + setStatus, +}) => { + const router = useRouter() + const [dropdownOpen, setDropdownOpen] = useState(false) + const dropdownRef = useRef(null) + + const handleAction = (actionKey: string) => { + switch (actionKey) { + case 'edit_program': + router.push(`/my/mentorship/programs/${programKey}/edit`) + break + case 'create_module': + router.push(`/my/mentorship/programs/${programKey}/modules/create`) + break + case 'edit_module': + if (moduleKey) { + router.push(`/my/mentorship/programs/${programKey}/modules/${moduleKey}/edit`) + } + break + case 'view_issues': + if (moduleKey) { + router.push(`/my/mentorship/programs/${programKey}/modules/${moduleKey}/issues`) + } + break + case 'publish': + setStatus?.(ProgramStatusEnum.Published) + break + case 'draft': + setStatus?.(ProgramStatusEnum.Draft) + break + case 'completed': + setStatus?.(ProgramStatusEnum.Completed) + break + } + setDropdownOpen(false) + } + + const options = + type === 'program' + ? [ + { key: 'edit_program', label: 'Edit' }, + { key: 'create_module', label: 'Add Module' }, + ...(status === ProgramStatusEnum.Draft ? [{ key: 'publish', label: 'Publish' }] : []), + ...(status === ProgramStatusEnum.Published || status === ProgramStatusEnum.Completed + ? [{ key: 'draft', label: 'Unpublish' }] + : []), + ...(status === ProgramStatusEnum.Published + ? [{ key: 'completed', label: 'Mark as Completed' }] + : []), + ] + : [ + { key: 'edit_module', label: 'Edit' }, + { key: 'view_issues', label: 'View Issues' }, + ] + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setDropdownOpen(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + + const handleToggle = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + setDropdownOpen((prev) => !prev) + } + + return ( +
+ + {dropdownOpen && ( +
+ {options.map((option) => { + const handleMenuItemClick = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + handleAction(option.key) + } + + return ( + + ) + })} +
+ )} +
+ ) +} + +export default EntityActions diff --git a/frontend/src/components/ModuleCard.tsx b/frontend/src/components/ModuleCard.tsx index 66c71f8d54..75669f283d 100644 --- a/frontend/src/components/ModuleCard.tsx +++ b/frontend/src/components/ModuleCard.tsx @@ -27,23 +27,17 @@ const ModuleCard = ({ modules, accessLevel, admins }: ModuleCardProps) => { const [showAllModule, setShowAllModule] = useState(false) if (modules.length === 1) { - return ( - - ) + return } const displayedModule = showAllModule ? modules : modules.slice(0, 4) + const isAdmin = accessLevel === 'admin' return (
{displayedModule.map((module) => { - return + return })}
{modules.length > 4 && ( @@ -69,26 +63,26 @@ const ModuleCard = ({ modules, accessLevel, admins }: ModuleCardProps) => { ) } -const ModuleItem = ({ details }: { details: Module }) => { +const ModuleItem = ({ module, isAdmin }: { module: Module; isAdmin: boolean }) => { const pathname = usePathname() return (
- + - - + + - {details.labels && details.labels.length > 0 && ( + {isAdmin && module.labels && module.labels.length > 0 && (
- +
)}
diff --git a/frontend/src/components/ModuleForm.tsx b/frontend/src/components/ModuleForm.tsx index ace8d99723..0768291a4e 100644 --- a/frontend/src/components/ModuleForm.tsx +++ b/frontend/src/components/ModuleForm.tsx @@ -63,13 +63,10 @@ const ModuleForm = ({
-

- Module Information -

-

- Module Configuration -

-

- Additional Details -