+ {snapshot.contributionHeatmapData &&
+ Object.keys(snapshot.contributionHeatmapData).length > 0 && (
+
+ )}
+
+ {/* Slack Communications Heatmap */}
+ {snapshot?.communicationHeatmapData &&
+ Object.keys(snapshot.communicationHeatmapData).length > 0 && (
+
)}
- {/* Slack Communications Heatmap */}
- {snapshot?.communicationHeatmapData &&
- Object.keys(snapshot.communicationHeatmapData).length > 0 && (
-
-
+ {renderTopActiveChannels()}
+
+ {/* Additional Information */}
+ {(candidate.member?.isOwaspBoardMember ||
+ candidate.member?.isFormerOwaspStaff ||
+ candidate.member?.isGsocMentor ||
+ leadsFlagshipProject) && (
+
+
+ Additional Information
+
+
+ {candidate.member?.isOwaspBoardMember && (
+
+ OWASP Board of Directors Member
+
+ )}
+ {candidate.member?.isFormerOwaspStaff && (
+
+ Former OWASP Staff Member
+
+ )}
+ {candidate.member?.isGsocMentor && (
+
+ Google Summer of Code Mentor
+
+ )}
+
+ {leadsFlagshipProject && (
+
+ This candidate may have additional community engagement in other Slack workspaces
+ based on the flagship level project(s) they are leading.
)}
+
+ )}
+
+ )
+}
+
+const BoardCandidatesPage = () => {
+ const { year } = useParams<{ year: string }>()
+ const {
+ data: graphQLData,
+ error: graphQLRequestError,
+ loading,
+ } = useQuery(GetBoardCandidatesDocument, {
+ variables: { year: Number.parseInt(year) },
+ })
- {renderTopActiveChannels()}
+ // Derive candidates directly from GraphQL data to avoid an extra render
+ const candidates: CandidateWithSnapshot[] = graphQLData?.boardOfDirectors?.candidates ?? []
- {/* Additional Information */}
- {(candidate.member?.isOwaspBoardMember ||
- candidate.member?.isFormerOwaspStaff ||
- candidate.member?.isGsocMentor ||
- leadsFlagshipProject) && (
-
-
- Additional Information
-
-
- {candidate.member?.isOwaspBoardMember && (
-
- OWASP Board of Directors Member
-
- )}
- {candidate.member?.isFormerOwaspStaff && (
-
- Former OWASP Staff Member
-
- )}
- {candidate.member?.isGsocMentor && (
-
- Google Summer of Code Mentor
-
- )}
-
- {leadsFlagshipProject && (
-
- This candidate may have additional community engagement in other Slack workspaces
- based on the flagship level project(s) they are leading.
-
- )}
-
- )}
-
- )
- }
+ // Keep reporting errors as a side-effect only
+ useEffect(() => {
+ if (graphQLRequestError) {
+ handleAppError(graphQLRequestError)
+ }
+ }, [graphQLRequestError])
if (loading) {
return
@@ -752,7 +737,7 @@ const BoardCandidatesPage = () => {
) : (
{candidates.map((candidate) => (
-
+
))}
)}
diff --git a/frontend/src/app/global-error.tsx b/frontend/src/app/global-error.tsx
index 6b0c5442b9..9532023d39 100644
--- a/frontend/src/app/global-error.tsx
+++ b/frontend/src/app/global-error.tsx
@@ -91,19 +91,29 @@ export const handleAppError = (error: unknown) => {
export const ErrorWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
{
- Sentry.captureException(error)
- const errorConfig = ERROR_CONFIGS['500']
- return
- }}
+ fallback={(props) => }
>
{children}
)
}
+export const SentryErrorFallback: React.FC<{
+ error: unknown
+ errorConfig?: ErrorDisplayProps
+}> = ({ error, errorConfig = ERROR_CONFIGS['500'] }) => {
+ React.useEffect(() => {
+ Sentry.captureException(error instanceof Error ? error : new Error(String(error)))
+ }, [error])
+
+ return
+}
+
export default function GlobalError({ error }: Readonly<{ error: Error }>) {
- Sentry.captureException(error)
+ React.useEffect(() => {
+ Sentry.captureException(error)
+ }, [error])
+
const errorConfig = ERROR_CONFIGS['500']
return
}
diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx
index 962008aed4..dabcbeca37 100644
--- a/frontend/src/app/members/[memberKey]/page.tsx
+++ b/frontend/src/app/members/[memberKey]/page.tsx
@@ -9,12 +9,76 @@ import { handleAppError, ErrorDisplay } from 'app/global-error'
import { GetUserDataDocument } from 'types/__generated__/userQueries.generated'
import { Badge } from 'types/badge'
+import { User } from 'types/user'
import { formatDate } from 'utils/dateFormatter'
import Badges from 'components/Badges'
import DetailsCard from 'components/CardDetailsPage'
import ContributionHeatmap from 'components/ContributionHeatmap'
import MemberDetailsPageSkeleton from 'components/skeletons/MemberDetailsPageSkeleton'
+type DateRange = { startDate: string; endDate: string }
+
+interface UserSummaryProps {
+ user: User | null
+ contributionData: Record
+ dateRange: DateRange
+ hasContributionData: boolean
+ formattedBio: React.ReactNode
+}
+
+export const UserSummary: React.FC = ({
+ user,
+ contributionData,
+ dateRange,
+ hasContributionData,
+ formattedBio,
+}) => (
+
+
+
+
+
+
+ @{user?.login}
+
+ {user?.badges && user.badges.length > 0 && (
+
+ {user.badges.slice().map((badge: Badge) => (
+
+
+
+ ))}
+
+ )}
+
+
{formattedBio}
+
+ {hasContributionData && dateRange.startDate && dateRange.endDate && (
+
+ )}
+
+
+)
+
const UserDetailsPage: React.FC = () => {
const { memberKey } = useParams<{ memberKey: string }>()
@@ -129,56 +193,6 @@ const UserDetailsPage: React.FC = () => {
{ icon: FaCodeMerge, value: user?.contributionsCount || 0, unit: 'Contribution' },
]
- const UserSummary = () => (
-
-
-
-
-
-
- @{user?.login}
-
- {user?.badges && user.badges.length > 0 && (
-
- {user.badges.slice().map((badge: Badge) => (
-
-
-
- ))}
-
- )}
-
-
{formattedBio}
-
- {hasContributionData && dateRange.startDate && dateRange.endDate && (
-
- )}
-
-
- )
-
return (
{
stats={userStats}
title={user?.name || user?.login}
type="user"
- userSummary={}
+ userSummary={
+
+ }
/>
)
}
diff --git a/frontend/src/components/SkeletonsBase.tsx b/frontend/src/components/SkeletonsBase.tsx
index 96577819bd..1519b5943f 100644
--- a/frontend/src/components/SkeletonsBase.tsx
+++ b/frontend/src/components/SkeletonsBase.tsx
@@ -1,4 +1,5 @@
import { Skeleton } from '@heroui/skeleton'
+import type { CardSkeletonProps } from 'types/skeleton'
import LoadingSpinner from 'components/LoadingSpinner'
import AboutSkeleton from 'components/skeletons/AboutSkeleton'
import CardSkeleton from 'components/skeletons/Card'
@@ -6,6 +7,8 @@ import MemberDetailsPageSkeleton from 'components/skeletons/MemberDetailsPageSke
import OrganizationDetailsPageSkeleton from 'components/skeletons/OrganizationDetailsPageSkeleton'
import SnapshotSkeleton from 'components/skeletons/SnapshotSkeleton'
import UserCardSkeleton from 'components/skeletons/UserCard'
+
+// Use CardSkeleton directly; wrapper removed
function userCardRender() {
const cardCount = 12
return (
@@ -35,31 +38,26 @@ const SkeletonBase = ({
indexName: string
loadingImageUrl: string
}) => {
- let Component
+ let componentProps: CardSkeletonProps = {}
+
switch (indexName) {
case 'chapters':
- Component = () =>
+ componentProps = { showLevel: false, showIcons: false, showLink: false }
break
case 'issues':
- Component = () => (
-
- )
+ componentProps = {
+ showLevel: false,
+ showIcons: true,
+ numIcons: 2,
+ showContributors: false,
+ showSocial: false,
+ }
break
case 'projects':
- Component = () => (
-
- )
+ componentProps = { showLink: false, showSocial: false, showIcons: true, numIcons: 3 }
break
case 'committees':
- Component = () => (
-
- )
+ componentProps = { showLink: false, showLevel: false, showIcons: true, numIcons: 1 }
break
case 'users':
return userCardRender()
@@ -82,11 +80,11 @@ const SkeletonBase = ({
{indexName == 'chapters' ? (
) : (
-
+
)}
-
-
-
+
+
+