11import React , { useState , useMemo } from 'react'
2- import { FiDownload } from 'react-icons/fi'
2+ import { FiDatabase , FiDownload } from 'react-icons/fi'
33import { useApp } from '../context/AppContext'
44import { C , SortTh , PageTitle , LoadMore } from '../components/UI'
55import { useSortedData } from '../hooks/useSortedData'
66import { computeBusFactor , exportContributorsCSV } from '../services/analytics'
7+ import { useNavigate } from 'react-router-dom'
8+ import EmptyStateCard from '../components/EmptyStateCard'
79
810export default function ContributorsPage ( ) {
911 const { model } = useApp ( )
@@ -12,6 +14,7 @@ export default function ContributorsPage() {
1214
1315 if ( ! model ) return null
1416 const { contributors } = model
17+ const navigate = useNavigate ( )
1518
1619 const busFactor = useMemo ( ( ) => computeBusFactor ( contributors ) , [ contributors ] )
1720 const topActive = contributors . slice ( 0 , 10 ) . filter ( c => c . freshness > 50 ) . length
@@ -122,51 +125,69 @@ export default function ContributorsPage() {
122125 { filtered . length } contributors — no rank column by design
123126 </ span >
124127 </ div >
125- < table style = { { width : '100%' , borderCollapse : 'collapse' } } >
126- < thead >
127- < tr >
128- < SortTh label = "Contributor" sortKey = "login" sortConfig = { sortConfig } onSort = { onSort } />
129- < SortTh label = "Total Contributions" sortKey = "totalContribs" sortConfig = { sortConfig } onSort = { onSort } />
130- < SortTh label = "Repos Contributed To" sortKey = "repos" sortConfig = { sortConfig } onSort = { onSort } />
131- < SortTh label = "Orgs" sortKey = "orgs" sortConfig = { sortConfig } onSort = { onSort } />
132- < SortTh label = "Last Active" sortKey = "lastActive" sortConfig = { sortConfig } onSort = { onSort } />
133- < th style = { { padding : '10px 14px' , fontSize : 11 , color : 'var(--text2)' , fontWeight : 600 , background : 'var(--surface2)' , borderBottom : '1px solid var(--border)' , textAlign : 'left' } } >
134- SIGNALS
135- </ th >
136- </ tr >
137- </ thead >
138- < tbody >
139- { visible . map ( ( c , i ) => (
140- < tr key = { c . login } style = { { borderBottom : '1px solid var(--border)' , background : i % 2 ? 'var(--surface2)' : 'transparent' } } >
141- < td style = { { padding : '10px 14px' } } >
142- < div style = { { display : 'flex' , alignItems : 'center' , gap : 8 } } >
143- < img src = { c . avatar_url } alt = { c . login } style = { { width : 28 , height : 28 , borderRadius : '50%' } } />
144- < span style = { { fontSize : 13 , fontWeight : 500 } } > { c . login } </ span >
145- </ div >
146- </ td >
147- < td style = { { padding : '10px 14px' } } >
148- < div style = { { display : 'flex' , alignItems : 'center' , gap : 8 } } >
149- < div style = { { width : 80 , height : 4 , background : 'var(--border)' , borderRadius : 2 } } >
150- < div style = { { width : `${ Math . min ( 100 , c . totalContribs / 15 ) } %` , height : '100%' , background : 'var(--accent)' , borderRadius : 2 } } />
151- </ div >
152- < span style = { { fontSize : 13 , color : 'var(--text2)' } } > { c . totalContribs . toLocaleString ( ) } </ span >
153- </ div >
154- </ td >
155- < td style = { { padding : '10px 14px' , fontSize : 13 , color : 'var(--text2)' } } > { c . repos . length } </ td >
156- < td style = { { padding : '10px 14px' , fontSize : 13 , color : 'var(--text2)' } } > { c . orgs . length } </ td >
157- < td style = { { padding : '10px 14px' , fontSize : 12 , color : 'var(--text2)' } } > { c . lastActive ?. slice ( 0 , 10 ) || '—' } </ td >
158- < td style = { { padding : '10px 14px' } } >
159- < div style = { { display : 'flex' , gap : 4 , flexWrap : 'wrap' } } >
160- { c . isConnector && < span style = { C . pill ( 'var(--accent)' , 'rgba(245,197,24,.12)' ) } > CONNECTOR</ span > }
161- { c . isCrossOrg && < span style = { C . pill ( 'var(--purple)' , 'rgba(168,85,247,.12)' ) } > CROSS-ORG</ span > }
162- { c . freshness > 70 && < span style = { C . pill ( 'var(--green)' , 'rgba(34,197,94,.12)' ) } > ACTIVE</ span > }
163- </ div >
164- </ td >
165- </ tr >
166- ) ) }
167- </ tbody >
168- </ table >
169- < LoadMore shown = { shown } total = { sorted . length } onLoad = { ( ) => setShown ( s => s + 20 ) } />
128+ { contributors ?. length ?
129+ ( < >
130+ < table style = { { width : '100%' , borderCollapse : 'collapse' } } >
131+ < thead >
132+ < tr >
133+ < SortTh label = "Contributor" sortKey = "login" sortConfig = { sortConfig } onSort = { onSort } />
134+ < SortTh label = "Total Contributions" sortKey = "totalContribs" sortConfig = { sortConfig } onSort = { onSort } />
135+ < SortTh label = "Repos Contributed To" sortKey = "repos" sortConfig = { sortConfig } onSort = { onSort } />
136+ < SortTh label = "Orgs" sortKey = "orgs" sortConfig = { sortConfig } onSort = { onSort } />
137+ < SortTh label = "Last Active" sortKey = "lastActive" sortConfig = { sortConfig } onSort = { onSort } />
138+ < th style = { { padding : '10px 14px' , fontSize : 11 , color : 'var(--text2)' , fontWeight : 600 , background : 'var(--surface2)' , borderBottom : '1px solid var(--border)' , textAlign : 'left' } } >
139+ SIGNALS
140+ </ th >
141+ </ tr >
142+ </ thead >
143+ < tbody >
144+ { visible . map ( ( c , i ) => (
145+ < tr key = { c . login } style = { { borderBottom : '1px solid var(--border)' , background : i % 2 ? 'var(--surface2)' : 'transparent' } } >
146+ < td style = { { padding : '10px 14px' } } >
147+ < div style = { { display : 'flex' , alignItems : 'center' , gap : 8 } } >
148+ < img src = { c . avatar_url } alt = { c . login } style = { { width : 28 , height : 28 , borderRadius : '50%' } } />
149+ < span style = { { fontSize : 13 , fontWeight : 500 } } > { c . login } </ span >
150+ </ div >
151+ </ td >
152+ < td style = { { padding : '10px 14px' } } >
153+ < div style = { { display : 'flex' , alignItems : 'center' , gap : 8 } } >
154+ < div style = { { width : 80 , height : 4 , background : 'var(--border)' , borderRadius : 2 } } >
155+ < div style = { { width : `${ Math . min ( 100 , c . totalContribs / 15 ) } %` , height : '100%' , background : 'var(--accent)' , borderRadius : 2 } } />
156+ </ div >
157+ < span style = { { fontSize : 13 , color : 'var(--text2)' } } > { c . totalContribs . toLocaleString ( ) } </ span >
158+ </ div >
159+ </ td >
160+ < td style = { { padding : '10px 14px' , fontSize : 13 , color : 'var(--text2)' } } > { c . repos . length } </ td >
161+ < td style = { { padding : '10px 14px' , fontSize : 13 , color : 'var(--text2)' } } > { c . orgs . length } </ td >
162+ < td style = { { padding : '10px 14px' , fontSize : 12 , color : 'var(--text2)' } } > { c . lastActive ?. slice ( 0 , 10 ) || '—' } </ td >
163+ < td style = { { padding : '10px 14px' } } >
164+ < div style = { { display : 'flex' , gap : 4 , flexWrap : 'wrap' } } >
165+ { c . isConnector && < span style = { C . pill ( 'var(--accent)' , 'rgba(245,197,24,.12)' ) } > CONNECTOR</ span > }
166+ { c . isCrossOrg && < span style = { C . pill ( 'var(--purple)' , 'rgba(168,85,247,.12)' ) } > CROSS-ORG</ span > }
167+ { c . freshness > 70 && < span style = { C . pill ( 'var(--green)' , 'rgba(34,197,94,.12)' ) } > ACTIVE</ span > }
168+ </ div >
169+ </ td >
170+ </ tr >
171+ ) ) }
172+ </ tbody >
173+ </ table >
174+ < LoadMore shown = { shown } total = { sorted . length } onLoad = { ( ) => setShown ( s => s + 20 ) } /> </ > ) :
175+ ( < >
176+ < div
177+ style = { {
178+ padding : '32px 24px' ,
179+ maxWidth : 900 ,
180+ margin : '0 auto' ,
181+ } }
182+ >
183+ < EmptyStateCard
184+ SvgIcon = { < FiDatabase size = { 36 } color = 'var(--accent)' /> }
185+ title = "No contributors found"
186+ description = "We couldn't find any contributor data for this organization. "
187+ buttonText = "Go to Home"
188+ onButtonClick = { ( ) => navigate ( '/' ) } />
189+ </ div >
190+ </ > ) }
170191 </ div >
171192 </ div >
172193 )
0 commit comments