Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "orgexplorer",
"version": "2.0.0",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
103 changes: 103 additions & 0 deletions src/components/EmptyStateCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@

export default function EmptyStateCard({
SvgIcon,
title = "No results found",
description = "We couldn't find anything for this organization right now.",
buttonText = "Explore Organizations",
Comment thread
rahul-vyas-dev marked this conversation as resolved.
onButtonClick,
}) {
Comment thread
rahul-vyas-dev marked this conversation as resolved.
return (
<div
style={{
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "40px 20px",
}}
>
<div
style={{
width: "100%",
maxWidth: "620px",
background: "var(--surface)",
border: "1px solid var(--border)",
borderRadius: "10px",
padding: "42px 36px",
textAlign: "center",
boxShadow: "0 0 0 1px rgba(255,255,255,0.02) inset",
}}
>
{/* Top Icon */}
<div
style={{
width: "72px",
height: "72px",
margin: "0 auto 28px",
borderRadius: "16px",
background: "rgba(255, 214, 0, 0.12)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{SvgIcon}
</div>

{/* Title */}
<h2
style={{
color: "var(--text)",
fontSize: "3rem",
fontWeight: 700,
lineHeight: 1.15,
marginBottom: "18px",
letterSpacing: "-1px",
}}
>
{title}
</h2>

{/* Description */}
<p
style={{
color: "#D4C58A",
fontSize: "1rem",
lineHeight: 1.7,
maxWidth: "470px",
margin: "0 auto",
}}
>
{description}
</p>

{/* CTA Button */}
<button
onClick={onButtonClick}
style={{
marginTop: "34px",
background: "var(--accent)",
color: "#111111",
border: "none",
padding: "16px 28px",
borderRadius: "2px",
fontSize: "1rem",
fontWeight: 700,
cursor: "pointer",
transition: "all 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = "translateY(-2px)";
e.currentTarget.style.opacity = "0.92";
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = "translateY(0px)";
e.currentTarget.style.opacity = "1";
}}
Comment thread
rahul-vyas-dev marked this conversation as resolved.
>
{buttonText}
</button>
Comment thread
rahul-vyas-dev marked this conversation as resolved.
</div>
</div>
);
}
113 changes: 67 additions & 46 deletions src/pages/ContributorsPage.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useState, useMemo } from 'react'
import { FiDownload } from 'react-icons/fi'
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'

export default function ContributorsPage() {
const { model } = useApp()
Expand All @@ -12,6 +14,7 @@ export default function ContributorsPage() {

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
Expand Down Expand Up @@ -122,51 +125,69 @@ export default function ContributorsPage() {
{filtered.length} contributors — no rank column by design
</span>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<SortTh label="Contributor" sortKey="login" sortConfig={sortConfig} onSort={onSort} />
<SortTh label="Total Contributions" sortKey="totalContribs" sortConfig={sortConfig} onSort={onSort} />
<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>
</tr>
</thead>
<tbody>
{visible.map((c, i) => (
<tr key={c.login} style={{ borderBottom: '1px solid var(--border)', background: i % 2 ? 'var(--surface2)' : 'transparent' }}>
<td style={{ padding: '10px 14px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<img src={c.avatar_url} alt={c.login} style={{ width: 28, height: 28, borderRadius: '50%' }} />
<span style={{ fontSize: 13, fontWeight: 500 }}>{c.login}</span>
</div>
</td>
<td style={{ padding: '10px 14px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 80, height: 4, background: 'var(--border)', borderRadius: 2 }}>
<div style={{ width: `${Math.min(100, c.totalContribs / 15)}%`, height: '100%', background: 'var(--accent)', borderRadius: 2 }} />
</div>
<span style={{ fontSize: 13, color: 'var(--text2)' }}>{c.totalContribs.toLocaleString()}</span>
</div>
</td>
<td style={{ padding: '10px 14px', fontSize: 13, color: 'var(--text2)' }}>{c.repos.length}</td>
<td style={{ padding: '10px 14px', fontSize: 13, color: 'var(--text2)' }}>{c.orgs.length}</td>
<td style={{ padding: '10px 14px', fontSize: 12, color: 'var(--text2)' }}>{c.lastActive?.slice(0, 10) || '—'}</td>
<td style={{ padding: '10px 14px' }}>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{c.isConnector && <span style={C.pill('var(--accent)', 'rgba(245,197,24,.12)')}>CONNECTOR</span>}
{c.isCrossOrg && <span style={C.pill('var(--purple)', 'rgba(168,85,247,.12)')}>CROSS-ORG</span>}
{c.freshness > 70 && <span style={C.pill('var(--green)', 'rgba(34,197,94,.12)')}>ACTIVE</span>}
</div>
</td>
</tr>
))}
</tbody>
</table>
<LoadMore shown={shown} total={sorted.length} onLoad={() => setShown(s => s + 20)} />
{contributors?.length ?
(<>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<SortTh label="Contributor" sortKey="login" sortConfig={sortConfig} onSort={onSort} />
<SortTh label="Total Contributions" sortKey="totalContribs" sortConfig={sortConfig} onSort={onSort} />
<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>
</tr>
</thead>
<tbody>
{visible.map((c, i) => (
<tr key={c.login} style={{ borderBottom: '1px solid var(--border)', background: i % 2 ? 'var(--surface2)' : 'transparent' }}>
<td style={{ padding: '10px 14px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<img src={c.avatar_url} alt={c.login} style={{ width: 28, height: 28, borderRadius: '50%' }} />
<span style={{ fontSize: 13, fontWeight: 500 }}>{c.login}</span>
</div>
</td>
<td style={{ padding: '10px 14px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 80, height: 4, background: 'var(--border)', borderRadius: 2 }}>
<div style={{ width: `${Math.min(100, c.totalContribs / 15)}%`, height: '100%', background: 'var(--accent)', borderRadius: 2 }} />
</div>
<span style={{ fontSize: 13, color: 'var(--text2)' }}>{c.totalContribs.toLocaleString()}</span>
</div>
</td>
<td style={{ padding: '10px 14px', fontSize: 13, color: 'var(--text2)' }}>{c.repos.length}</td>
<td style={{ padding: '10px 14px', fontSize: 13, color: 'var(--text2)' }}>{c.orgs.length}</td>
<td style={{ padding: '10px 14px', fontSize: 12, color: 'var(--text2)' }}>{c.lastActive?.slice(0, 10) || '—'}</td>
<td style={{ padding: '10px 14px' }}>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{c.isConnector && <span style={C.pill('var(--accent)', 'rgba(245,197,24,.12)')}>CONNECTOR</span>}
{c.isCrossOrg && <span style={C.pill('var(--purple)', 'rgba(168,85,247,.12)')}>CROSS-ORG</span>}
{c.freshness > 70 && <span style={C.pill('var(--green)', 'rgba(34,197,94,.12)')}>ACTIVE</span>}
</div>
</td>
</tr>
))}
</tbody>
</table>
<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('/')}/>
Comment thread
rahul-vyas-dev marked this conversation as resolved.
</div>
</>)}
</div>
</div>
)
Expand Down
29 changes: 27 additions & 2 deletions src/pages/NetworkPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import React, { useEffect, useRef, useState } from 'react'
import * as d3 from 'd3'
import { useApp } from '../context/AppContext'
import { C, PageTitle } from '../components/UI'
import EmptyStateCard from '../components/EmptyStateCard'
import { FiDatabase } from 'react-icons/fi'
import { useNavigate } from 'react-router-dom'

export default function NetworkPage() {
const { model } = useApp()
Expand Down Expand Up @@ -126,7 +129,7 @@ export default function NetworkPage() {
return () => sim.stop()
}, [model, showRepos, showContribs])

if (!model) return null
const navigate = useNavigate()

return (
<div style={{ padding: '32px 24px', maxWidth: 1100, margin: '0 auto' }} className="fade-up">
Expand Down Expand Up @@ -154,7 +157,9 @@ export default function NetworkPage() {
</div>

{/* Canvas */}
<div style={{ ...C.card, padding: 0, overflow: 'hidden', position: 'relative' }}>
{model.allRepos?.length ? (
<>
<div style={{ ...C.card, padding: 0, overflow: 'hidden', position: 'relative' }}>
<svg ref={svgRef} style={{ width: '100%', height: 580, display: 'block', background: 'var(--bg)' }} />

{/* Tooltip */}
Expand Down Expand Up @@ -195,6 +200,26 @@ export default function NetworkPage() {
Drag nodes to reposition — scroll to zoom
</div>
</div>
</>) :
(
<>
<div
style={{
padding: '32px 24px',
maxWidth: 900,
margin: '0 auto',
}}
>
<EmptyStateCard
SvgIcon={<FiDatabase size={36} color='var(--accent)'/>}
title="No repositories found"
description="We couldn't find any repositories in this organization."
buttonText="Go to Home"
onButtonClick={() => navigate('/')}
/>
Comment thread
rahul-vyas-dev marked this conversation as resolved.
</div>
</>
)}
</div>
)
}
28 changes: 25 additions & 3 deletions src/pages/RepositoriesPage.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useState, useMemo } from 'react'
import { FiDownload, FiGrid, FiList } from 'react-icons/fi'
import { FiDatabase, FiDownload, FiGrid, FiList } from 'react-icons/fi'
import { useApp } from '../context/AppContext'
import { C, Badge, HealthBar, SortTh, PageTitle, LoadMore } from '../components/UI'
import { useSortedData } from '../hooks/useSortedData'
import { exportReposCSV } from '../services/analytics'
import EmptyStateCard from '../components/EmptyStateCard'
import { useNavigate } from 'react-router-dom'

const LIFECYCLES = ['All','Thriving','Stable','Dormant','Abandoned']
const LC_ACTIVE = { Thriving:'var(--green)', Stable:'var(--blue)', Dormant:'var(--amber)', Abandoned:'var(--red)' }
Expand All @@ -16,6 +18,7 @@ export default function RepositoriesPage() {
const [view, setView] = useState('grid')
const [shown, setShown] = useState(20)

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

Expand Down Expand Up @@ -95,7 +98,8 @@ export default function RepositoriesPage() {
))}
</div>
</div>

{allRepos?.length ? (
<>
{/* Table view */}
{view === 'list' && (
<div style={{ ...C.card, padding: 0, overflowX: 'auto' }}>
Expand Down Expand Up @@ -182,7 +186,25 @@ export default function RepositoriesPage() {
</div>
<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 repositories available"
description="We couldn't find any repositories for this organization yet."
buttonText="Go to Home"
onButtonClick={() => navigate('/')}
/>
Comment thread
rahul-vyas-dev marked this conversation as resolved.
</div>
)}
</div>
)
}
Loading