Skip to content

Commit 9957629

Browse files
Merge pull request #69 from rahul-vyas-dev/feat/empty-state-card
adding reusable empty-card for edge case handling.
2 parents aba4cca + 50e05f6 commit 9957629

5 files changed

Lines changed: 223 additions & 52 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "orgexplorer",
3-
"version": "2.0.0",
3+
"version": "1.0.0",
44
"private": true,
55
"type": "module",
66
"scripts": {

src/components/EmptyStateCard.jsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
2+
export default function EmptyStateCard({
3+
SvgIcon,
4+
title = "No results found",
5+
description = "We couldn't find anything for this organization right now.",
6+
buttonText = "Explore Organizations",
7+
onButtonClick,
8+
}) {
9+
return (
10+
<div
11+
style={{
12+
width: "100%",
13+
display: "flex",
14+
justifyContent: "center",
15+
alignItems: "center",
16+
padding: "40px 20px",
17+
}}
18+
>
19+
<div
20+
style={{
21+
width: "100%",
22+
maxWidth: "620px",
23+
background: "var(--surface)",
24+
border: "1px solid var(--border)",
25+
borderRadius: "10px",
26+
padding: "42px 36px",
27+
textAlign: "center",
28+
boxShadow: "0 0 0 1px rgba(255,255,255,0.02) inset",
29+
}}
30+
>
31+
{/* Top Icon */}
32+
<div
33+
style={{
34+
width: "72px",
35+
height: "72px",
36+
margin: "0 auto 28px",
37+
borderRadius: "16px",
38+
background: "rgba(255, 214, 0, 0.12)",
39+
display: "flex",
40+
alignItems: "center",
41+
justifyContent: "center",
42+
}}
43+
>
44+
{SvgIcon}
45+
</div>
46+
47+
{/* Title */}
48+
<h2
49+
style={{
50+
color: "var(--text)",
51+
fontSize: "3rem",
52+
fontWeight: 700,
53+
lineHeight: 1.15,
54+
marginBottom: "18px",
55+
letterSpacing: "-1px",
56+
}}
57+
>
58+
{title}
59+
</h2>
60+
61+
{/* Description */}
62+
<p
63+
style={{
64+
color: "#D4C58A",
65+
fontSize: "1rem",
66+
lineHeight: 1.7,
67+
maxWidth: "470px",
68+
margin: "0 auto",
69+
}}
70+
>
71+
{description}
72+
</p>
73+
74+
{/* CTA Button */}
75+
<button
76+
onClick={onButtonClick}
77+
style={{
78+
marginTop: "34px",
79+
background: "var(--accent)",
80+
color: "#111111",
81+
border: "none",
82+
padding: "16px 28px",
83+
borderRadius: "2px",
84+
fontSize: "1rem",
85+
fontWeight: 700,
86+
cursor: "pointer",
87+
transition: "all 0.2s ease",
88+
}}
89+
onMouseEnter={(e) => {
90+
e.currentTarget.style.transform = "translateY(-2px)";
91+
e.currentTarget.style.opacity = "0.92";
92+
}}
93+
onMouseLeave={(e) => {
94+
e.currentTarget.style.transform = "translateY(0px)";
95+
e.currentTarget.style.opacity = "1";
96+
}}
97+
>
98+
{buttonText}
99+
</button>
100+
</div>
101+
</div>
102+
);
103+
}

src/pages/ContributorsPage.jsx

Lines changed: 67 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import React, { useState, useMemo } from 'react'
2-
import { FiDownload } from 'react-icons/fi'
2+
import { FiDatabase, FiDownload } from 'react-icons/fi'
33
import { useApp } from '../context/AppContext'
44
import { C, SortTh, PageTitle, LoadMore } from '../components/UI'
55
import { useSortedData } from '../hooks/useSortedData'
66
import { computeBusFactor, exportContributorsCSV } from '../services/analytics'
7+
import { useNavigate } from 'react-router-dom'
8+
import EmptyStateCard from '../components/EmptyStateCard'
79

810
export 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
)

src/pages/NetworkPage.jsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import React, { useEffect, useRef, useState } from 'react'
22
import * as d3 from 'd3'
33
import { useApp } from '../context/AppContext'
44
import { C, PageTitle } from '../components/UI'
5+
import EmptyStateCard from '../components/EmptyStateCard'
6+
import { FiDatabase } from 'react-icons/fi'
7+
import { useNavigate } from 'react-router-dom'
58

69
export default function NetworkPage() {
710
const { model } = useApp()
@@ -126,7 +129,7 @@ export default function NetworkPage() {
126129
return () => sim.stop()
127130
}, [model, showRepos, showContribs])
128131

129-
if (!model) return null
132+
const navigate = useNavigate()
130133

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

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

160165
{/* Tooltip */}
@@ -195,6 +200,26 @@ export default function NetworkPage() {
195200
Drag nodes to reposition — scroll to zoom
196201
</div>
197202
</div>
203+
</>) :
204+
(
205+
<>
206+
<div
207+
style={{
208+
padding: '32px 24px',
209+
maxWidth: 900,
210+
margin: '0 auto',
211+
}}
212+
>
213+
<EmptyStateCard
214+
SvgIcon={<FiDatabase size={36} color='var(--accent)'/>}
215+
title="No repositories found"
216+
description="We couldn't find any repositories in this organization."
217+
buttonText="Go to Home"
218+
onButtonClick={() => navigate('/')}
219+
/>
220+
</div>
221+
</>
222+
)}
198223
</div>
199224
)
200225
}

src/pages/RepositoriesPage.jsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import React, { useState, useMemo } from 'react'
2-
import { FiDownload, FiGrid, FiList } from 'react-icons/fi'
2+
import { FiDatabase, FiDownload, FiGrid, FiList } from 'react-icons/fi'
33
import { useApp } from '../context/AppContext'
44
import { C, Badge, HealthBar, SortTh, PageTitle, LoadMore } from '../components/UI'
55
import { useSortedData } from '../hooks/useSortedData'
66
import { exportReposCSV } from '../services/analytics'
7+
import EmptyStateCard from '../components/EmptyStateCard'
8+
import { useNavigate } from 'react-router-dom'
79

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

21+
const navigate = useNavigate()
1922
if (!model) return null
2023
const { allRepos } = model
2124

@@ -95,7 +98,8 @@ export default function RepositoriesPage() {
9598
))}
9699
</div>
97100
</div>
98-
101+
{allRepos?.length ? (
102+
<>
99103
{/* Table view */}
100104
{view === 'list' && (
101105
<div style={{ ...C.card, padding: 0, overflowX: 'auto' }}>
@@ -182,7 +186,25 @@ export default function RepositoriesPage() {
182186
</div>
183187
<LoadMore shown={shown} total={sorted.length} onLoad={() => setShown(s => s + 20)} />
184188
</>
185-
)}
189+
)}
190+
</>)
191+
: (
192+
<div
193+
style={{
194+
padding: '32px 24px',
195+
maxWidth: 900,
196+
margin: '0 auto',
197+
}}
198+
>
199+
<EmptyStateCard
200+
SvgIcon={<FiDatabase size={36} color="var(--accent)" />}
201+
title="No repositories available"
202+
description="We couldn't find any repositories for this organization yet."
203+
buttonText="Go to Home"
204+
onButtonClick={() => navigate('/')}
205+
/>
206+
</div>
207+
)}
186208
</div>
187209
)
188210
}

0 commit comments

Comments
 (0)