Skip to content

Commit 77ec7cb

Browse files
MaxGhenisclaude
andcommitted
Add aggregate view, repos count, merge duplicate accounts
- Add "All" button showing aggregate stats across all contributors - Display repos count as 5th stat metric - Add account aliases to merge eccuraa→elenacura and nwoodruff-co→nikhilwoodruff - Add contributors grid in aggregate view showing each person's stats - Remove self-review references from copy and tagline - Update tagline to "Your 2025 contribution summary" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2f9c698 commit 77ec7cb

5 files changed

Lines changed: 344 additions & 42 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Year in Code: View your PolicyEngine GitHub contributions.
1111
- Monthly activity charts
1212
- Top repositories by contribution
1313
- PR list with file changes and discussion stats
14-
- Copy summary button for self-reviews
14+
- Copy summary button to share stats
1515

1616
## How It Works
1717

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<link rel="icon" href="https://policyengine.org/favicon.ico" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>PolicyEngine 2025 Year in Code</title>
8-
<meta name="description" content="View your 2025 GitHub contributions to PolicyEngine for self-reviews" />
8+
<meta name="description" content="View 2025 GitHub contributions to PolicyEngine" />
99
</head>
1010
<body>
1111
<div id="root"></div>

scripts/fetch-data.js

Lines changed: 89 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ const START_DATE = '2025-01-01';
1818
const END_DATE = '2025-12-31';
1919
const MIN_CONTRIBUTIONS = 1; // Include anyone with at least 1 commit or PR
2020

