From 19b2131081655064f119decf2fe14633dc44c6e3 Mon Sep 17 00:00:00 2001 From: adithya-naik Date: Thu, 31 Jul 2025 21:24:51 +0530 Subject: [PATCH 1/9] Improve user details UX on member page (#1889) --- frontend/src/components/CardDetailsPage.tsx | 71 +++++++++++++++++---- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 4eb1e9f18f..fa10cd7c8d 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -55,6 +55,53 @@ const DetailsCard = ({ type, userSummary, }: DetailsCardProps) => { + // Helper function to render detail values with appropriate links + const renderDetailValue = (detail: { label: string; value: string }) => { + const { label, value } = detail + + // Don't render links for empty/placeholder values + if (!value || value === "N/A" || value === "Not available" || value === "Unknown") { + return value + } + + switch (label) { + case "Email": + // Render email as mailto link + return ( + + {value} + + ) + + case "Company": + // Render company as GitHub link if it starts with @ + if (value.startsWith("@")) { + const companyName = value.slice(1) // Remove @ prefix + return ( + + {value} + + ) + } + // Return as plain text if no @ prefix + return value + + default: + // Return other fields as plain text + return value + } + } + return (
@@ -103,14 +150,14 @@ const DetailsCard = ({ {details?.map((detail) => detail?.label === 'Leaders' ? (
- {detail.label}:{' '} + {detail.label}:{' '}
) : (
- {detail.label}: {detail?.value || 'Unknown'} + {detail.label}: {renderDetailValue(detail)}
- ) + ), )} {socialLinks && (type === 'chapter' || type === 'committee') && ( @@ -139,7 +186,7 @@ const DetailsCard = ({ ))} )} - {type === 'chapter' && geolocationData && ( + {type === 'chapter' && geolocationData && (
)} - {(type === 'project' || + {(type === 'project' || type === 'repository' || type === 'user' || type === 'organization') && ( @@ -207,16 +254,16 @@ const DetailsCard = ({
)} - {(type === 'project' || type === 'user' || type === 'organization') && - repositories.length > 0 && ( - }> - - - )} + {(type === 'project' || type === 'user' || type === 'organization') && + repositories.length > 0 && ( + }> + + + )} {IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && ( )} - {entityKey && ['chapter', 'project', 'repository'].includes(type) && ( + {entityKey && ['chapter', 'project', 'repository'].includes(type) && ( Date: Thu, 31 Jul 2025 22:07:06 +0530 Subject: [PATCH 2/9] =?UTF-8?q?=E2=9C=85=20refactor:=20cleanup=20and=20opt?= =?UTF-8?q?imizations=20in=20DetailsCard=20as=20suggested-#1889?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/CardDetailsPage.tsx | 30 +++++++++++++-------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index fa10cd7c8d..e6e8cb87f3 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -10,6 +10,7 @@ import { } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Link from 'next/link' +import { useCallback } from "react" import type { DetailsCardProps } from 'types/card' import { capitalize } from 'utils/capitalize' import { IS_PROJECT_HEALTH_ENABLED } from 'utils/credentials' @@ -29,6 +30,10 @@ import SecondaryCard from 'components/SecondaryCard' import SponsorCard from 'components/SponsorCard' import ToggleableList from 'components/ToggleableList' import TopContributorsList from 'components/TopContributorsList' +// Email validation regex +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ +// Helper to sanitize URLs +const sanitizeForUrl = (str: string) => encodeURIComponent(str.trim()) const DetailsCard = ({ description, @@ -56,7 +61,7 @@ const DetailsCard = ({ userSummary, }: DetailsCardProps) => { // Helper function to render detail values with appropriate links - const renderDetailValue = (detail: { label: string; value: string }) => { + const renderDetailValue = useCallback((detail: { label: string; value: string }) => { const { label, value } = detail // Don't render links for empty/placeholder values @@ -66,10 +71,13 @@ const DetailsCard = ({ switch (label) { case "Email": - // Render email as mailto link + // Render email as mailto link if valid + if (!EMAIL_REGEX.test(value)) { + return value // Return as plain text for invalid emails + } return ( @@ -80,7 +88,7 @@ const DetailsCard = ({ case "Company": // Render company as GitHub link if it starts with @ if (value.startsWith("@")) { - const companyName = value.slice(1) // Remove @ prefix + const companyName = sanitizeForUrl(value.slice(1)) // Remove @ prefix and sanitize return ( @@ -117,7 +125,7 @@ const DetailsCard = ({ {!isActive && ( Inactive - + )}

{description}

@@ -150,7 +158,7 @@ const DetailsCard = ({ {details?.map((detail) => detail?.label === 'Leaders' ? (
- {detail.label}:{' '} + {detail.label}:{' '}
) : ( @@ -225,7 +233,7 @@ const DetailsCard = ({ maxInitialDisplay={12} /> )} - {(type === 'project' || + {(type === 'project' || type === 'repository' || type === 'user' || type === 'organization') && ( @@ -254,8 +262,8 @@ const DetailsCard = ({
)} - {(type === 'project' || type === 'user' || type === 'organization') && - repositories.length > 0 && ( + {(type === 'project' || type === 'user' || type === 'organization') && + repositories.length > 0 && ( }> @@ -263,7 +271,7 @@ const DetailsCard = ({ {IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && ( )} - {entityKey && ['chapter', 'project', 'repository'].includes(type) && ( + {entityKey && ['chapter', 'project', 'repository'].includes(type) && ( Date: Fri, 1 Aug 2025 21:29:50 +0530 Subject: [PATCH 3/9] refactor/handled ReDoS vulnerability --- frontend/src/components/CardDetailsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index e6e8cb87f3..8defb0eacf 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -31,7 +31,7 @@ import SponsorCard from 'components/SponsorCard' import ToggleableList from 'components/ToggleableList' import TopContributorsList from 'components/TopContributorsList' // Email validation regex -const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i // Helper to sanitize URLs const sanitizeForUrl = (str: string) => encodeURIComponent(str.trim()) From 23984ac40df70369bc79ede1ad2413f037990f34 Mon Sep 17 00:00:00 2001 From: adithya-naik Date: Tue, 5 Aug 2025 10:12:28 +0530 Subject: [PATCH 4/9] removed excess coomments-#1889 --- frontend/src/components/CardDetailsPage.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 8defb0eacf..d0a3c5abb8 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -30,9 +30,7 @@ import SecondaryCard from 'components/SecondaryCard' import SponsorCard from 'components/SponsorCard' import ToggleableList from 'components/ToggleableList' import TopContributorsList from 'components/TopContributorsList' -// Email validation regex const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i -// Helper to sanitize URLs const sanitizeForUrl = (str: string) => encodeURIComponent(str.trim()) const DetailsCard = ({ @@ -60,20 +58,16 @@ const DetailsCard = ({ type, userSummary, }: DetailsCardProps) => { - // Helper function to render detail values with appropriate links const renderDetailValue = useCallback((detail: { label: string; value: string }) => { const { label, value } = detail - - // Don't render links for empty/placeholder values if (!value || value === "N/A" || value === "Not available" || value === "Unknown") { return value } switch (label) { case "Email": - // Render email as mailto link if valid if (!EMAIL_REGEX.test(value)) { - return value // Return as plain text for invalid emails + return value } return ( ) } - // Return as plain text if no @ prefix return value default: - // Return other fields as plain text return value } }, []) From 7c7ac7987f07debe53b6622730b23a6af659b83d Mon Sep 17 00:00:00 2001 From: adithya-naik Date: Tue, 5 Aug 2025 10:19:06 +0530 Subject: [PATCH 5/9] Resolved merge conflict in CardDetailsPage.tsx --- frontend/src/components/CardDetailsPage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index d0a3c5abb8..d7a65ca04e 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -255,10 +255,10 @@ const DetailsCard = ({ )} {(type === 'project' || type === 'user' || type === 'organization') && repositories.length > 0 && ( - }> - - - )} + }> + + + )} {IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && ( )} From 866da53e8e0bbbeb27d00057e144084194093f50 Mon Sep 17 00:00:00 2001 From: Kate Date: Fri, 8 Aug 2025 18:59:50 -0700 Subject: [PATCH 6/9] Fix tests --- .../unit/components/CardDetailsPage.test.tsx | 2 +- frontend/src/components/CardDetailsPage.tsx | 91 +++++++++++-------- 2 files changed, 54 insertions(+), 39 deletions(-) diff --git a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx index 4a74d9e262..21f3e4d33d 100644 --- a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx +++ b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx @@ -810,7 +810,7 @@ describe('CardDetailsPage', () => { render() - expect(screen.getAllByText('Unknown')).toHaveLength(3) + expect(screen.getAllByText('N/A')).toHaveLength(3) }) it('handles empty languages and topics arrays', () => { diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index d7a65ca04e..e92894e083 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -10,7 +10,8 @@ import { } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Link from 'next/link' -import { useCallback } from "react" +import { useCallback } from 'react' +import type { JSX } from 'react' import type { DetailsCardProps } from 'types/card' import { capitalize } from 'utils/capitalize' import { IS_PROJECT_HEALTH_ENABLED } from 'utils/credentials' @@ -58,48 +59,62 @@ const DetailsCard = ({ type, userSummary, }: DetailsCardProps) => { - const renderDetailValue = useCallback((detail: { label: string; value: string }) => { - const { label, value } = detail - if (!value || value === "N/A" || value === "Not available" || value === "Unknown") { - return value - } + const renderDetailValue = useCallback( + (detail: { label: string; value: string | JSX.Element }) => { + const { label, value } = detail - switch (label) { - case "Email": - if (!EMAIL_REGEX.test(value)) { - return value - } - return ( - - {value} - - ) + if ( + value == null || + value === '' || + value === 'N/A' || + value === 'Not available' || + value === 'Unknown' + ) { + return 'N/A' + } + + if (typeof value !== 'string') { + return value + } - case "Company": - if (value.startsWith("@")) { - const companyName = sanitizeForUrl(value.slice(1)) + switch (label) { + case 'Email': + if (!EMAIL_REGEX.test(value)) { + return value + } return ( {value} ) - } - return value - default: - return value - } - }, []) + case 'Company': + if (value.startsWith('@')) { + const companyName = sanitizeForUrl(value.slice(1)) + return ( + + {value} + + ) + } + return value + + default: + return value + } + }, + [] + ) return (
@@ -116,7 +131,7 @@ const DetailsCard = ({ {!isActive && ( Inactive - + )}

{description}

@@ -156,7 +171,7 @@ const DetailsCard = ({
{detail.label}: {renderDetailValue(detail)}
- ), + ) )} {socialLinks && (type === 'chapter' || type === 'committee') && ( @@ -185,7 +200,7 @@ const DetailsCard = ({ ))} )} - {type === 'chapter' && geolocationData && ( + {type === 'chapter' && geolocationData && (
)} {(type === 'project' || type === 'user' || type === 'organization') && - repositories.length > 0 && ( + repositories.length > 0 && ( }> From c79faf5ef8e0f69f3a58322fd7643e6d6d181852 Mon Sep 17 00:00:00 2001 From: Kate Date: Fri, 8 Aug 2025 20:48:20 -0700 Subject: [PATCH 7/9] Update n/a vs unknown --- frontend/__tests__/unit/components/CardDetailsPage.test.tsx | 2 +- frontend/__tests__/unit/pages/UserDetails.test.tsx | 2 +- frontend/src/components/CardDetailsPage.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx index 21f3e4d33d..4a74d9e262 100644 --- a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx +++ b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx @@ -810,7 +810,7 @@ describe('CardDetailsPage', () => { render() - expect(screen.getAllByText('N/A')).toHaveLength(3) + expect(screen.getAllByText('Unknown')).toHaveLength(3) }) it('handles empty languages and topics arrays', () => { diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index 75bdf32930..222fb45c54 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -465,7 +465,7 @@ describe('UserDetailsPage', () => { render() await waitFor(() => { - expect(screen.getAllByText('N/A').length).toBe(3) + expect(screen.getAllByText('Unknown').length).toBe(3) }) }) test('does not render sponsor block', async () => { diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index e92894e083..8bbb481c29 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -70,7 +70,7 @@ const DetailsCard = ({ value === 'Not available' || value === 'Unknown' ) { - return 'N/A' + return 'Unknown' } if (typeof value !== 'string') { From 112b7034e472ac2a58501a7c391cbd541ceafa69 Mon Sep 17 00:00:00 2001 From: Jatoth Adithya Naik Date: Sun, 31 Aug 2025 17:47:08 +0530 Subject: [PATCH 8/9] unknown to N/A --- frontend/src/components/CardDetailsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 2510f962c2..239712fec0 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -84,7 +84,7 @@ const DetailsCard = ({ value === 'Not available' || value === 'Unknown' ) { - return 'Unknown' + return 'N/A' } if (typeof value !== 'string') { From bfa5c0df693eb1b30f02bfbc207f451f2f971d1a Mon Sep 17 00:00:00 2001 From: Jatoth Adithya Naik Date: Sun, 31 Aug 2025 17:48:59 +0530 Subject: [PATCH 9/9] in tests -- unknown to N/A --- frontend/__tests__/unit/pages/UserDetails.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index 222fb45c54..75bdf32930 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -465,7 +465,7 @@ describe('UserDetailsPage', () => { render() await waitFor(() => { - expect(screen.getAllByText('Unknown').length).toBe(3) + expect(screen.getAllByText('N/A').length).toBe(3) }) }) test('does not render sponsor block', async () => {