Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

214 changes: 188 additions & 26 deletions src/pages/ContributorsPage.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,57 @@
import React, { useState, useMemo } from 'react'
import React, { useState, useMemo, useEffect, useRef } from 'react'
import { FiDatabase, FiDownload } from 'react-icons/fi'
import { useApp } from '../context/AppContext'
import { C, SortTh, PageTitle, LoadMore } from '../components/UI'
import { useSortedData } from '../hooks/useSortedData'
import { computeBusFactor, exportContributorsCSV } from '../services/analytics'
import { useNavigate } from 'react-router-dom'
import EmptyStateCard from '../components/EmptyStateCard'
import { BsFillInfoSquareFill } from "react-icons/bs";

export default function ContributorsPage() {
const { model } = useApp()
const [search, setSearch] = useState('')
const [shown, setShown] = useState(20)
const [shown, setShown] = useState(20)
const [openInfo, setOpenInfo] = useState(null)
const busFactorRef = useRef(null)
const freshnessRef = useRef(null)
const signalRef = useRef(null)
Comment on lines 11 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add "use client" directive for client-side hooks.

This component uses client-side React hooks (useState, useEffect, useRef) but is missing the "use client" directive required in NextJS 13+ App Router. Without it, the component will fail when rendered as a server component.

As per coding guidelines, ensure that "use client" is being used for components with client-side features.

🔧 Add the directive at the top of the file
+"use client"
+
 import React, { useState, useMemo, useEffect, useRef } from 'react'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default function ContributorsPage() {
const { model } = useApp()
const [search, setSearch] = useState('')
const [shown, setShown] = useState(20)
const [shown, setShown] = useState(20)
const [openInfo, setOpenInfo] = useState(null)
const busFactorRef = useRef(null)
const freshnessRef = useRef(null)
const signalRef = useRef(null)
"use client"
import React, { useState, useMemo, useEffect, useRef } from 'react'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/ContributorsPage.jsx` around lines 11 - 18, This component
(ContributorsPage) uses client-side hooks (useState, useEffect, useRef) so add
the Next.js client directive by placing "use client" as the very first line of
the file so the component is treated as a client component; ensure the string is
before the import/exports and leave all existing hooks (useState, useEffect,
useRef) and functions intact so ContributorsPage continues to use client-side
behavior correctly.

Source: Coding guidelines


useEffect(() => {
const handleClickOutside = (e) => {
if (
busFactorRef.current &&
busFactorRef.current.contains(e.target)
) return

if (
freshnessRef.current &&
freshnessRef.current.contains(e.target)
) return

if (
signalRef.current &&
signalRef.current.contains(e.target)
) return

setOpenInfo(null)
}

document.addEventListener('mousedown', handleClickOutside)

return () =>
document.removeEventListener('mousedown', handleClickOutside)
}, [])

if (!model) return null
const { contributors } = model
const navigate = useNavigate()

const busFactor = useMemo(() => computeBusFactor(contributors), [contributors])
const topActive = contributors.slice(0, 10).filter(c => c.freshness > 50).length
const freshPct = contributors.length ? Math.round(topActive / Math.min(10, contributors.length) * 100) : 0
const busFactor = useMemo(() => computeBusFactor(contributors), [contributors])
const topActive = contributors.slice(0, 10).filter(c => c.freshness > 50).length
const freshPct = contributors.length ? Math.round(topActive / Math.min(10, contributors.length) * 100) : 0
const connectors = contributors.filter(c => c.isConnector)
const crossOrg = contributors.filter(c => c.isCrossOrg)
const crossOrg = contributors.filter(c => c.isCrossOrg)

const filtered = useMemo(() =>
contributors.filter(c => !search || c.login.toLowerCase().includes(search.toLowerCase())),
Expand All @@ -30,13 +61,13 @@ export default function ContributorsPage() {
const visible = sorted.slice(0, shown)

const riskColor = r => r === 'critical' ? 'var(--red)' : r === 'high' ? 'var(--amber)' : 'var(--green)'
const riskBar = r => r === 'critical' ? '90%' : r === 'high' ? '60%' : '25%'
const riskBar = r => r === 'critical' ? '90%' : r === 'high' ? '60%' : '25%'

return (
<div style={{ padding: '32px 24px', maxWidth: 1100, margin: '0 auto' }} className="fade-up">
<PageTitle
title="Contributor Intelligence"
subtitle="Analyzing contribution patterns, coverage risk, and organizational health — sorted by coverage and recency, not by commit count"
subtitle="Analyzing contribution patterns, coverage risk, and organizational health"
right={
<button onClick={() => exportContributorsCSV(contributors)} style={{ ...C.btn('ghost'), fontSize: 12, display: 'flex', alignItems: 'center', gap: 5 }}>
<FiDownload size={13} /> Export CSV
Expand All @@ -52,7 +83,42 @@ export default function ContributorsPage() {
...C.card,
borderColor: busFactor.risk === 'critical' ? 'rgba(239,68,68,.4)' : busFactor.risk === 'high' ? 'rgba(245,158,11,.4)' : 'var(--border)',
}}>
<div style={{ ...C.label, marginBottom: 8 }}>Bus Factor Risk</div>
<div ref={busFactorRef}
style={{ ...C.label, marginBottom: 8, position: 'relative' }}
className="flex justify-between items-center"
>
<p>Bus Factor Risk</p>

<button
onClick={() =>
setOpenInfo(openInfo === 'busfactor' ? null : 'busfactor')
}
className="p-2 rounded-full hover:bg-zinc-800 transition"
>
<BsFillInfoSquareFill className="text-white cursor-pointer" />
</button>

{openInfo === 'busfactor' && (
<div style={{ ...C.card, position: 'absolute', top: '120%', right: 0, width: '320px', zIndex: 100 }}>
<div className='text-xs text-(--text)'>
<h4 style={{ marginBottom: 8 }} className='text-(--accent)'>Bus Factor</h4>

<p> Measures contributor concentration risk. </p>

<ul style={{ marginLeft: 16 }}>
<li>1 = Critical Risk</li>
<li>2 = High Risk</li>
<li>3+ = Healthy Distribution</li>
</ul>
</div>

<p style={{ marginTop: 8 }}>
Higher values indicate knowledge is distributed across more contributors,
reducing dependency on a small number of individuals.
</p>
</div>
)}
</div>
Comment on lines +86 to +121

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Externalize user-visible strings for internationalization.

The popover contains hardcoded user-visible strings like "Bus Factor Risk", "Measures contributor concentration risk", "1 = Critical Risk", etc. As per coding guidelines, user-visible strings should be externalized to resource files (i18n).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/ContributorsPage.jsx` around lines 86 - 121, Replace hardcoded UI
strings in the ContributorsPage popover with i18n resource keys: move texts like
"Bus Factor Risk", "Bus Factor", "Measures contributor concentration risk.",
list items "1 = Critical Risk", "2 = High Risk", "3+ = Healthy Distribution",
and the paragraph about higher values into your localization files and reference
them via the app's translation helper (e.g., t('contributors.busFactor.title'),
t('contributors.busFactor.description'), etc.) inside the component that uses
busFactorRef, openInfo, setOpenInfo and the popover JSX (the element using
C.card and BsFillInfoSquareFill); update the JSX to call the translation
function for each user-visible string and ensure any punctuation or markup
remains in the component while text lives in the resource file.

Source: Coding guidelines

<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<div style={{ fontSize: 22, fontWeight: 700 }}>Bus Factor: {busFactor.factor}</div>
<span style={{
Expand All @@ -79,7 +145,53 @@ export default function ContributorsPage() {

{/* Freshness Index */}
<div style={C.card}>
<div style={{ ...C.label, marginBottom: 12 }}>Freshness Index</div>
<div ref={freshnessRef}
style={{ ...C.label, marginBottom: 12, position: 'relative' }}
className="flex justify-between items-center"
>
<p>Freshness Index</p>

<button
onClick={() =>
setOpenInfo(openInfo === 'freshness' ? null : 'freshness')
}
className="p-2 rounded-full hover:bg-zinc-800 transition"
>
<BsFillInfoSquareFill className="text-white cursor-pointer" />
</button>

{openInfo === 'freshness' && (
<div
style={{
...C.card,
position: 'absolute',
top: '120%',
right: 0,
width: '320px',
zIndex: 100,
}}
className='text-xs'
>
<div className="text-(--text) text-xs">
<h4 className='text-(--accent)'>Freshness Index</h4>

<p>
Measures how active and recently engaged the contributor community is.
</p>

<ul className="ml-2">
<li><strong>High Score</strong> = Contributors active recently</li>
<li><strong>Medium Score</strong> = Some recent activity</li>
<li><strong>Low Score</strong> = Limited recent participation</li>
</ul>
</div>

<p style={{ marginTop: 8 }}>
Higher values indicate stronger project momentum and ongoing maintenance.
</p>
</div>
)}
</div>
Comment on lines +148 to +194

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Externalize user-visible strings for internationalization.

The Freshness Index popover contains hardcoded user-visible strings like "Freshness Index", "Measures how active and recently engaged...", "High Score", etc. As per coding guidelines, user-visible strings should be externalized to resource files (i18n).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/ContributorsPage.jsx` around lines 148 - 194, Extract all hardcoded
user-facing strings inside the Freshness Index popover (e.g., the labels and
paragraphs rendered when openInfo === 'freshness', including "Freshness Index",
the descriptive paragraph, "High Score"/"Medium Score"/"Low Score", and the
final sentence) into your i18n resource files and replace them with lookups from
your localization utility (the same pattern used elsewhere in
ContributorsPage.jsx). Update the JSX to call the i18n keys (use the existing
state/symbols like freshnessRef, openInfo, setOpenInfo and keep the popover
rendering logic and styles intact) and add new keys to the locale resources for
each string so translations can be provided.

Source: Coding guidelines

<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 12 }}>
<div style={{
width: 64, height: 64, borderRadius: '50%', flexShrink: 0,
Expand Down Expand Up @@ -122,7 +234,7 @@ export default function ContributorsPage() {
style={{ ...C.input, width: 220 }}
/>
<span style={{ fontSize: 12, color: 'var(--text2)' }}>
{filtered.length} contributors — no rank column by design
{filtered.length} contributors found
</span>
</div>
{contributors?.length ?
Expand All @@ -135,8 +247,58 @@ export default function ContributorsPage() {
<SortTh label="Repos Contributed To" sortKey="repos" sortConfig={sortConfig} onSort={onSort} />
<SortTh label="Orgs" sortKey="orgs" sortConfig={sortConfig} onSort={onSort} />
<SortTh label="Last Active" sortKey="lastActive" sortConfig={sortConfig} onSort={onSort} />
<th style={{ padding: '10px 14px', fontSize: 11, color: 'var(--text2)', fontWeight: 600, background: 'var(--surface2)', borderBottom: '1px solid var(--border)', textAlign: 'left' }}>
SIGNALS
<th
style={{ padding: '10px 14px', fontSize: 11, color: 'var(--text2)', fontWeight: 600, background: 'var(--surface2)', borderBottom: '1px solid var(--border)', textAlign: 'left', position: 'relative' }}
ref={signalRef}
>
<div
className="flex items-center gap-2"
style={{ position: 'relative' }}
>
<p>SIGNALS</p>

<button
onClick={() =>
setOpenInfo(openInfo === 'signals' ? null : 'signals')
}
className="p-2 rounded-full hover:bg-zinc-800 transition"
>
<BsFillInfoSquareFill className="text-white cursor-pointer" />
</button>

{openInfo === 'signals' && (
<div
style={{
...C.card,
position: 'absolute',
top: '130%',
right: 2,
width: '320px',
zIndex: 100,
}}
>
<h4 className='mb-2 text-(--accent)'>Contributor Signals</h4>

<div className="text-(--text) text-xs">
<p>
Measures how contributors connect repositories and organizations.
</p>

<ul className="ml-2 mt-2">
<li>
<strong>Connector Contributors</strong> — active in 3+ repositories.
</li>
<li>
<strong>Cross-Org Contributors</strong> — contribute across multiple organizations.
</li>
</ul></div>

<p style={{ marginTop: 8 }}>
Higher values indicate stronger collaboration and knowledge sharing.
</p>
</div>
)}
</div>
</th>
</tr>
</thead>
Expand Down Expand Up @@ -174,21 +336,21 @@ export default function ContributorsPage() {
<LoadMore shown={shown} total={sorted.length} onLoad={() => setShown(s => s + 20)} /></>) :
(<>
<div
style={{
padding: '32px 24px',
maxWidth: 900,
margin: '0 auto',
}}
>
<EmptyStateCard
SvgIcon={<FiDatabase size={36} color='var(--accent)'/>}
title="No contributors found"
description="We couldn't find any contributor data for this organization. "
buttonText="Go to Home"
onButtonClick={() => navigate('/')}/>
style={{
padding: '32px 24px',
maxWidth: 900,
margin: '0 auto',
}}
>
<EmptyStateCard
SvgIcon={<FiDatabase size={36} color='var(--accent)' />}
title="No contributors found"
description="We couldn't find any contributor data for this organization. "
buttonText="Go to Home"
onButtonClick={() => navigate('/')} />
</div>
</>)}
</div>
</div>
</div >
)
}
Loading
Loading