21+
// Merge alternate accounts into primary accounts
22+
// Key = alias, Value = primary account
23+
const ACCOUNT_ALIASES = {
24+
'eccuraa': 'elenacura',
25+
'nwoodruff-co': 'nikhilwoodruff'
26+
};
27+
2128
const TOKEN = process.env.GITHUB_TOKEN;
2229
const headers = {
2330
'Accept': 'application/vnd.github.v3+json',
@@ -84,10 +91,15 @@ async function discoverContributors() {
8491
for (const commit of commitsData.items) {
8592
const author = commit.author;
8693
if (author && author.login && !author.login.includes('[bot]')) {
87-
const existing = contributors.get(author.login) || { commits: 0, prs: 0 };
94+
// Resolve alias to primary account
95+
const login = ACCOUNT_ALIASES[author.login] || author.login;
96+
const existing = contributors.get(login) || { commits: 0, prs: 0, aliases: [] };
8897
existing.commits++;
8998
existing.avatar = author.avatar_url;
90-
contributors.set(author.login, existing);
99+
if (author.login !== login && !existing.aliases.includes(author.login)) {
100+
existing.aliases.push(author.login);
101+
}
102+
contributors.set(login, existing);
91103
}
92104
}
93105

@@ -101,10 +113,15 @@ async function discoverContributors() {
101113
for (const pr of prsData.items) {
102114
const author = pr.user;
103115
if (author && author.login && !author.login.includes('[bot]')) {
104-
const existing = contributors.get(author.login) || { commits: 0, prs: 0 };
116+
// Resolve alias to primary account
117+
const login = ACCOUNT_ALIASES[author.login] || author.login;
118+
const existing = contributors.get(login) || { commits: 0, prs: 0, aliases: [] };
105119
existing.prs++;
106120
existing.avatar = author.avatar_url;
107-
contributors.set(author.login, existing);
121+
if (author.login !== login && !existing.aliases.includes(author.login)) {
122+
existing.aliases.push(author.login);
123+
}
124+
contributors.set(login, existing);
108125
}
109126
}
110127

@@ -117,7 +134,8 @@ async function discoverContributors() {
117134
github: login,
118135
name: login, // Will be updated with real name if available
119136
avatar: data.avatar,
120-
estimatedActivity: total
137+
estimatedActivity: total,
138+
aliases: data.aliases || []
121139
});
122140
}
123141
}
@@ -167,6 +185,8 @@ async function fetchPRDetails(owner, repo, prNumber) {
167185

168186
async function fetchMemberData(member) {
169187
console.log(`\nFetching data for @${member.github}...`);
188+
const aliases = member.aliases || [];
189+
const allAccounts = [member.github, ...aliases];
170190

171191
// Get user profile
172192
const profile = await fetchUserProfile(member.github);
@@ -176,36 +196,64 @@ async function fetchMemberData(member) {
176196

177197
await sleep(500);
178198

179-
// Fetch commits
199+
// Fetch commits for all accounts (primary + aliases)
180200
console.log(' Commits...');
181-
const commitsUrl = `https://api.github.com/search/commits?q=author:${member.github}+org:${GITHUB_ORG}+committer-date:${START_DATE}..${END_DATE}`;
182-
const commitsData = await fetchAllPages(commitsUrl, 10);
201+
const allCommits = [];
202+
let totalCommits = 0;
203+
for (const account of allAccounts) {
204+
const commitsUrl = `https://api.github.com/search/commits?q=author:${account}+org:${GITHUB_ORG}+committer-date:${START_DATE}..${END_DATE}`;
205+
const commitsData = await fetchAllPages(commitsUrl, 10);
206+
allCommits.push(...commitsData.items);
207+
totalCommits += commitsData.total_count;
208+
await sleep(500);
209+
}
183210

184-
await sleep(1000);
211+
await sleep(500);
185212

186-
// Fetch PRs authored
213+
// Fetch PRs authored for all accounts
187214
console.log(' PRs authored...');
188-
const prsUrl = `https://api.github.com/search/issues?q=author:${member.github}+org:${GITHUB_ORG}+type:pr+created:${START_DATE}..${END_DATE}`;
189-
const prsData = await fetchAllPages(prsUrl, 10);
215+
const allPRs = [];
216+
let totalPRs = 0;
217+
for (const account of allAccounts) {
218+
const prsUrl = `https://api.github.com/search/issues?q=author:${account}+org:${GITHUB_ORG}+type:pr+created:${START_DATE}..${END_DATE}`;
219+
const prsData = await fetchAllPages(prsUrl, 10);
220+
allPRs.push(...prsData.items);
221+
totalPRs += prsData.total_count;
222+
await sleep(500);
223+
}
190224

191-
await sleep(1000);
225+
await sleep(500);
192226

193-
// Fetch PRs reviewed
227+
// Fetch PRs reviewed for all accounts
194228
console.log(' PRs reviewed...');
195-
const reviewsUrl = `https://api.github.com/search/issues?q=reviewed-by:${member.github}+org:${GITHUB_ORG}+type:pr+created:${START_DATE}..${END_DATE}`;
196-
const reviewsData = await fetchAllPages(reviewsUrl, 5);
229+
let totalReviews = 0;
230+
const allReviews = [];
231+
for (const account of allAccounts) {
232+
const reviewsUrl = `https://api.github.com/search/issues?q=reviewed-by:${account}+org:${GITHUB_ORG}+type:pr+created:${START_DATE}..${END_DATE}`;
233+
const reviewsData = await fetchAllPages(reviewsUrl, 5);
234+
allReviews.push(...reviewsData.items);
235+
totalReviews += reviewsData.total_count;
236+
await sleep(500);
237+
}
197238

198-
await sleep(1000);
239+
await sleep(500);
199240

200-
// Fetch issues created
241+
// Fetch issues created for all accounts
201242
console.log(' Issues created...');
202-
const issuesUrl = `https://api.github.com/search/issues?q=author:${member.github}+org:${GITHUB_ORG}+type:issue+created:${START_DATE}..${END_DATE}`;
203-
const issuesData = await fetchAllPages(issuesUrl, 5);
243+
let totalIssues = 0;
244+
const allIssues = [];
245+
for (const account of allAccounts) {
246+
const issuesUrl = `https://api.github.com/search/issues?q=author:${account}+org:${GITHUB_ORG}+type:issue+created:${START_DATE}..${END_DATE}`;
247+
const issuesData = await fetchAllPages(issuesUrl, 5);
248+
allIssues.push(...issuesData.items);
249+
totalIssues += issuesData.total_count;
250+
await sleep(500);
251+
}
204252

205253
// Fetch PR details for top 30 PRs
206254
console.log(' Fetching PR details...');
207255
const prsWithFiles = [];
208-
for (const pr of prsData.items.slice(0, 30)) {
256+
for (const pr of allPRs.slice(0, 30)) {
209257
const repoName = pr.repository_url.split('/').pop();
210258
await sleep(300);
211259
const [files, details] = await Promise.all([
@@ -228,21 +276,30 @@ async function fetchMemberData(member) {
228276

229277
// Process commits by repo
230278
const repoCommits = {};
231-
for (const commit of commitsData.items) {
279+
for (const commit of allCommits) {
232280
const repo = commit.repository.name;
233281
repoCommits[repo] = (repoCommits[repo] || 0) + 1;
234282
}
235283

284+
// Count unique repos contributed to
285+
const reposContributed = new Set();
286+
for (const commit of allCommits) {
287+
reposContributed.add(commit.repository.name);
288+
}
289+
for (const pr of allPRs) {
290+
reposContributed.add(pr.repository_url.split('/').pop());
291+
}
292+
236293
// Process monthly activity
237294
const monthlyPRs = new Array(12).fill(0);
238295
const monthlyCommits = new Array(12).fill(0);
239296

240-
for (const pr of prsData.items) {
297+
for (const pr of allPRs) {
241298
const month = new Date(pr.created_at).getMonth();
242299
monthlyPRs[month]++;
243300
}
244301

245-
for (const commit of commitsData.items) {
302+
for (const commit of allCommits) {
246303
const date = commit.commit?.author?.date || commit.commit?.committer?.date;
247304
if (date) {
248305
const month = new Date(date).getMonth();
@@ -253,18 +310,19 @@ async function fetchMemberData(member) {
253310
return {
254311
member,
255312
stats: {
256-
commits: commitsData.total_count,
257-
prs: prsData.total_count,
258-
reviews: reviewsData.total_count,
259-
issues: issuesData.total_count,
313+
commits: totalCommits,
314+
prs: totalPRs,
315+
reviews: totalReviews,
316+
issues: totalIssues,
317+
repos: reposContributed.size,
260318
},
261319
repoCommits,
262320
monthlyPRs,
263321
monthlyCommits,
264322
prs: prsWithFiles,
265-
reviews: reviewsData.items.slice(0, 30),
266-
issues: issuesData.items.slice(0, 30),
267-
commits: commitsData.items.slice(0, 100).map(c => ({
323+
reviews: allReviews.slice(0, 30),
324+
issues: allIssues.slice(0, 30),
325+
commits: allCommits.slice(0, 100).map(c => ({
268326
sha: c.sha,
269327
message: c.commit.message.split('\n')[0],
270328
date: c.commit.author?.date,

src/App.css

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,17 @@ body {
356356
/* Stats Grid - The Hero */
357357
.stats {
358358
display: grid;
359-
grid-template-columns: repeat(4, 1fr);
359+
grid-template-columns: repeat(5, 1fr);
360360
gap: 1rem;
361361
}
362362

363-
@media (max-width: 768px) {
363+
@media (max-width: 900px) {
364+
.stats {
365+
grid-template-columns: repeat(3, 1fr);
366+
}
367+
}
368+
369+
@media (max-width: 640px) {
364370
.stats {
365371
grid-template-columns: repeat(2, 1fr);
366372
}
@@ -750,6 +756,99 @@ body {
750756
background: var(--teal-500);
751757
}
752758

759+
/* Avatar Group (for All view) */
760+
.avatar-group {
761+
display: flex;
762+
align-items: center;
763+
}
764+
765+
.avatar-small {
766+
width: 48px;
767+
height: 48px;
768+
border-radius: 50%;
769+
border: 3px solid var(--dark-800);
770+
margin-left: -12px;
771+
position: relative;
772+
box-shadow: 0 0 20px var(--glow-teal-soft);
773+
}
774+
775+
.avatar-small:first-child {
776+
margin-left: 0;
777+
}
778+
779+
.avatar-more {
780+
display: flex;
781+
align-items: center;
782+
justify-content: center;
783+
width: 48px;
784+
height: 48px;
785+
border-radius: 50%;
786+
background: var(--teal-600);
787+
border: 3px solid var(--dark-800);
788+
margin-left: -12px;
789+
font-family: var(--font-mono);
790+
font-size: 0.75rem;
791+
font-weight: 700;
792+
color: #fff;
793+
}
794+
795+
/* Contributors Grid */
796+
.contributors-grid {
797+
display: grid;
798+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
799+
gap: 0.75rem;
800+
}
801+
802+
.contributor-card {
803+
display: flex;
804+
align-items: center;
805+
gap: 1rem;
806+
padding: 1rem 1.25rem;
807+
background: rgba(255, 255, 255, 0.02);
808+
border: 1px solid rgba(255, 255, 255, 0.04);
809+
border-radius: 12px;
810+
cursor: pointer;
811+
transition: all 0.3s ease;
812+
animation: slideUp 0.4s ease backwards;
813+
animation-delay: var(--delay);
814+
text-align: left;
815+
}
816+
817+
.contributor-card:hover {
818+
background: rgba(255, 255, 255, 0.06);
819+
border-color: rgba(49, 151, 149, 0.3);
820+
transform: translateY(-2px);
821+
}
822+
823+
.contributor-avatar {
824+
width: 44px;
825+
height: 44px;
826+
border-radius: 50%;
827+
border: 2px solid var(--teal-600);
828+
}
829+
830+
.contributor-info {
831+
display: flex;
832+
flex-direction: column;
833+
gap: 0.25rem;
834+
min-width: 0;
835+
}
836+
837+
.contributor-name {
838+
font-size: 0.9375rem;
839+
font-weight: 600;
840+
color: rgba(255, 255, 255, 0.9);
841+
white-space: nowrap;
842+
overflow: hidden;
843+
text-overflow: ellipsis;
844+
}
845+
846+
.contributor-stats {
847+
font-family: var(--font-mono);
848+
font-size: 0.6875rem;
849+
color: rgba(255, 255, 255, 0.4);
850+
}
851+
753852
/* Responsive */
754853
@media (max-width: 640px) {
755854
.header {
@@ -763,6 +862,10 @@ body {
763862
text-align: center;
764863
}
765864

865+
.avatar-group {
866+
justify-content: center;
867+
}
868+
766869
.copy-inner {
767870
flex-direction: column;
768871
text-align: center;
@@ -771,4 +874,8 @@ body {
771874
.stat-value {
772875
font-size: 2.25rem;
773876
}
877+
878+
.contributors-grid {
879+
grid-template-columns: 1fr;
880+
}
774881
}

0 commit comments

Comments
 (0)