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 = ({