diff --git a/frontend/__tests__/mockData/mockCommitteeDetailsData.ts b/frontend/__tests__/mockData/mockCommitteeDetailsData.ts index ec8d11ec84..15ba8501f7 100644 --- a/frontend/__tests__/mockData/mockCommitteeDetailsData.ts +++ b/frontend/__tests__/mockData/mockCommitteeDetailsData.ts @@ -3,6 +3,7 @@ export const mockCommitteeDetailsData = { contributorsCount: 10, forksCount: 5, issuesCount: 3, + key: 'test_committee', leaders: ['Leader 1', 'Leader 2'], name: 'Test Committee', relatedUrls: ['https://twitter.com/testcommittee', 'https://github.com/testcommittee'], diff --git a/frontend/__tests__/mockData/mockOrganizationData.ts b/frontend/__tests__/mockData/mockOrganizationData.ts index 49bd182716..19c621e429 100644 --- a/frontend/__tests__/mockData/mockOrganizationData.ts +++ b/frontend/__tests__/mockData/mockOrganizationData.ts @@ -102,6 +102,7 @@ export const mockOrganizationDetailsData = { tagName: 'v1.0.0', publishedAt: 1727390000, url: 'https://github.com/test-org/test-repo-1/releases/tag/v1.0.0', + organizationName: 'test-org', repositoryName: 'test-repo-1', author: { login: 'user1', @@ -113,6 +114,7 @@ export const mockOrganizationDetailsData = { tagName: 'v2.0.0', publishedAt: 1727380000, url: 'https://github.com/test-org/test-repo-2/releases/tag/v2.0.0', + organizationName: 'test-org', repositoryName: 'test-repo-2', author: { login: 'user2', diff --git a/frontend/__tests__/unit/components/ItemCardList.test.tsx b/frontend/__tests__/unit/components/ItemCardList.test.tsx index d5c9ba522c..46f60eaf72 100644 --- a/frontend/__tests__/unit/components/ItemCardList.test.tsx +++ b/frontend/__tests__/unit/components/ItemCardList.test.tsx @@ -537,10 +537,81 @@ describe('ItemCardList Component', () => { /> ) + // When URL is missing, the title should render as plain text, not a link. + const titleText = screen.getByText('Test Issue Title') + expect(titleText).toBeInTheDocument() + + // Verify it's not wrapped in a link. + const titleLinks = screen.queryAllByTestId('link') + const titleLink = titleLinks.find((link) => link.textContent?.includes('Test Issue Title')) + expect(titleLink).toBeUndefined() + }) + + it('renders title as link when valid url is present', () => { + const itemWithUrl = { ...mockIssue, url: 'https://example.com/issue' } + + render( + + ) + const titleLinks = screen.getAllByTestId('link') const titleLink = titleLinks.find((link) => link.textContent?.includes('Test Issue Title')) + expect(titleLink).toHaveAttribute('href', 'https://example.com/issue') + }) + + it('uses id as key when objectID is not present', () => { + const itemWithId = { ...mockIssue, objectID: undefined, id: 'test-id-123' } + + render( + + ) + + expect(screen.getByText('Test Issue Title')).toBeInTheDocument() + }) + + it('generates fallback key when no identifiers are present', () => { + const itemWithoutIds = { + ...mockIssue, + objectID: undefined, + id: undefined, + repositoryName: '', + title: '', + name: '', + url: '', + } + + render( + + ) + + // Component should render without crashing + expect(screen.getByText('Fallback Key')).toBeInTheDocument() + }) + + it('uses objectID as primary key identifier', () => { + const itemWithObjectId = { ...mockIssue, objectID: 'object-123' } + + render( + + ) - expect(titleLink).toHaveAttribute('href', '') + expect(screen.getByText('Test Issue Title')).toBeInTheDocument() }) }) diff --git a/frontend/__tests__/unit/components/SortBy.test.tsx b/frontend/__tests__/unit/components/SortBy.test.tsx index fe12b25e66..0cc5770454 100644 --- a/frontend/__tests__/unit/components/SortBy.test.tsx +++ b/frontend/__tests__/unit/components/SortBy.test.tsx @@ -94,4 +94,36 @@ describe('', () => { expect(container).toBeInTheDocument() expect(hiddenSelect).toHaveAccessibleName(/Sort By/) }) + + it('toggles order when Enter key is pressed on sort order button', async () => { + await act(async () => { + render() + }) + await act(async () => { + const buttons = screen.getAllByRole('button') + const orderButton = buttons[1] + fireEvent.keyDown(orderButton, { key: 'Enter' }) + }) + expect(defaultProps.onOrderChange).toHaveBeenCalledWith('desc') + }) + + it('toggles order when Space key is pressed on sort order button', async () => { + await act(async () => { + render() + }) + await act(async () => { + const buttons = screen.getAllByRole('button') + const orderButton = buttons[1] + fireEvent.keyDown(orderButton, { key: ' ' }) + }) + expect(defaultProps.onOrderChange).toHaveBeenCalledWith('asc') + }) + + it('does not render order button when selectedSortOption is default', async () => { + await act(async () => { + render() + }) + const sortOrderButton = screen.queryByLabelText(/Sort in/i) + expect(sortOrderButton).not.toBeInTheDocument() + }) }) diff --git a/frontend/__tests__/unit/components/ToggleableList.test.tsx b/frontend/__tests__/unit/components/ToggleableList.test.tsx index 32a45f4c8a..e6a116f8a0 100644 --- a/frontend/__tests__/unit/components/ToggleableList.test.tsx +++ b/frontend/__tests__/unit/components/ToggleableList.test.tsx @@ -201,4 +201,72 @@ describe('ToggleableList', () => { 'hover:scale-105' ) }) + + it('handles Enter key press on item button', () => { + render() + const button = screen.getByText('React') + fireEvent.keyDown(button, { key: 'Enter' }) + expect(mockPush).toHaveBeenCalledWith('/projects?q=React') + }) + + it('handles Space key press on item button', () => { + render() + const button = screen.getByText('Vue') + fireEvent.keyDown(button, { key: ' ' }) + expect(mockPush).toHaveBeenCalledWith('/projects?q=Vue') + }) + + it('prevents default behavior on keyboard event', () => { + render() + const button = screen.getByText('React') + fireEvent.keyDown(button, { key: 'Enter' }) + expect(mockPush).toHaveBeenCalled() + }) + + it('does not navigate when button is disabled and clicked', () => { + render( + + ) + const button = screen.getByText('React') + fireEvent.click(button) + expect(mockPush).not.toHaveBeenCalled() + }) + + it('does not navigate when button is disabled and Enter key is pressed', () => { + render() + const button = screen.getByText('React') + fireEvent.keyDown(button, { key: 'Enter' }) + expect(mockPush).not.toHaveBeenCalled() + }) + + it('does not navigate when button is disabled and Space key is pressed', () => { + render() + const button = screen.getByText('Vue') + fireEvent.keyDown(button, { key: ' ' }) + expect(mockPush).not.toHaveBeenCalled() + }) + + it('has disabled attribute when isDisabled is true', () => { + render() + const button = screen.getByText('React') + expect(button).toBeDisabled() + }) + + it('does not have disabled attribute when isDisabled is false', () => { + render() + const button = screen.getByText('React') + expect(button).not.toBeDisabled() + }) + + it('applies cursor-default class when isDisabled is true', () => { + render() + const button = screen.getByText('React') + expect(button).toHaveClass('cursor-default') + }) + + it('applies cursor-pointer class when isDisabled is false', () => { + render() + const button = screen.getByText('React') + expect(button).toHaveClass('cursor-pointer') + }) }) diff --git a/frontend/__tests__/unit/pages/CreateModule.test.tsx b/frontend/__tests__/unit/pages/CreateModule.test.tsx index 79279a8f59..eec8e7c1d3 100644 --- a/frontend/__tests__/unit/pages/CreateModule.test.tsx +++ b/frontend/__tests__/unit/pages/CreateModule.test.tsx @@ -50,7 +50,7 @@ describe('CreateModulePage', () => { }) it('submits the form and navigates to programs page', async () => { - const user = userEvent.setup() + const user = userEvent.setup({ delay: null }) ;(useSession as jest.Mock).mockReturnValue({ data: { user: { login: 'admin-user' } }, @@ -95,7 +95,7 @@ describe('CreateModulePage', () => { () => { expect(mockQuery).toHaveBeenCalled() }, - { timeout: 2000 } + { timeout: 1000 } ) const projectOption = await waitFor( diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx index 955f1dc781..70201e81cc 100644 --- a/frontend/src/app/about/page.tsx +++ b/frontend/src/app/about/page.tsx @@ -1,4 +1,5 @@ 'use client' + import { useQuery } from '@apollo/client/react' import { Tooltip } from '@heroui/tooltip' import upperFirst from 'lodash/upperFirst' @@ -12,6 +13,7 @@ import { HiUserGroup } from 'react-icons/hi' import { IconWrapper } from 'wrappers/IconWrapper' import { ErrorDisplay, handleAppError } from 'app/global-error' import { GetAboutPageDataDocument } from 'types/__generated__/aboutQueries.generated' +import type { Leader } from 'types/leader' import { technologies, missionContent, @@ -77,11 +79,14 @@ const About = () => { const projectMetadata = data?.project const topContributors = data?.topContributors - const leadersData = [data?.leader1, data?.leader2, data?.leader3].filter(Boolean).map((user) => ({ - description: user?.login ? leaders[user.login as keyof typeof leaders] : '', - memberName: user?.name || user?.login, - member: user, - })) + const leadersData = [data?.leader1, data?.leader2, data?.leader3] + .filter(Boolean) + .map((user) => ({ + description: user?.login ? leaders[user.login as keyof typeof leaders] : '', + memberName: user?.name || user?.login, + member: user, + })) + .filter((leader) => leader.memberName) as Leader[] const [showAllRoadmap, setShowAllRoadmap] = useState(false) const [showAllTimeline, setShowAllTimeline] = useState(false) diff --git a/frontend/src/app/api/auth/[...nextauth]/route.ts b/frontend/src/app/api/auth/[...nextauth]/route.ts index 990b0e2617..726a4f3944 100644 --- a/frontend/src/app/api/auth/[...nextauth]/route.ts +++ b/frontend/src/app/api/auth/[...nextauth]/route.ts @@ -6,7 +6,7 @@ import { IsProjectLeaderDocument, } from 'types/__generated__/mentorshipQueries.generated' import { ExtendedProfile, ExtendedSession } from 'types/auth' -import { IS_GITHUB_AUTH_ENABLED } from 'utils/env.server' +import { IS_GITHUB_AUTH_ENABLED, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } from 'utils/env.server' async function checkIfProjectLeader(login: string): Promise { try { @@ -47,8 +47,8 @@ const providers = [] if (IS_GITHUB_AUTH_ENABLED) { providers.push( GitHubProvider({ - clientId: process.env.NEXT_SERVER_GITHUB_CLIENT_ID, - clientSecret: process.env.NEXT_SERVER_GITHUB_CLIENT_SECRET, + clientId: GITHUB_CLIENT_ID!, + clientSecret: GITHUB_CLIENT_SECRET!, profile(profile) { return { email: profile.email, @@ -88,7 +88,8 @@ const authOptions: AuthOptions = { } if (trigger === 'update' && session) { - token.isOwaspStaff = (session as ExtendedSession).user.isOwaspStaff || false + const extSession = session as ExtendedSession + token.isOwaspStaff = extSession.user?.isOwaspStaff || false } return token }, @@ -96,11 +97,12 @@ const authOptions: AuthOptions = { async session({ session, token }) { ;(session as ExtendedSession).accessToken = token.accessToken as string - if (session.user) { - ;(session as ExtendedSession).user.login = token.login as string - ;(session as ExtendedSession).user.isMentor = token.isMentor as boolean - ;(session as ExtendedSession).user.isLeader = token.isLeader as boolean - ;(session as ExtendedSession).user.isOwaspStaff = token.isOwaspStaff as boolean + if (session?.user) { + const extSession = session as ExtendedSession + extSession.user!.login = token.login as string + extSession.user!.isMentor = token.isMentor as boolean + extSession.user!.isLeader = token.isLeader as boolean + extSession.user!.isOwaspStaff = token.isOwaspStaff as boolean } return session }, diff --git a/frontend/src/app/board/[year]/candidates/page.tsx b/frontend/src/app/board/[year]/candidates/page.tsx index 56ac410a29..2820c5933e 100644 --- a/frontend/src/app/board/[year]/candidates/page.tsx +++ b/frontend/src/app/board/[year]/candidates/page.tsx @@ -30,18 +30,19 @@ type Candidate = { memberEmail: string description: string member?: { + __typename?: string avatarUrl: string - bio?: string - createdAt?: number - firstOwaspContributionAt?: number + bio: string + createdAt: number + firstOwaspContributionAt: number | null id: string - isFormerOwaspStaff?: boolean - isGsocMentor?: boolean - isOwaspBoardMember?: boolean - linkedinPageId?: string + isFormerOwaspStaff: boolean + isGsocMentor: boolean + isOwaspBoardMember: boolean + linkedinPageId: string login: string name: string - } + } | null } type MemberSnapshot = { @@ -332,11 +333,13 @@ const BoardCandidatesPage = () => { onClick={(e) => { e.stopPropagation() e.preventDefault() - window.open( - `https://linkedin.com/in/${candidate.member.linkedinPageId}`, - '_blank', - 'noopener,noreferrer' - ) + if (candidate.member?.linkedinPageId) { + window.open( + `https://linkedin.com/in/${candidate.member.linkedinPageId}`, + '_blank', + 'noopener,noreferrer' + ) + } }} className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" aria-label={`${candidate.memberName}'s LinkedIn profile`} diff --git a/frontend/src/app/chapters/[chapterKey]/layout.tsx b/frontend/src/app/chapters/[chapterKey]/layout.tsx index 194c749d19..783e42fdbb 100644 --- a/frontend/src/app/chapters/[chapterKey]/layout.tsx +++ b/frontend/src/app/chapters/[chapterKey]/layout.tsx @@ -33,7 +33,7 @@ export async function generateMetadata({ keywords: ['owasp', 'security', 'chapter', chapterKey, chapter.name], title: chapter.name, }) - : null + : {} } export default async function ChapterDetailsLayout({ diff --git a/frontend/src/app/chapters/[chapterKey]/page.tsx b/frontend/src/app/chapters/[chapterKey]/page.tsx index eec856984f..d037752fa8 100644 --- a/frontend/src/app/chapters/[chapterKey]/page.tsx +++ b/frontend/src/app/chapters/[chapterKey]/page.tsx @@ -5,6 +5,7 @@ import { useParams } from 'next/navigation' import { useEffect } from 'react' import { handleAppError, ErrorDisplay } from 'app/global-error' import { GetChapterDataDocument } from 'types/__generated__/chapterQueries.generated' +import type { Chapter } from 'types/chapter' import { getContributionStats } from 'utils/contributionDataUtils' import { formatDate, getDateRange } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' @@ -55,9 +56,9 @@ export default function ChapterDetailsPage() { } const details = [ - { label: 'Last Updated', value: formatDate(chapter.updatedAt) }, - { label: 'Location', value: chapter.suggestedLocation }, - { label: 'Region', value: chapter.region }, + { label: 'Last Updated', value: formatDate(chapter.updatedAt) ?? '' }, + { label: 'Location', value: chapter.suggestedLocation ?? '' }, + { label: 'Region', value: chapter.region ?? '' }, { label: 'URL', value: ( @@ -83,7 +84,7 @@ export default function ChapterDetailsPage() { endDate={endDate} entityKey={chapter.key} entityLeaders={chapter.entityLeaders} - geolocationData={[chapter]} + geolocationData={[chapter as unknown as Chapter]} isActive={chapter.isActive} socialLinks={chapter.relatedUrls} startDate={startDate} diff --git a/frontend/src/app/chapters/page.tsx b/frontend/src/app/chapters/page.tsx index 14f08833a6..621a8d9a6d 100644 --- a/frontend/src/app/chapters/page.tsx +++ b/frontend/src/app/chapters/page.tsx @@ -49,7 +49,7 @@ const ChaptersPage = () => { const renderChapterCard = (chapter: Chapter) => { const params: string[] = ['updatedAt'] const filteredIcons = getFilteredIcons(chapter, params) - const formattedUrls = handleSocialUrls(chapter.relatedUrls) + const formattedUrls = handleSocialUrls(chapter.relatedUrls ?? []) const handleButtonClick = () => { router.push(`/chapters/${chapter.key}`) @@ -63,11 +63,11 @@ const ChaptersPage = () => { return ( { return ( ) } diff --git a/frontend/src/app/community/snapshots/[id]/layout.tsx b/frontend/src/app/community/snapshots/[id]/layout.tsx index 4080286411..dc44df7521 100644 --- a/frontend/src/app/community/snapshots/[id]/layout.tsx +++ b/frontend/src/app/community/snapshots/[id]/layout.tsx @@ -23,7 +23,7 @@ export async function generateMetadata({ keywords: ['owasp', 'snapshot', snapshotKey, snapshot?.title], title: snapshot?.title, }) - : null + : {} } export default function SnapshotDetailsLayout({ children }: { children: React.ReactNode }) { return children diff --git a/frontend/src/app/community/snapshots/[id]/page.tsx b/frontend/src/app/community/snapshots/[id]/page.tsx index 9478cd2004..5da0304710 100644 --- a/frontend/src/app/community/snapshots/[id]/page.tsx +++ b/frontend/src/app/community/snapshots/[id]/page.tsx @@ -7,6 +7,7 @@ import { handleAppError, ErrorDisplay } from 'app/global-error' import { GetSnapshotDetailsDocument } from 'types/__generated__/snapshotQueries.generated' import type { Chapter } from 'types/chapter' import type { Project } from 'types/project' +import type { Release as ReleaseType } from 'types/release' import { level } from 'utils/data' import { formatDate } from 'utils/dateFormatter' import { getFilteredIcons, handleSocialUrls } from 'utils/utility' @@ -52,10 +53,12 @@ const SnapshotDetailsPage: React.FC = () => { return ( { const renderChapterCard = (chapter: Chapter) => { const params: string[] = ['updatedAt'] const filteredIcons = getFilteredIcons(chapter, params) - const formattedUrls = handleSocialUrls(chapter.relatedUrls) + const formattedUrls = handleSocialUrls(chapter.relatedUrls ?? []) const handleButtonClick = () => { router.push(`/chapters/${chapter.key}`) @@ -84,7 +87,7 @@ const SnapshotDetailsPage: React.FC = () => { cardKey={chapter.key} icons={filteredIcons} social={formattedUrls} - summary={chapter.summary} + summary={chapter.summary ?? ''} title={chapter.name} url={`/chapters/${chapter.key}`} /> @@ -142,7 +145,7 @@ const SnapshotDetailsPage: React.FC = () => {
{ {snapshot.newChapters .filter((chapter) => chapter.isActive) .map((chapter) => ( - {renderChapterCard(chapter)} + + {renderChapterCard(chapter as unknown as Chapter)} + ))}
@@ -182,8 +187,11 @@ const SnapshotDetailsPage: React.FC = () => { {snapshot.newReleases.map((release, index) => { return ( diff --git a/frontend/src/app/contribute/page.tsx b/frontend/src/app/contribute/page.tsx index 335ad8dc0f..24317b1bfd 100644 --- a/frontend/src/app/contribute/page.tsx +++ b/frontend/src/app/contribute/page.tsx @@ -1,4 +1,5 @@ 'use client' + import { useSearchPage } from 'hooks/useSearchPage' import React, { useState } from 'react' import { FaGithub, FaWandMagicSparkles } from 'react-icons/fa6' @@ -41,28 +42,28 @@ const ContributePage = () => { } return ( - + setModalOpenIndex(null)} + summary={issue.summary ?? ''} title={issue.title} - summary={issue.summary} - hint={issue.hint} - button={viewIssueButton} - description="The issue summary and the recommended steps to address it have been generated by AI" > ) diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 82c8c40abc..ec9194fbdd 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -84,7 +84,7 @@ export default function RootLayout({ - + {GTM_ID && } ) } diff --git a/frontend/src/app/members/[memberKey]/layout.tsx b/frontend/src/app/members/[memberKey]/layout.tsx index b633c69a32..3d676e78f4 100644 --- a/frontend/src/app/members/[memberKey]/layout.tsx +++ b/frontend/src/app/members/[memberKey]/layout.tsx @@ -23,7 +23,7 @@ export async function generateMetadata({ }, }) const user = data?.user - const title = user?.name || user?.login + const title = user?.name || user?.login || memberKey return user ? generateSeoMetadata({ @@ -32,7 +32,7 @@ export async function generateMetadata({ keywords: [user.login, user.name, 'owasp', 'owasp community member'], title: title, }) - : null + : {} } export default async function UserDetailsLayout({ 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 4f02335261..9485ad1ca6 100644 --- a/frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx +++ b/frontend/src/app/mentorship/programs/[programKey]/modules/[moduleKey]/page.tsx @@ -6,6 +6,7 @@ import { useParams } from 'next/navigation' import { useEffect } from 'react' import { ErrorDisplay, handleAppError } from 'app/global-error' import { GetProgramAdminsAndModulesDocument } from 'types/__generated__/moduleQueries.generated' +import type { PullRequest } from 'types/pullRequest' import { formatDate } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' @@ -69,13 +70,13 @@ const ModuleDetailsPage = () => { return ( diff --git a/frontend/src/app/mentorship/programs/[programKey]/page.tsx b/frontend/src/app/mentorship/programs/[programKey]/page.tsx index f17e47dd4f..7d81cc94f1 100644 --- a/frontend/src/app/mentorship/programs/[programKey]/page.tsx +++ b/frontend/src/app/mentorship/programs/[programKey]/page.tsx @@ -67,13 +67,15 @@ const ProgramDetailsPage = () => { return ( 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 8938a2f8f0..5f02f14e1d 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx @@ -32,16 +32,26 @@ const EditProgramPage = () => { skip: !programKey, fetchPolicy: 'network-only', }) - const [formData, setFormData] = useState({ - name: '', + const [formData, setFormData] = useState<{ + adminLogins?: string + description: string + domains: string + endedAt: string + menteesLimit: number + name: string + startedAt: string + status?: string + tags: string + }>({ + adminLogins: '', description: '', + domains: '', + endedAt: '', menteesLimit: 0, + name: '', startedAt: '', - endedAt: '', - tags: '', - domains: '', - adminLogins: '', status: ProgramStatusEnum.Draft, + tags: '', }) const [accessStatus, setAccessStatus] = useState<'checking' | 'allowed' | 'denied'>('checking') useEffect(() => { @@ -100,16 +110,16 @@ const EditProgramPage = () => { e.preventDefault() try { const input = { - key: programKey, - name: formData.name, + adminLogins: parseCommaSeparated(formData.adminLogins), description: formData.description, + domains: parseCommaSeparated(formData.domains), + endedAt: formData.endedAt, + key: programKey, menteesLimit: Number(formData.menteesLimit), + name: formData.name, startedAt: formData.startedAt, - endedAt: formData.endedAt, + status: (formData.status as ProgramStatusEnum) || ProgramStatusEnum.Draft, tags: parseCommaSeparated(formData.tags), - domains: parseCommaSeparated(formData.domains), - adminLogins: parseCommaSeparated(formData.adminLogins), - status: formData.status, } const result = await updateProgram({ 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 3a925886f3..8b4543b9f6 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 @@ -7,6 +7,7 @@ import { useSession } from 'next-auth/react' import React, { useEffect, useState } from 'react' import { ErrorDisplay, handleAppError } from 'app/global-error' import { ExperienceLevelEnum } from 'types/__generated__/graphql' +import type { UpdateModuleInput } from 'types/__generated__/graphql' import { UpdateModuleDocument } from 'types/__generated__/moduleMutations.generated' import { GetProgramAdminsAndModulesDocument } from 'types/__generated__/moduleQueries.generated' import { GetProgramAndModulesDocument } from 'types/__generated__/programsQueries.generated' @@ -76,17 +77,17 @@ const EditModulePage = () => { if (accessStatus === 'allowed' && data?.getModule) { const m = data.getModule setFormData({ - name: m.name || '', description: m.description || '', - experienceLevel: m.experienceLevel || ExperienceLevelEnum.Beginner, - startedAt: formatDateForInput(m.startedAt), - endedAt: formatDateForInput(m.endedAt), domains: (m.domains || []).join(', '), - projectName: m.projectName, - tags: (m.tags || []).join(', '), + endedAt: formatDateForInput(m.endedAt), + experienceLevel: m.experienceLevel || ExperienceLevelEnum.Beginner, labels: (m.labels || []).join(', '), - projectId: m.projectId || '', mentorLogins: (m.mentors || []).map((mentor: { login: string }) => mentor.login).join(', '), + name: m.name || '', + projectId: m.projectId || '', + projectName: m.projectName || '', + startedAt: formatDateForInput(m.startedAt), + tags: (m.tags || []).join(', '), }) } }, [accessStatus, data]) @@ -96,10 +97,10 @@ const EditModulePage = () => { if (!formData) return try { - const input = { + const input: UpdateModuleInput = { description: formData.description, domains: parseCommaSeparated(formData.domains), - endedAt: formData.endedAt || null, + endedAt: formData.endedAt || '', experienceLevel: formData.experienceLevel as ExperienceLevelEnum, key: moduleKey, labels: parseCommaSeparated(formData.labels), @@ -108,7 +109,7 @@ const EditModulePage = () => { programKey: programKey, projectId: formData.projectId, projectName: formData.projectName, - startedAt: formData.startedAt || null, + startedAt: formData.startedAt || '', tags: parseCommaSeparated(formData.tags), } @@ -150,7 +151,7 @@ const EditModulePage = () => { >} onSubmit={handleSubmit} loading={mutationLoading} submitText="Save" 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 730450765d..1964857d9a 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 @@ -88,7 +88,7 @@ const ModuleIssueDetailsPage = () => { } = useIssueMutations({ programKey, moduleKey, issueId }) const issue = data?.getModule?.issueByNumber - const taskDeadline = data?.getModule?.taskDeadline as string | undefined + const taskDeadline = (data?.getModule?.taskDeadline as string | undefined) ?? null const getButtonClassName = (disabled: boolean) => `inline-flex items-center justify-center rounded-md border p-1.5 text-sm ${ diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeKey]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeKey]/page.tsx index a3f54807e9..ed30caf4ae 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeKey]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeKey]/page.tsx @@ -153,7 +153,7 @@ const MenteeProfilePage = () => { {/* Domains and Skills */} - {(menteeDetails.domains?.length > 0 || menteeDetails.tags?.length > 0) && ( + {((menteeDetails.domains ?? []).length > 0 || (menteeDetails.tags ?? []).length > 0) && (
{menteeDetails.domains && menteeDetails.domains.length > 0 && ( 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 26ad68c9f4..9773892714 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 @@ -60,16 +60,16 @@ const ModuleDetailsPage = () => { return ( 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 d61ad014b1..76280838b6 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 @@ -34,7 +34,19 @@ const CreateModulePage = () => { fetchPolicy: 'network-only', }) - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState<{ + description: string + domains: string + endedAt: string + experienceLevel: string + labels: string + mentorLogins: string + name: string + projectId: string + projectName: string + startedAt: string + tags: string + }>({ description: '', domains: '', endedAt: '', @@ -87,15 +99,15 @@ const CreateModulePage = () => { const input = { description: formData.description, domains: parseCommaSeparated(formData.domains), - endedAt: formData.endedAt || null, - experienceLevel: formData.experienceLevel, + endedAt: formData.endedAt, + experienceLevel: formData.experienceLevel as ExperienceLevelEnum, labels: parseCommaSeparated(formData.labels), mentorLogins: parseCommaSeparated(formData.mentorLogins), name: formData.name, programKey: programKey, projectId: formData.projectId, projectName: formData.projectName, - startedAt: formData.startedAt || null, + startedAt: formData.startedAt, tags: parseCommaSeparated(formData.tags), } @@ -117,7 +129,10 @@ const CreateModulePage = () => { } catch (error) { addToast({ title: 'Creation Failed', - description: error.message || 'Something went wrong while creating the module.', + description: + error instanceof Error + ? error.message + : 'Something went wrong while creating the module.', color: 'danger', variant: 'solid', timeout: 4000, diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx index 2ec882c417..ef97797c36 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx @@ -46,14 +46,25 @@ const ProgramDetailsPage = () => { return true }, [isAdmin, program]) - const updateStatus = async (newStatus: ProgramStatusEnum) => { - if (!program || !isAdmin) { + const updateStatus = async (newStatus: string) => { + if (!Object.values(ProgramStatusEnum).includes(newStatus as ProgramStatusEnum)) { addToast({ - title: 'Permission Denied', - description: 'Only admins can update the program status.', + color: 'danger', + description: 'The provided status is not valid.', + timeout: 3000, + title: 'Invalid Status', variant: 'solid', + }) + return + } + + if (!program || !isAdmin) { + addToast({ color: 'danger', + description: 'Only admins can update the program status.', timeout: 3000, + title: 'Permission Denied', + variant: 'solid', }) return } @@ -64,7 +75,7 @@ const ProgramDetailsPage = () => { inputData: { key: program.key, name: program.name, - status: newStatus, + status: newStatus as ProgramStatusEnum, }, }, }) @@ -94,30 +105,30 @@ const ProgramDetailsPage = () => { } const programDetails = [ - { 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: '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 ?? 0) }, { label: 'Experience Levels', - value: program.experienceLevels?.map((level) => titleCaseWord(level)).join(', ') || 'N/A', + value: program?.experienceLevels?.map((level) => titleCaseWord(level)).join(', ') || 'N/A', }, ] return ( ) diff --git a/frontend/src/app/my/mentorship/programs/create/page.tsx b/frontend/src/app/my/mentorship/programs/create/page.tsx index ef0cba9728..f70df560aa 100644 --- a/frontend/src/app/my/mentorship/programs/create/page.tsx +++ b/frontend/src/app/my/mentorship/programs/create/page.tsx @@ -15,7 +15,7 @@ import ProgramForm from 'components/ProgramForm' const CreateProgramPage = () => { const router = useRouter() const { data: session, status } = useSession() - const isProjectLeader = (session as ExtendedSession)?.user.isLeader + const isProjectLeader = (session as ExtendedSession | undefined)?.user?.isLeader const [redirected, setRedirected] = useState(false) @@ -80,7 +80,8 @@ const CreateProgramPage = () => { router.push('/my/mentorship') } catch (err) { addToast({ - description: err?.message || 'Unable to complete the requested operation.', + description: + err instanceof Error ? err.message : 'Unable to complete the requested operation.', title: 'GraphQL Request Failed', timeout: 3000, shouldShowTimeoutProgress: true, diff --git a/frontend/src/app/organizations/[organizationKey]/layout.tsx b/frontend/src/app/organizations/[organizationKey]/layout.tsx index d148c6c501..35334b56bb 100644 --- a/frontend/src/app/organizations/[organizationKey]/layout.tsx +++ b/frontend/src/app/organizations/[organizationKey]/layout.tsx @@ -14,7 +14,7 @@ export async function generateMetadata({ params, }: { params: Promise<{ organizationKey: string }> -}): Promise { +}): Promise { const { organizationKey } = await params const { data } = await apolloClient.query({ query: GetOrganizationMetadataDocument, @@ -23,7 +23,7 @@ export async function generateMetadata({ }, }) const organization = data?.organization - const title = organization?.name ?? organization?.login + const title = organization?.name ?? organization?.login ?? '' return organization ? generateSeoMetadata({ @@ -31,7 +31,7 @@ export async function generateMetadata({ description: organization?.description ?? `${title} organization details`, title: title, }) - : null + : undefined } async function generateOrganizationStructuredData(organizationKey: string) { diff --git a/frontend/src/app/organizations/[organizationKey]/page.tsx b/frontend/src/app/organizations/[organizationKey]/page.tsx index 95350011fa..76fe6e3d3a 100644 --- a/frontend/src/app/organizations/[organizationKey]/page.tsx +++ b/frontend/src/app/organizations/[organizationKey]/page.tsx @@ -1,4 +1,5 @@ 'use client' + import { useQuery } from '@apollo/client/react' import Link from 'next/link' import { useParams } from 'next/navigation' @@ -8,6 +9,11 @@ import { FaCodeFork, FaFolderOpen, FaStar } from 'react-icons/fa6' import { HiUserGroup } from 'react-icons/hi' import { handleAppError, ErrorDisplay } from 'app/global-error' import { GetOrganizationDataDocument } from 'types/__generated__/organizationQueries.generated' +import type { Issue } from 'types/issue' +import type { Milestone } from 'types/milestone' +import type { RepositoryCardProps } from 'types/project' +import type { PullRequest } from 'types/pullRequest' +import type { Release } from 'types/release' import { formatDate } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' import OrganizationDetailsPageSkeleton from 'components/skeletons/OrganizationDetailsPageSkeleton' @@ -110,11 +116,21 @@ const OrganizationDetailsPage = () => { return ( ({ + ...release, + publishedAt: release.publishedAt as number, + })) as Release[] + } + recentMilestones={recentMilestones as Milestone[]} + repositories={ + repositories?.map((repo) => ({ + ...repo, + organization: repo.organization ? { login: repo.organization.login } : undefined, + })) as RepositoryCardProps[] + } stats={organizationStats} summary={organization.description} title={organization.name} diff --git a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/layout.tsx b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/layout.tsx index e9a0a40bd5..83523a70aa 100644 --- a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/layout.tsx +++ b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/layout.tsx @@ -25,7 +25,7 @@ export async function generateMetadata({ repositoryKey: string organizationKey: string }> -}): Promise { +}): Promise { const { repositoryKey, organizationKey } = await params const data = await getRepositoryMetadata(organizationKey, repositoryKey) const repository = data?.repository diff --git a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx index 2d155fe662..f1abaf8936 100644 --- a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx +++ b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx @@ -28,6 +28,10 @@ const RepositoryDetailsPage = () => { const repository = data?.repository const topContributors = data?.topContributors ?? [] const recentPullRequests = data?.recentPullRequests + const recentIssues = repository?.issues?.map((issue) => ({ + ...issue, + author: issue.author ?? undefined, + })) useEffect(() => { if (graphQLRequestError) { @@ -106,10 +110,10 @@ const RepositoryDetailsPage = () => { isArchived={repository.isArchived} languages={repository.languages} projectName={repository.project?.name} - pullRequests={recentPullRequests} - recentIssues={repository.issues} - recentMilestones={repository.recentMilestones} - recentReleases={repository.releases} + pullRequests={recentPullRequests ?? []} + recentIssues={recentIssues ?? []} + recentMilestones={repository.recentMilestones ?? []} + recentReleases={repository.releases ?? []} stats={RepositoryStats} summary={repository.description} title={repository.name} diff --git a/frontend/src/app/organizations/page.tsx b/frontend/src/app/organizations/page.tsx index 07d216f393..bb567387ad 100644 --- a/frontend/src/app/organizations/page.tsx +++ b/frontend/src/app/organizations/page.tsx @@ -37,7 +37,7 @@ const OrganizationPage = () => { return (
- {data.upcomingEvents.map((event: Event, index: number) => ( + {data?.upcomingEvents?.map((event: Event, index: number) => (
@@ -187,7 +190,7 @@ export default function Home() { url: event.url, }} iconClassName="h-4 w-4 mr-2" - label={formatDateRange(event.startDate, event.endDate)} + label={formatDateRange(event.startDate, event.endDate ?? event.startDate)} showLabel />
@@ -204,7 +207,7 @@ export default function Home() { isOpen={modalOpenIndex === index} onClose={() => setModalOpenIndex(null)} title={event.name} - summary={event.summary} + summary={event.summary ?? ''} button={{ label: 'View Event', url: event.url }} description="The event summary has been generated by AI" > @@ -223,7 +226,7 @@ export default function Home() { className="overflow-hidden" >
- {data.recentChapters?.map((chapter) => ( + {data?.recentChapters?.map((chapter) => (

- {data.recentProjects?.map((project) => ( + {data?.recentProjects?.map((project) => (

@@ -322,19 +325,28 @@ export default function Home() { />

- - + {data?.recentIssues && } + {data?.recentMilestones && ( + + )}
- - + {data?.recentPullRequests && ( + + )} + {data?.recentReleases && ( + + )}
(
-
{millify(stat.value)}+
+
+ {millify(stat.value ?? 0)}+ +
{stat.label}
@@ -401,9 +415,11 @@ export default function Home() { Join OWASP
- - - + {data?.sponsors && ( + + + + )}

diff --git a/frontend/src/app/projects/[projectKey]/layout.tsx b/frontend/src/app/projects/[projectKey]/layout.tsx index 85303ac28c..ceeb30cf54 100644 --- a/frontend/src/app/projects/[projectKey]/layout.tsx +++ b/frontend/src/app/projects/[projectKey]/layout.tsx @@ -23,7 +23,7 @@ export async function generateMetadata({ params: Promise<{ projectKey: string }> -}): Promise { +}): Promise { const { projectKey } = await params const data = await getProjectMetadata(projectKey) const project = data?.project @@ -35,7 +35,7 @@ export async function generateMetadata({ keywords: ['owasp', 'project', projectKey, project.name], title: project.name, }) - : null + : undefined } export default async function ProjectDetailsLayout({ diff --git a/frontend/src/app/projects/[projectKey]/page.tsx b/frontend/src/app/projects/[projectKey]/page.tsx index dce36a4d2b..456b060f16 100644 --- a/frontend/src/app/projects/[projectKey]/page.tsx +++ b/frontend/src/app/projects/[projectKey]/page.tsx @@ -9,6 +9,12 @@ import { FaCodeFork, FaFolderOpen, FaStar } from 'react-icons/fa6' import { HiUserGroup } from 'react-icons/hi' import { ErrorDisplay, handleAppError } from 'app/global-error' import { GetProjectDocument } from 'types/__generated__/projectQueries.generated' +import type { HealthMetricsProps } from 'types/healthMetrics' +import type { Issue } from 'types/issue' +import type { Milestone } from 'types/milestone' +import type { RepositoryCardProps } from 'types/project' +import type { PullRequest } from 'types/pullRequest' +import type { Release } from 'types/release' import { getContributionStats } from 'utils/contributionDataUtils' import { formatDate, getDateRange } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' @@ -109,14 +115,14 @@ const ProjectDetailsPage = () => { endDate={endDate} entityKey={project.key} entityLeaders={project.entityLeaders} - healthMetricsData={project.healthMetricsList} + healthMetricsData={project.healthMetricsList as unknown as HealthMetricsProps[]} isActive={project.isActive} languages={project.languages} - pullRequests={project.recentPullRequests} - recentIssues={project.recentIssues} - recentMilestones={project.recentMilestones} - recentReleases={project.recentReleases} - repositories={project.repositories} + pullRequests={project.recentPullRequests as unknown as PullRequest[]} + recentIssues={project.recentIssues as unknown as Issue[]} + recentMilestones={project.recentMilestones as unknown as Milestone[]} + recentReleases={project.recentReleases as unknown as Release[]} + repositories={project.repositories as unknown as RepositoryCardProps[]} startDate={startDate} stats={projectStats} summary={project.summary} diff --git a/frontend/src/app/projects/dashboard/metrics/[projectKey]/page.tsx b/frontend/src/app/projects/dashboard/metrics/[projectKey]/page.tsx index 3f12991efc..6293fc68de 100644 --- a/frontend/src/app/projects/dashboard/metrics/[projectKey]/page.tsx +++ b/frontend/src/app/projects/dashboard/metrics/[projectKey]/page.tsx @@ -40,7 +40,7 @@ const ProjectHealthMetricsDetails: FC = () => { handleAppError(graphqlError) } if (data?.project?.healthMetricsLatest) { - setMetricsLatest(data.project.healthMetricsLatest) + setMetricsLatest(data.project.healthMetricsLatest as unknown as HealthMetricsProps) } if (data?.project?.healthMetricsList) { setMetricsList(data.project.healthMetricsList) @@ -53,10 +53,12 @@ const ProjectHealthMetricsDetails: FC = () => { const labels = metricsList?.map((m) => - new Date(m.createdAt).toLocaleString('default', { - month: 'short', - day: 'numeric', - }) + m.createdAt + ? new Date(m.createdAt).toLocaleString('default', { + month: 'short', + day: 'numeric', + }) + : '' ) || [] return (
@@ -71,7 +73,7 @@ const ProjectHealthMetricsDetails: FC = () => { />
- + { : 'Funding Requirements Not Compliant' } icon={FaDollarSign} - compliant={metricsLatest.isFundingRequirementsCompliant} + compliant={metricsLatest.isFundingRequirementsCompliant ?? false} /> { : 'Leader Requirements Not Compliant' } icon={FaHandshake} - compliant={metricsLatest.isLeaderRequirementsCompliant} + compliant={metricsLatest.isLeaderRequirementsCompliant ?? false} />
@@ -99,7 +101,7 @@ const ProjectHealthMetricsDetails: FC = () => { series={[ { name: 'Stars', - data: metricsList.map((m) => m.starsCount), + data: metricsList.map((m) => m.starsCount ?? 0), }, ]} labels={labels} @@ -111,7 +113,7 @@ const ProjectHealthMetricsDetails: FC = () => { series={[ { name: 'Forks', - data: metricsList.map((m) => m.forksCount), + data: metricsList.map((m) => m.forksCount ?? 0), }, ]} labels={labels} @@ -125,19 +127,19 @@ const ProjectHealthMetricsDetails: FC = () => { series={[ { name: 'Open Issues', - data: metricsList.map((m) => m.openIssuesCount), + data: metricsList.map((m) => m.openIssuesCount ?? 0), }, { name: 'Unassigned Issues', - data: metricsList.map((m) => m.unassignedIssuesCount), + data: metricsList.map((m) => m.unassignedIssuesCount ?? 0), }, { name: 'Unanswered Issues', - data: metricsList.map((m) => m.unansweredIssuesCount), + data: metricsList.map((m) => m.unansweredIssuesCount ?? 0), }, { name: 'Total Issues', - data: metricsList.map((m) => m.totalIssuesCount), + data: metricsList.map((m) => m.totalIssuesCount ?? 0), }, ]} labels={labels} @@ -149,7 +151,7 @@ const ProjectHealthMetricsDetails: FC = () => { series={[ { name: 'Open Pull Requests', - data: metricsList.map((m) => m.openPullRequestsCount), + data: metricsList.map((m) => m.openPullRequestsCount ?? 0), }, ]} labels={labels} @@ -163,11 +165,11 @@ const ProjectHealthMetricsDetails: FC = () => { series={[ { name: 'Recent Releases', - data: metricsList.map((m) => m.recentReleasesCount), + data: metricsList.map((m) => m.recentReleasesCount ?? 0), }, { name: 'Total Releases', - data: metricsList.map((m) => m.totalReleasesCount), + data: metricsList.map((m) => m.totalReleasesCount ?? 0), }, ]} labels={labels} @@ -179,7 +181,7 @@ const ProjectHealthMetricsDetails: FC = () => { series={[ { name: 'Contributors', - data: metricsList.map((m) => m.contributorsCount), + data: metricsList.map((m) => m.contributorsCount ?? 0), }, ]} labels={labels} @@ -197,18 +199,18 @@ const ProjectHealthMetricsDetails: FC = () => { 'Days Since OWASP Page Last Update', ]} days={[ - metricsLatest.ageDays, - metricsLatest.lastCommitDays, - metricsLatest.lastReleaseDays, - metricsLatest.lastPullRequestDays, - metricsLatest.owaspPageLastUpdateDays, + metricsLatest.ageDays ?? 0, + metricsLatest.lastCommitDays ?? 0, + metricsLatest.lastReleaseDays ?? 0, + metricsLatest.lastPullRequestDays ?? 0, + metricsLatest.owaspPageLastUpdateDays ?? 0, ]} requirements={[ - metricsLatest.ageDaysRequirement, - metricsLatest.lastCommitDaysRequirement, - metricsLatest.lastReleaseDaysRequirement, - metricsLatest.lastPullRequestDaysRequirement, - metricsLatest.owaspPageLastUpdateDaysRequirement, + metricsLatest.ageDaysRequirement ?? 0, + metricsLatest.lastCommitDaysRequirement ?? 0, + metricsLatest.lastReleaseDaysRequirement ?? 0, + metricsLatest.lastPullRequestDaysRequirement ?? 0, + metricsLatest.owaspPageLastUpdateDaysRequirement ?? 0, ]} reverseColors={[true, false, false, false, false]} /> diff --git a/frontend/src/app/projects/dashboard/metrics/page.tsx b/frontend/src/app/projects/dashboard/metrics/page.tsx index f091a53550..0c58068f8e 100644 --- a/frontend/src/app/projects/dashboard/metrics/page.tsx +++ b/frontend/src/app/projects/dashboard/metrics/page.tsx @@ -1,8 +1,9 @@ 'use client' + import { useQuery } from '@apollo/client/react' import { Pagination } from '@heroui/react' import { useSearchParams, useRouter } from 'next/navigation' -import { FC, useState, useEffect } from 'react' +import { FC, useState, useEffect, Key } from 'react' import { FaFilter, FaArrowDownWideShort, FaArrowUpWideShort } from 'react-icons/fa6' import { handleAppError } from 'app/global-error' import { Ordering, ProjectLevel } from 'types/__generated__/graphql' @@ -114,14 +115,14 @@ const MetricsPage: FC = () => { const currentFilterKeys = [] if (healthFilter) { currentFilters = { - ...healthFiltersMapping[healthFilter], + ...healthFiltersMapping[healthFilter as keyof typeof healthFiltersMapping], } currentFilterKeys.push(healthFilter) } if (levelFilter) { currentFilters = { ...currentFilters, - ...levelFiltersMapping[levelFilter], + ...levelFiltersMapping[levelFilter as keyof typeof levelFiltersMapping], } currentFilterKeys.push(levelFilter) } @@ -156,7 +157,7 @@ const MetricsPage: FC = () => { useEffect(() => { if (data) { - setMetrics(data.projectHealthMetrics) + setMetrics(data.projectHealthMetrics as unknown as HealthMetricsProps[]) setMetricsLength(data.projectHealthMetricsDistinctLength) } if (graphQLRequestError) { @@ -237,13 +238,13 @@ const MetricsPage: FC = () => { selectedLabels={ urlKey ? [SORT_FIELDS.find((f) => f.key === urlKey)?.label || ''] : [] } - onAction={(key: string) => { + onAction={(key: Key) => { if (key === 'reset-sort') { handleSort(null) return } - handleSort(key) + handleSort(key as string) }} />
@@ -254,17 +255,23 @@ const MetricsPage: FC = () => { selectionMode="multiple" selectedKeys={activeFilters} selectedLabels={getKeysLabels(filteringSections, activeFilters)} - onAction={(key: string) => { + onAction={(key: Key) => { // Because how apollo caches pagination, we need to reset the pagination. setPagination({ offset: 0, limit: PAGINATION_LIMIT }) let newFilters = { ...currentFilters } const newParams = new URLSearchParams(searchParams.toString()) - if (key in healthFiltersMapping) { - newParams.set('health', key) - newFilters = { ...newFilters, ...healthFiltersMapping[key] } - } else if (key in levelFiltersMapping) { - newParams.set('level', key) - newFilters = { ...newFilters, ...levelFiltersMapping[key] } + if ((key as string) in healthFiltersMapping) { + newParams.set('health', key as string) + newFilters = { + ...newFilters, + ...healthFiltersMapping[key as string as keyof typeof healthFiltersMapping], + } + } else if ((key as string) in levelFiltersMapping) { + newParams.set('level', key as string) + newFilters = { + ...newFilters, + ...levelFiltersMapping[key as string as keyof typeof levelFiltersMapping], + } } else { newParams.delete('health') newParams.delete('level') diff --git a/frontend/src/app/projects/dashboard/page.tsx b/frontend/src/app/projects/dashboard/page.tsx index 6ff4809714..eec349f960 100644 --- a/frontend/src/app/projects/dashboard/page.tsx +++ b/frontend/src/app/projects/dashboard/page.tsx @@ -34,7 +34,7 @@ const ProjectsDashboardPage: FC = () => { useEffect(() => { if (data) { - setStats(data.projectHealthStats) + setStats(data.projectHealthStats as unknown as ProjectHealthStats) } if (graphQLRequestError) { handleAppError(graphQLRequestError) diff --git a/frontend/src/app/projects/page.tsx b/frontend/src/app/projects/page.tsx index 8f09f8faf2..445724e57d 100644 --- a/frontend/src/app/projects/page.tsx +++ b/frontend/src/app/projects/page.tsx @@ -47,11 +47,11 @@ const ProjectsPage = () => { return ( = { chapter: 'gap-2 md:col-span-3', + committee: 'gap-2 md:col-span-5', module: 'gap-2 md:col-span-7', + organization: 'gap-2 md:col-span-5', program: 'gap-2 md:col-span-7', + project: 'gap-2 md:col-span-5', + repository: 'gap-2 md:col-span-5', + user: 'gap-2 md:col-span-5', } const hasContributions = @@ -139,7 +144,7 @@ const DetailsCard = ({

{title}

- {type === 'program' && accessLevel === 'admin' && canUpdateStatus && ( + {type === 'program' && accessLevel === 'admin' && canUpdateStatus && programKey && ( admin.login === session?.user?.login) && ( )} {!isActive && } {isArchived && type === 'repository' && } - {IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && ( - scrollToAnchor('issues-trend')} - /> - )} + {IS_PROJECT_HEALTH_ENABLED && + type === 'project' && + healthMetricsData && + healthMetricsData.length > 0 && + healthMetricsData[0].score !== undefined && ( + scrollToAnchor('issues-trend')} + /> + )}
@@ -197,7 +207,7 @@ const DetailsCard = ({ )} - {showStatistics(type) && ( + {showStatistics(type) && stats && ( } @@ -216,7 +226,7 @@ const DetailsCard = ({ ))} )} - {type === 'chapter' && geolocationData && ( + {type === 'chapter' && geolocationData && geolocationData.length > 0 && (
)}
- {(type === 'project' || type === 'repository') && ( + {(type === 'project' || type === 'repository') && (languages || topics) && (
- {languages.length !== 0 && ( + {languages && languages.length !== 0 && ( } /> )} - {topics.length !== 0 && ( + {topics && topics.length !== 0 && ( - {tags?.length > 0 && ( + {tags && tags.length > 0 && ( )} - {domains?.length > 0 && ( + {domains && domains.length > 0 && ( )} - {labels?.length > 0 && ( + {labels && labels.length > 0 && (
- - + {recentIssues && } + {recentMilestones && }
)} {showPullRequestsAndReleases(type) && (
- - + {pullRequests && } + {recentReleases && ( + + )}
)} {type === 'module' && pullRequests && pullRequests.length > 0 && ( @@ -388,19 +404,25 @@ const DetailsCard = ({ )} - {type === 'program' && modules.length > 0 && ( - <> - {modules.length === 1 ? ( -
- -
- ) : ( - }> - - - )} - - )} + {type === 'program' && + modules && + modules.length > 0 && + (() => { + const modulesList = modules + return ( + <> + {modulesList.length === 1 ? ( +
+ +
+ ) : ( + }> + + + )} + + ) + })()} {type === 'program' && recentMilestones && recentMilestones.length > 0 && ( }>
@@ -460,7 +482,7 @@ const DetailsCard = ({
- {formatDate(milestone.createdAt)} + {milestone.createdAt && {formatDate(milestone.createdAt)}}
@@ -492,16 +514,22 @@ const DetailsCard = ({ )} )} - {IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && ( - - )} - {entityKey && ['chapter', 'project', 'repository'].includes(type) && ( - - )} + {IS_PROJECT_HEALTH_ENABLED && + type === 'project' && + healthMetricsData && + healthMetricsData.length > 0 && } + {entityKey && + ['chapter', 'project', 'repository'].includes(type) && + (projectName || title) && + (() => { + return ( + + ) + })()}
) @@ -509,13 +537,13 @@ const DetailsCard = ({ export default DetailsCard -export const SocialLinks = ({ urls }) => { +export const SocialLinks = ({ urls }: { urls: string[] }) => { if (!urls || urls.length === 0) return null return (
Social Links
- {urls.map((url) => { + {urls.map((url: string) => { const SocialIcon = getSocialIcon(url) return ( [ - chapter._geoloc?.lat ?? chapter.geoLocation?.lat, - chapter._geoloc?.lng ?? chapter.geoLocation?.lng, + (chapter._geoloc?.lat ?? chapter.geoLocation?.lat) as number, + (chapter._geoloc?.lng ?? chapter.geoLocation?.lng) as number, ]) ) const maxZoom = 7 @@ -113,8 +113,8 @@ const MapViewUpdater = ({ const nearestChapter = validGeoLocData[0] map.setView( [ - nearestChapter._geoloc?.lat ?? nearestChapter.geoLocation?.lat, - nearestChapter._geoloc?.lng ?? nearestChapter.geoLocation?.lng, + (nearestChapter._geoloc?.lat ?? nearestChapter.geoLocation?.lat) as number, + (nearestChapter._geoloc?.lng ?? nearestChapter.geoLocation?.lng) as number, ], maxZoom ) @@ -219,8 +219,8 @@ const ChapterMap = ({ diff --git a/frontend/src/components/ContributionHeatmap.tsx b/frontend/src/components/ContributionHeatmap.tsx index 903b859235..95a7e2338c 100644 --- a/frontend/src/components/ContributionHeatmap.tsx +++ b/frontend/src/components/ContributionHeatmap.tsx @@ -179,7 +179,15 @@ const getChartOptions = (isDarkMode: boolean, unit: string) => ({ style: { fontSize: '12px', }, - custom: ({ seriesIndex, dataPointIndex, w }) => { + custom: ({ + seriesIndex, + dataPointIndex, + w, + }: { + seriesIndex: number + dataPointIndex: number + w: { config: { series: Array<{ data: Array<{ y: number; date: string }> }> } } + }) => { const data = w.config.series[seriesIndex].data[dataPointIndex] if (!data) return '' diff --git a/frontend/src/components/EntityActions.tsx b/frontend/src/components/EntityActions.tsx index 552de36911..e56015c606 100644 --- a/frontend/src/components/EntityActions.tsx +++ b/frontend/src/components/EntityActions.tsx @@ -11,7 +11,7 @@ interface EntityActionsProps { programKey: string moduleKey?: string status?: string - setStatus?: (newStatus: string) => void + setStatus?: (newStatus: ProgramStatusEnum) => void | Promise } const EntityActions: React.FC = ({ diff --git a/frontend/src/components/HealthMetrics.tsx b/frontend/src/components/HealthMetrics.tsx index 64dd3c8411..68f62bb411 100644 --- a/frontend/src/components/HealthMetrics.tsx +++ b/frontend/src/components/HealthMetrics.tsx @@ -6,12 +6,14 @@ import BarChart from 'components/BarChart' import LineChart from 'components/LineChart' const HealthMetrics: React.FC<{ data: HealthMetricsProps[] }> = ({ data }) => { - const openIssuesCountArray = data.map((item) => item.openIssuesCount) + const openIssuesCountArray = data.map((item) => item.openIssuesCount ?? 0) const labels = data.map((item) => { - return new Date(item.createdAt).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - }) + return item.createdAt + ? new Date(item.createdAt).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + }) + : '' }) const length = data.length return ( @@ -27,11 +29,11 @@ const HealthMetrics: React.FC<{ data: HealthMetricsProps[] }> = ({ data }) => { }, { name: 'Unassigned Issues', - data: data.map((item) => item.unassignedIssuesCount), + data: data.map((item) => item.unassignedIssuesCount ?? 0), }, { name: 'Unanswered Issues', - data: data.map((item) => item.unansweredIssuesCount), + data: data.map((item) => item.unansweredIssuesCount ?? 0), }, ]} labels={labels} @@ -43,7 +45,7 @@ const HealthMetrics: React.FC<{ data: HealthMetricsProps[] }> = ({ data }) => { series={[ { name: 'Open Pull Requests', - data: data.map((item) => item.openPullRequestsCount), + data: data.map((item) => item.openPullRequestsCount ?? 0), }, ]} labels={labels} @@ -57,7 +59,7 @@ const HealthMetrics: React.FC<{ data: HealthMetricsProps[] }> = ({ data }) => { series={[ { name: 'Stars', - data: data.map((item) => item.starsCount), + data: data.map((item) => item.starsCount ?? 0), }, ]} labels={labels} @@ -69,7 +71,7 @@ const HealthMetrics: React.FC<{ data: HealthMetricsProps[] }> = ({ data }) => { series={[ { name: 'Forks', - data: data.map((item) => item.forksCount), + data: data.map((item) => item.forksCount ?? 0), }, ]} labels={labels} diff --git a/frontend/src/components/InfoBlock.tsx b/frontend/src/components/InfoBlock.tsx index 494cd58cab..281e9777f1 100644 --- a/frontend/src/components/InfoBlock.tsx +++ b/frontend/src/components/InfoBlock.tsx @@ -10,8 +10,8 @@ const InfoBlock = ({ label = '', pluralizedName, precision = 1, - unit, - value, + unit = '', + value = 0, }: { className?: string icon: IconType @@ -19,7 +19,7 @@ const InfoBlock = ({ pluralizedName?: string precision?: number unit?: string - value: number + value?: number }) => { const name = pluralizedName ? pluralize(value, unit, pluralizedName) : pluralize(value, unit) const formattedValue = value ? `${millify(value, { precision })} ${name}` : `No ${name}` diff --git a/frontend/src/components/ItemCardList.tsx b/frontend/src/components/ItemCardList.tsx index ee23896eb8..bc852da346 100644 --- a/frontend/src/components/ItemCardList.tsx +++ b/frontend/src/components/ItemCardList.tsx @@ -11,6 +11,8 @@ import type { Release } from 'types/release' import SecondaryCard from 'components/SecondaryCard' import { TruncatedText } from 'components/TruncatedText' +type ItemCardData = Issue | Milestone | PullRequest | Release + interface AuthorAvatarProps { author: { avatarUrl: string @@ -90,40 +92,72 @@ const ItemCardList = ({
- {data.map((item, index) => ( -
-
-
- {showAvatar && ( - - - - )} -

- - - -

+ {data.map((item, index) => { + const getItemKey = (i: ItemCardData, idx: number): string => { + if ('objectID' in i && i.objectID) return i.objectID + if ('id' in i && i.id) return i.id + const repoName = 'repositoryName' in i ? i.repositoryName : '' + const title = 'title' in i ? i.title : '' + const name = 'name' in i ? i.name : '' + const url = 'url' in i ? i.url : '' + const key = `${repoName || ''}-${title || name || ''}-${url || ''}` + return key || `item-${idx}` + } + return ( +
+
+
+ {showAvatar && item.author && ( + + + + )} +

+ {'url' in item && item.url ? ( + + + + ) : ( + + )} +

+
+
+ {renderDetails(item as unknown as Parameters[0])} +
-
{renderDetails(item)}
-
- ))} + ) + })}
) : (

Nothing to display.

diff --git a/frontend/src/components/Leaders.tsx b/frontend/src/components/Leaders.tsx index 9f824b005e..8627936820 100644 --- a/frontend/src/components/Leaders.tsx +++ b/frontend/src/components/Leaders.tsx @@ -28,7 +28,7 @@ const Leaders: React.FC = ({ users }) => { {users.map((user) => ( , label: 'View Profile', diff --git a/frontend/src/components/MetricsCard.tsx b/frontend/src/components/MetricsCard.tsx index 445d06484b..d900909b96 100644 --- a/frontend/src/components/MetricsCard.tsx +++ b/frontend/src/components/MetricsCard.tsx @@ -24,13 +24,13 @@ const MetricsCard: FC<{ metric: HealthMetricsProps }> = ({ metric }) => { className={clsx( 'flex-shrink-0 rounded px-3 py-1.5 text-center text-white lg:px-4 lg:py-2 dark:text-gray-900', { - 'bg-green-500': metric.score >= 75, - 'bg-orange-500': metric.score >= 50 && metric.score < 75, - 'bg-red-500': metric.score < 50, + 'bg-green-500': (metric.score ?? 0) >= 75, + 'bg-orange-500': (metric.score ?? 0) >= 50 && (metric.score ?? 0) < 75, + 'bg-red-500': (metric.score ?? 0) < 50, } )} > -

Score: {metric.score}

+

Score: {metric.score ?? 0}


@@ -64,11 +64,13 @@ const MetricsCard: FC<{ metric: HealthMetricsProps }> = ({ metric }) => { Health Checked

- {new Date(metric.createdAt).toLocaleString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - })} + {metric.createdAt + ? new Date(metric.createdAt).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }) + : 'N/A'}

diff --git a/frontend/src/components/ModuleForm.tsx b/frontend/src/components/ModuleForm.tsx index fa1c8db336..c95642bf1a 100644 --- a/frontend/src/components/ModuleForm.tsx +++ b/frontend/src/components/ModuleForm.tsx @@ -372,7 +372,7 @@ export const ProjectSelector = ({ variables: { query: trimmedQuery }, }) - const projects = data.searchProjects || [] + const projects = data?.searchProjects || [] const filtered = projects.filter((proj) => proj.id !== value) setItems(filtered.slice(0, 5)) } catch (err) { @@ -398,16 +398,9 @@ export const ProjectSelector = ({ } }, [inputValue, fetchSuggestions]) - const handleSelectionChange = (keys: React.Key | Set | 'all') => { - let keySet: Set - if (keys instanceof Set) { - keySet = keys - } else if (keys === 'all') { - keySet = new Set() - } else { - keySet = new Set([keys]) - } - const selectedKey = Array.from(keySet as Set)[0] + const handleSelectionChange = (keys: React.Key | null) => { + if (keys === null) return + const selectedKey = keys as string if (selectedKey) { const selectedProject = items.find((item) => item.id === selectedKey) if (selectedProject) { diff --git a/frontend/src/components/MultiSearch.tsx b/frontend/src/components/MultiSearch.tsx index 32bc3596a2..51d263f433 100644 --- a/frontend/src/components/MultiSearch.tsx +++ b/frontend/src/components/MultiSearch.tsx @@ -14,6 +14,8 @@ import type { Project } from 'types/project' import type { MultiSearchBarProps, Suggestion } from 'types/search' import type { User } from 'types/user' +type SearchHit = Chapter | Event | Organization | Project | User + const MultiSearchBar: React.FC = ({ isLoaded, placeholder, @@ -83,7 +85,7 @@ const MultiSearchBar: React.FC = ({ }, [debouncedSearch]) const handleSuggestionClick = useCallback( - (suggestion: Chapter | Project | User | Event | Organization, indexName: string) => { + (suggestion: SearchHit, indexName: string) => { setSearchQuery(suggestion.name ?? '') setShowSuggestions(false) @@ -193,11 +195,7 @@ const MultiSearchBar: React.FC = ({ } } - const handleSuggestionKeyDown = ( - e: React.KeyboardEvent, - hit: Chapter | Project | User | Event | Organization, - indexName: string - ) => { + const handleSuggestionKeyDown = (e: React.KeyboardEvent, hit: SearchHit, indexName: string) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() e.stopPropagation() @@ -264,7 +262,7 @@ const MultiSearchBar: React.FC = ({
    {suggestion.hits.map((hit, subIndex) => (
  • ).key || (hit as unknown as Record).login || (hit as unknown as Record).url}`} className={`flex items-center px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 ${ highlightedIndex?.index === index && highlightedIndex?.subIndex === subIndex ? 'bg-gray-100 dark:bg-gray-700' @@ -278,7 +276,10 @@ const MultiSearchBar: React.FC = ({ className="flex w-full cursor-pointer items-center overflow-hidden border-none bg-transparent p-0 text-left focus:rounded focus:outline-2 focus:outline-offset-2 focus:outline-blue-500" > {getIconForIndex(suggestion.indexName)} - {hit.name || hit.login} + + {(hit as unknown as Record).name || + (hit as unknown as Record).login} +
  • ))} diff --git a/frontend/src/components/NavDropDown.tsx b/frontend/src/components/NavDropDown.tsx index 889dd8defb..4de110314a 100644 --- a/frontend/src/components/NavDropDown.tsx +++ b/frontend/src/components/NavDropDown.tsx @@ -10,13 +10,13 @@ interface NavDropDownProps { } export default function NavDropdown({ link, pathname }: Readonly) { const [isOpen, setIsOpen] = useState(false) - const dropdownRef = useRef(null) + const dropdownRef = useRef(null) const dropdownId = useId() // For closing dropdown when clicking outside useEffect(() => { function handleClickOutside(event: MouseEvent) { - if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false) } } diff --git a/frontend/src/components/ProgramCard.tsx b/frontend/src/components/ProgramCard.tsx index dff2c8622f..d92bf56873 100644 --- a/frontend/src/components/ProgramCard.tsx +++ b/frontend/src/components/ProgramCard.tsx @@ -28,7 +28,7 @@ const ProgramCard: React.FC = ({ program, href, accessLevel, i ], }) - const roleClass = { + const roleClass: Record = { admin: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200', mentor: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', default: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200', diff --git a/frontend/src/components/ProjectsDashboardDropDown.tsx b/frontend/src/components/ProjectsDashboardDropDown.tsx index 9a51089013..2b0e40f287 100644 --- a/frontend/src/components/ProjectsDashboardDropDown.tsx +++ b/frontend/src/components/ProjectsDashboardDropDown.tsx @@ -6,6 +6,7 @@ import { DropdownSection, Button, } from '@heroui/react' +import type { Key } from 'react' import { FC } from 'react' import type { IconType } from 'react-icons' @@ -14,7 +15,7 @@ import { IconWrapper } from 'wrappers/IconWrapper' import { DropDownSectionProps } from 'types/DropDownSectionProps' const ProjectsDashboardDropDown: FC<{ - onAction: (key: string) => void + onAction: (key: Key) => void selectedKeys?: string[] selectedLabels?: string[] selectionMode: 'single' | 'multiple' diff --git a/frontend/src/components/Release.tsx b/frontend/src/components/Release.tsx index 02179b9abe..935dfd8b50 100644 --- a/frontend/src/components/Release.tsx +++ b/frontend/src/components/Release.tsx @@ -68,14 +68,18 @@ const Release: React.FC = ({ )}

    - + {release.organizationName && release.repositoryName ? ( + + + + ) : ( - + )}

@@ -91,9 +95,9 @@ const Release: React.FC = ({ disabled={!release.organizationName || !release.repositoryName} onClick={handleClickRepository} onKeyDown={handleKeyDown} - aria-label={`View repository ${release.repositoryName}`} + aria-label={`View repository ${release.repositoryName ?? 'Unknown repository'}`} > - +
diff --git a/frontend/src/components/RepositoryCard.tsx b/frontend/src/components/RepositoryCard.tsx index cab997cc89..03c0b5a661 100644 --- a/frontend/src/components/RepositoryCard.tsx +++ b/frontend/src/components/RepositoryCard.tsx @@ -18,6 +18,8 @@ const RepositoryCard: React.FC = ({ const toggleRepositories = () => setShowAllRepositories(!showAllRepositories) + if (!repositories) return null + const displayedRepositories = showAllRepositories ? repositories : repositories.slice(0, maxInitialDisplay) diff --git a/frontend/src/components/UserCard.tsx b/frontend/src/components/UserCard.tsx index 0ef856b39c..321c1ba9f2 100644 --- a/frontend/src/components/UserCard.tsx +++ b/frontend/src/components/UserCard.tsx @@ -59,24 +59,24 @@ const UserCard = ({ )}
- {(followersCount > 0 || repositoriesCount > 0 || badgeCount > 0) && ( + {((followersCount ?? 0) > 0 || (repositoriesCount ?? 0) > 0 || (badgeCount ?? 0) > 0) && (
- {followersCount > 0 && ( + {(followersCount ?? 0) > 0 && (
- {millify(followersCount, { precision: 1 })} + {millify(followersCount as number, { precision: 1 })}
)} - {repositoriesCount > 0 && ( + {(repositoriesCount ?? 0) > 0 && (
- {millify(repositoriesCount, { precision: 1 })} + {millify(repositoriesCount as number, { precision: 1 })}
)} - {badgeCount > 0 && ( + {(badgeCount ?? 0) > 0 && (
- {millify(badgeCount, { precision: 1 })} + {millify(badgeCount as number, { precision: 1 })}
)}
diff --git a/frontend/src/components/UserMenu.tsx b/frontend/src/components/UserMenu.tsx index 9e4843ec9c..97f09da8f4 100644 --- a/frontend/src/components/UserMenu.tsx +++ b/frontend/src/components/UserMenu.tsx @@ -75,7 +75,7 @@ export default function UserMenu({ >
User avatar validateEndDateLocal(formData.endedAt), }, ] diff --git a/frontend/src/hooks/useDjangoSession.ts b/frontend/src/hooks/useDjangoSession.ts index 9a26c0905a..6742739de8 100644 --- a/frontend/src/hooks/useDjangoSession.ts +++ b/frontend/src/hooks/useDjangoSession.ts @@ -34,7 +34,7 @@ export const useDjangoSession: () => { // The cookie name is set in SESSION_COOKIE_NAME of backend/settings/base.py. syncSession({ variables: { - accessToken: (session as ExtendedSession).accessToken, + accessToken: (session as ExtendedSession).accessToken ?? '', }, }) .then((response) => { diff --git a/frontend/src/hooks/useLogout.ts b/frontend/src/hooks/useLogout.ts index 407b993115..df71382a71 100644 --- a/frontend/src/hooks/useLogout.ts +++ b/frontend/src/hooks/useLogout.ts @@ -20,7 +20,7 @@ export const useLogout = () => { await client.clearStore() // Removes Apollo cache } catch (error) { await signOut({ callbackUrl: '/' }) - throw new Error('Logout failed: ' + error.message) + throw new Error('Logout failed: ' + (error instanceof Error ? error.message : String(error))) } finally { setIsLoggingOut(false) } diff --git a/frontend/src/hooks/useSearchPage.ts b/frontend/src/hooks/useSearchPage.ts index d207ce9b29..af907559e9 100644 --- a/frontend/src/hooks/useSearchPage.ts +++ b/frontend/src/hooks/useSearchPage.ts @@ -104,7 +104,7 @@ export function useSearchPage({ if ('hits' in response) { setItems(response.hits) - setTotalPages(response.totalPages) + setTotalPages(response.totalPages ?? 0) } else { handleAppError(response) } diff --git a/frontend/src/instrumentation-client.ts b/frontend/src/instrumentation-client.ts index ccd24efde9..f3451f0cb8 100644 --- a/frontend/src/instrumentation-client.ts +++ b/frontend/src/instrumentation-client.ts @@ -11,7 +11,7 @@ import { Sentry.init({ debug: false, dsn: SENTRY_DSN, - environment: ENVIRONMENT.toLowerCase(), + environment: (ENVIRONMENT ?? 'local').toLowerCase(), integrations: [Sentry.browserTracingIntegration, Sentry.replayIntegration], release: RELEASE_VERSION, replaysOnErrorSampleRate: 0.5, diff --git a/frontend/src/sentry.server.config.ts b/frontend/src/sentry.server.config.ts index b85ab12189..df8594ea97 100644 --- a/frontend/src/sentry.server.config.ts +++ b/frontend/src/sentry.server.config.ts @@ -4,7 +4,7 @@ import { SENTRY_DSN, ENVIRONMENT, RELEASE_VERSION } from 'utils/env.client' Sentry.init({ dsn: SENTRY_DSN || '', enabled: !!SENTRY_DSN, - environment: ENVIRONMENT.toLowerCase(), + environment: (ENVIRONMENT ?? 'local').toLowerCase(), release: RELEASE_VERSION, replaysOnErrorSampleRate: 0.5, replaysSessionSampleRate: 0.5, diff --git a/frontend/src/server/apolloClient.ts b/frontend/src/server/apolloClient.ts index ee490a7d1b..b39d8b9dfb 100644 --- a/frontend/src/server/apolloClient.ts +++ b/frontend/src/server/apolloClient.ts @@ -1,4 +1,4 @@ -import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client' +import { ApolloClient, InMemoryCache, HttpLink, NormalizedCacheObject } from '@apollo/client' import { setContext } from '@apollo/client/link/context' import { cookies } from 'next/headers' import { fetchCsrfTokenServer } from 'server/fetchCsrfTokenServer' @@ -22,7 +22,9 @@ async function createApolloClient() { }) return new ApolloClient({ - cache: new InMemoryCache().restore(globalThis.__APOLLO_STATE__ ?? {}), + cache: new InMemoryCache().restore( + (globalThis as unknown as { __APOLLO_STATE__?: NormalizedCacheObject }).__APOLLO_STATE__ ?? {} + ), link: authLink.concat(httpLink), ssrMode: true, }) diff --git a/frontend/src/server/fetchCsrfToken.ts b/frontend/src/server/fetchCsrfToken.ts index 23572218cc..848375e516 100644 --- a/frontend/src/server/fetchCsrfToken.ts +++ b/frontend/src/server/fetchCsrfToken.ts @@ -3,6 +3,9 @@ import { CSRF_URL } from 'utils/env.client' export const fetchCsrfToken = async (): Promise => { try { + if (!CSRF_URL) { + throw new AppError(500, 'CSRF_URL is not configured') + } const response = await fetch(CSRF_URL, { credentials: 'include', method: 'GET', @@ -20,12 +23,14 @@ export const fetchCsrfToken = async (): Promise => { } return data.csrftoken - } catch (error) { + } catch (error: unknown) { if (error instanceof AppError) { throw error } - const message = error?.message || 'Unexpected error while fetching CSRF token' + const message = + (error instanceof Error ? error.message : String(error)) || + 'Unexpected error while fetching CSRF token' throw new AppError(500, message) } } diff --git a/frontend/src/server/fetchCsrfTokenServer.ts b/frontend/src/server/fetchCsrfTokenServer.ts index 207de6ba50..c966d8bfc8 100644 --- a/frontend/src/server/fetchCsrfTokenServer.ts +++ b/frontend/src/server/fetchCsrfTokenServer.ts @@ -1,4 +1,7 @@ export const fetchCsrfTokenServer = async (): Promise => { + if (!process.env.NEXT_SERVER_CSRF_URL) { + throw new Error('NEXT_SERVER_CSRF_URL is not configured') + } const response = await fetch(process.env.NEXT_SERVER_CSRF_URL, { credentials: 'include', method: 'GET', diff --git a/frontend/src/types/chapter.ts b/frontend/src/types/chapter.ts index 017488cbb1..891ada00f9 100644 --- a/frontend/src/types/chapter.ts +++ b/frontend/src/types/chapter.ts @@ -13,7 +13,7 @@ export type Chapter = { } createdAt?: number entityLeaders?: Leader[] - geoLocation?: GeoLocation + geoLocation?: GeoLocation | null isActive?: boolean key: string leaders?: string[] diff --git a/frontend/src/types/committee.ts b/frontend/src/types/committee.ts index d68fe9c1d1..988334428d 100644 --- a/frontend/src/types/committee.ts +++ b/frontend/src/types/committee.ts @@ -5,7 +5,7 @@ export type Committee = { createdAt: number forksCount?: number issuesCount?: number - key?: string + key: string leaders: string[] name: string objectID?: string diff --git a/frontend/src/types/healthMetrics.ts b/frontend/src/types/healthMetrics.ts index e38f1180db..6f9ac49d58 100644 --- a/frontend/src/types/healthMetrics.ts +++ b/frontend/src/types/healthMetrics.ts @@ -6,10 +6,10 @@ export type ApexLineChartSeries = { export type ApexBarChartDataSeries = { x: string y: number - fill? + fill?: string fillColor?: string strokeColor?: string - meta? + meta?: unknown goals?: { barHeightOffset?: number columnWidthOffset?: number diff --git a/frontend/src/types/markdown-it-task-lists.d.ts b/frontend/src/types/markdown-it-task-lists.d.ts new file mode 100644 index 0000000000..04a73be900 --- /dev/null +++ b/frontend/src/types/markdown-it-task-lists.d.ts @@ -0,0 +1,10 @@ +declare module 'markdown-it-task-lists' { + import MarkdownIt from 'markdown-it' + interface TaskListsOptions { + enabled?: boolean + label?: boolean + labelAfter?: boolean + } + const taskLists: (md: MarkdownIt, options?: TaskListsOptions) => void + export default taskLists +} diff --git a/frontend/src/types/mentorship.ts b/frontend/src/types/mentorship.ts index 71fbbb18e7..899c90bb2b 100644 --- a/frontend/src/types/mentorship.ts +++ b/frontend/src/types/mentorship.ts @@ -17,7 +17,7 @@ export type Program = { endedAt: string domains?: string[] tags?: string[] - userRole?: string + userRole?: string | null admins?: Contributor[] modules?: Module[] recentMilestones?: Milestone[] @@ -99,7 +99,7 @@ export type MenteeDetails = { name: string avatarUrl: string email?: string - bio?: string + bio?: string | null domains?: string[] | null tags?: string[] | null experienceLevel?: string diff --git a/frontend/src/types/milestone.ts b/frontend/src/types/milestone.ts index 9656c02b4c..f2e9128c42 100644 --- a/frontend/src/types/milestone.ts +++ b/frontend/src/types/milestone.ts @@ -1,14 +1,16 @@ import type { User } from 'types/user' export type Milestone = { - author?: User + __typename?: string + author?: User | null body?: string closedIssuesCount?: number createdAt?: string + id?: string openIssuesCount?: number - organizationName?: string + organizationName?: string | null progress?: number - repositoryName?: string + repositoryName?: string | null state?: string title: string url?: string diff --git a/frontend/src/types/project.ts b/frontend/src/types/project.ts index e954cdd013..23ce6f7f32 100644 --- a/frontend/src/types/project.ts +++ b/frontend/src/types/project.ts @@ -68,7 +68,7 @@ export type RepositoryCardProps = { key?: string name: string openIssuesCount: number - organization?: Organization + organization?: Organization | null starsCount: number subscribersCount?: number url: string diff --git a/frontend/src/types/pullRequest.ts b/frontend/src/types/pullRequest.ts index d028bd488b..41e9e0e935 100644 --- a/frontend/src/types/pullRequest.ts +++ b/frontend/src/types/pullRequest.ts @@ -1,7 +1,7 @@ import type { User } from 'types/user' export type PullRequest = { - author?: User + author?: User | null createdAt: string | number id?: string mergedAt?: string diff --git a/frontend/src/types/release.ts b/frontend/src/types/release.ts index c82d2225c2..085f2478d6 100644 --- a/frontend/src/types/release.ts +++ b/frontend/src/types/release.ts @@ -1,14 +1,16 @@ import type { RepositoryDetails, User } from 'types/user' export type Release = { - author?: User + __typename?: string + author?: User | null id: string isPreRelease?: boolean name: string - organizationName?: string + organizationName?: string | null projectName?: string publishedAt: number repository?: RepositoryDetails - repositoryName: string + repositoryName?: string | null tagName: string + url?: string } diff --git a/frontend/src/utils/helpers/githubHeatmap.ts b/frontend/src/utils/helpers/githubHeatmap.ts index fd06f967c7..6f3939fad2 100644 --- a/frontend/src/utils/helpers/githubHeatmap.ts +++ b/frontend/src/utils/helpers/githubHeatmap.ts @@ -52,7 +52,8 @@ export const fetchHeatmapData = async (username: string): Promise new Date(item.date) <= endDate && new Date(item.date) >= startDate + (item: HeatmapContribution) => + new Date(item.date) <= endDate && new Date(item.date) >= startDate ) const allDates: string[] = [] @@ -61,7 +62,9 @@ export const fetchHeatmapData = async (username: string): Promise { - const contribution = heatmapData.contributions.find((c) => c.date === date) + const contribution = heatmapData.contributions.find( + (c: HeatmapContribution) => c.date === date + ) return contribution ? { date: contribution.date, diff --git a/frontend/src/utils/metaconfig.ts b/frontend/src/utils/metaconfig.ts index bfd34dda7c..40e85b7b4a 100644 --- a/frontend/src/utils/metaconfig.ts +++ b/frontend/src/utils/metaconfig.ts @@ -65,7 +65,10 @@ export function generateSeoMetadata({ } } -export function getStaticMetadata(pageKey, canonicalPath?: string): Metadata { +export function getStaticMetadata( + pageKey: keyof typeof METADATA_CONFIG, + canonicalPath?: string +): Metadata { if (!METADATA_CONFIG[pageKey]) { throw new Error(`No metadata configuration found for key: ${pageKey}`) } @@ -76,6 +79,6 @@ export function getStaticMetadata(pageKey, canonicalPath?: string): Metadata { description: config.description, keywords: config.keywords, title: config.pageTitle, - type: config.type, + type: config.type as 'website' | 'article' | 'profile', }) } diff --git a/frontend/src/utils/structuredData.ts b/frontend/src/utils/structuredData.ts index 319428994b..9fac0c626f 100644 --- a/frontend/src/utils/structuredData.ts +++ b/frontend/src/utils/structuredData.ts @@ -3,7 +3,7 @@ import type { User } from 'types/user' export const formatISODate = (input?: number | string): string => { if (input == null) { - return undefined + return '' } const date = @@ -48,12 +48,12 @@ export function generateProfilePageStructuredData( description: user.bio, identifier: user.login, image: user.avatarUrl, - ...(user.followersCount > 0 && { + ...((user.followersCount ?? 0) > 0 && { interactionStatistic: [ { '@type': 'InteractionCounter', interactionType: 'https://schema.org/FollowAction', - userInteractionCount: user.followersCount, + userInteractionCount: user.followersCount ?? 0, }, ], }), @@ -63,7 +63,9 @@ export function generateProfilePageStructuredData( url: 'https://nest.owasp.org/members', }, name: user.name || user.login, - sameAs: [user.url], + ...(user.url && { + sameAs: [user.url], + }), url: `${baseUrl}/members/${user.login}`, ...(user.company && { worksFor: { diff --git a/frontend/src/wrappers/provider.tsx b/frontend/src/wrappers/provider.tsx index 0d935724c9..974bdb304d 100644 --- a/frontend/src/wrappers/provider.tsx +++ b/frontend/src/wrappers/provider.tsx @@ -22,6 +22,14 @@ export function Providers({ }: Readonly<{ children: React.ReactNode }>) { + if (!apolloClient) { + return ( +
+ Configuration Error: GraphQL Client failed to initialize +
+ ) + } + return ( diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 6789ce40a1..58b2b35fe0 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -34,7 +34,7 @@ "utils/*": ["src/utils/*"], "wrappers/*": ["src/wrappers/*"] }, - "strict": false + "strict": true }, "include": [ ".next/dev/types/**/*.ts", @@ -44,5 +44,5 @@ "next-env.d.ts", "src" ], - "exclude": ["node_modules"] + "exclude": ["__tests__/**/*", "jest.setup.ts", "node_modules"] }