Skip to content

Instantly share code, notes, and snippets.

@nsdevaraj
Created April 13, 2026 06:15
Show Gist options
  • Select an option

  • Save nsdevaraj/0f4275b7fbcdec954368b70b77684c41 to your computer and use it in GitHub Desktop.

Select an option

Save nsdevaraj/0f4275b7fbcdec954368b70b77684c41 to your computer and use it in GitHub Desktop.
github productivity
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub Contributions Tracker</title>
<style>
:root {
--color-primary: #238636;
--color-secondary: #0d1117;
--color-text: #c9d1d9;
--color-border: #30363d;
--color-bg: #0d1117;
--color-surface: #161b22;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background-color: var(--color-bg);
color: var(--color-text);
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
}
h1 {
font-size: 28px;
margin-bottom: 10px;
color: #58a6ff;
}
.subtitle {
color: #8b949e;
margin-bottom: 30px;
font-size: 14px;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 30px;
flex-wrap: wrap;
}
button {
padding: 8px 16px;
background-color: var(--color-primary);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s;
}
button:hover {
background-color: #2ea043;
}
button:active {
background-color: #238636;
}
.users-list {
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.user-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
margin-bottom: 12px;
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
transition: background-color 0.2s;
}
.user-item:last-child {
margin-bottom: 0;
}
.user-item:hover {
background-color: #161b22;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.user-name {
font-weight: 500;
color: #58a6ff;
}
.contributions {
background-color: var(--color-primary);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-weight: 500;
font-size: 14px;
min-width: 100px;
text-align: center;
}
.contributions.loading {
background-color: #30363d;
color: #8b949e;
}
.stats {
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.stat-item {
display: flex;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid var(--color-border);
}
.stat-item:last-child {
border-bottom: none;
}
.stat-label {
color: #8b949e;
}
.stat-value {
font-weight: 600;
color: #58a6ff;
font-size: 18px;
}
.console-output {
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
margin-top: 20px;
}
.console-log {
color: #8b949e;
margin-bottom: 8px;
}
.console-info {
color: #58a6ff;
}
.console-success {
color: #3fb950;
}
.console-error {
color: #f85149;
}
.spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid #30363d;
border-top: 2px solid #58a6ff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
color: #f85149;
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<h1>🐙 GitHub Contributions Tracker</h1>
<p class="subtitle">Fetch and analyze total contributions from GitHub users</p>
<div class="controls">
<button onclick="fetchAllContributions()">Fetch All Contributions</button>
<button onclick="exportToExcel()">Export to Excel</button>
<button onclick="clearConsole()">Clear Console</button>
</div>
<div class="users-list">
<h2 style="margin-bottom: 16px; color: #58a6ff;">Users</h2>
<div id="usersList"></div>
</div>
<div class="stats">
<div class="stat-item">
<span class="stat-label">Total Users</span>
<span class="stat-value" id="totalUsers">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Total Contributions</span>
<span class="stat-value" id="totalContributions">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Average per User</span>
<span class="stat-value" id="averageContributions">0</span>
</div>
</div>
<div class="console-output" id="consoleOutput"></div>
</div>
<script src="https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js"></script>
<script>
const ORG_NAME = 'lumelinc';
const GITHUB_TOKEN = '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';
let users = []; // Will be populated from org members
let contributions = {};
let monthlyContributions = {};
let consoleMessages = [];
function getMonthRanges() {
const ranges = [];
const now = new Date();
for (let i = 2; i >= 0; i--) {
const start = new Date(now.getFullYear(), now.getMonth() - i, 1);
const end = new Date(now.getFullYear(), now.getMonth() - i + 1, 1);
ranges.push({
label: start.toLocaleString('default', { month: 'short', year: 'numeric' }),
from: start.toISOString(),
to: end.toISOString()
});
}
return ranges;
}
const monthRanges = getMonthRanges();
function log(message, type = 'log') {
const timestamp = new Date().toLocaleTimeString();
const formattedMessage = `[${timestamp}] ${message}`;
consoleMessages.push({ message: formattedMessage, type });
console.log(`%c${formattedMessage}`, getConsoleStyle(type));
updateConsoleOutput();
}
function getConsoleStyle(type) {
const styles = {
log: 'color: #8b949e;',
info: 'color: #58a6ff; font-weight: bold;',
success: 'color: #3fb950; font-weight: bold;',
error: 'color: #f85149; font-weight: bold;'
};
return styles[type] || styles.log;
}
function updateConsoleOutput() {
const output = document.getElementById('consoleOutput');
output.innerHTML = consoleMessages
.map(msg => `<div class="console-log console-${msg.type}">${escapeHtml(msg.message)}</div>`)
.join('');
output.scrollTop = output.scrollHeight;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function fetchContributions(username) {
try {
log(`Fetching monthly contributions for ${username}...`, 'info');
monthlyContributions[username] = {};
// Build GraphQL query with aliased monthly ranges
const fragments = monthRanges.map((r, i) =>
`m${i}: contributionsCollection(from: "${r.from}", to: "${r.to}") {
totalCommitContributions
totalPullRequestContributions
totalPullRequestReviewContributions
totalIssueContributions
contributionCalendar { totalContributions }
}`
).join('\n');
const query = `query($login: String!) {
user(login: $login) {
${fragments}
}
}`;
const gqlResponse = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${GITHUB_TOKEN}`,
},
body: JSON.stringify({ query, variables: { login: username } })
}).catch(err => {
log(`GraphQL fetch error: ${err.message}`, 'error');
return null;
});
let yearTotal = 0;
if (gqlResponse?.ok) {
const gqlData = await gqlResponse.json();
if (gqlData.errors) {
log(`GraphQL error for ${username}: ${gqlData.errors[0]?.message}`, 'error');
}
if (gqlData.data?.user) {
monthRanges.forEach((r, i) => {
const cc = gqlData.data.user[`m${i}`];
const total = cc?.contributionCalendar?.totalContributions || 0;
monthlyContributions[username][r.label] = total;
yearTotal += total;
});
log(` → monthly: ${monthRanges.map(r => monthlyContributions[username][r.label]).join(', ')}`, 'log');
} else if (gqlData.data?.user === null) {
log(`User ${username} not found in GitHub`, 'error');
}
} else if (gqlResponse) {
const errorData = await gqlResponse.json().catch(() => ({}));
log(`GraphQL HTTP ${gqlResponse.status}: ${errorData.message || 'Unknown error'}`, 'error');
}
contributions[username] = yearTotal;
log(`✓ ${username}: ${yearTotal} total contributions`, 'success');
return yearTotal;
} catch (error) {
log(`✗ Error fetching ${username}: ${error.message}`, 'error');
contributions[username] = 0;
return 0;
}
}
async function fetchOrgMembers() {
log(`Fetching members from organization: ${ORG_NAME}...`, 'info');
try {
let allMembers = [];
let page = 1;
while (true) {
const response = await fetch(`https://api.github.com/orgs/${ORG_NAME}/members?per_page=100&page=${page}`, {
headers: {
'Accept': 'application/vnd.github.v3+json',
'Authorization': `Bearer ${GITHUB_TOKEN}`
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status} - Make sure token has org:read scope`);
}
const members = await response.json();
if (members.length === 0) break;
allMembers = allMembers.concat(members);
log(`Fetched page ${page} (${members.length} members)...`, 'log');
if (members.length < 100) break;
page++;
}
users = allMembers.map(m => m.login);
log(`✓ Found ${users.length} total members: ${users.join(', ')}`, 'success');
return users;
} catch (error) {
log(`✗ Error fetching org members: ${error.message}`, 'error');
return [];
}
}
async function fetchAllContributions() {
// First fetch org members
await fetchOrgMembers();
if (users.length === 0) {
log('No users found. Check token permissions.', 'error');
return;
}
log('Starting contribution fetch for all users...', 'info');
log(`Users: ${users.join(', ')}`, 'log');
updateUsersList('loading');
let totalContribs = 0;
for (const user of users) {
const count = await fetchContributions(user);
totalContribs += count;
updateUsersList();
await new Promise(resolve => setTimeout(resolve, 300)); // Rate limiting
}
log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, 'log');
log(`Total Contributions: ${totalContribs}`, 'success');
log(`Average per User: ${(totalContribs / users.length).toFixed(2)}`, 'info');
log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, 'log');
updateStats(totalContribs);
}
function updateUsersList(state = 'normal') {
const usersList = document.getElementById('usersList');
if (users.length === 0) { usersList.innerHTML = ''; return; }
const headerCols = monthRanges.map(r => `<th style="padding:6px 8px;font-size:11px;color:#8b949e;white-space:nowrap;">${r.label}</th>`).join('');
const header = `<tr><th style="padding:6px 8px;text-align:left;color:#58a6ff;">User</th>${headerCols}<th style="padding:6px 8px;color:#58a6ff;">Total</th></tr>`;
const rows = users.map(user => {
const total = contributions[user] || 0;
const isLoading = state === 'loading' && total === 0;
const monthlyCells = monthRanges.map(r => {
const val = monthlyContributions[user]?.[r.label] ?? '';
return `<td style="padding:6px 8px;text-align:center;font-size:13px;">${isLoading ? '' : val}</td>`;
}).join('');
return `<tr style="border-bottom:1px solid var(--color-border);">
<td style="padding:6px 8px;"><span class="user-name">${user}</span></td>
${monthlyCells}
<td style="padding:6px 8px;text-align:center;">
<span class="contributions ${isLoading ? 'loading' : ''}">
${isLoading ? '<span class="spinner"></span>' : total}
</span>
</td>
</tr>`;
}).join('');
usersList.innerHTML = `<div style="overflow-x:auto;"><table style="width:100%;border-collapse:collapse;">${header}${rows}</table></div>`;
}
function updateStats(total) {
document.getElementById('totalUsers').textContent = users.length;
document.getElementById('totalContributions').textContent = total;
document.getElementById('averageContributions').textContent = (total / users.length).toFixed(2);
}
function exportToExcel() {
if (users.length === 0 || Object.keys(contributions).length === 0) {
log('No data to export. Fetch contributions first.', 'error');
return;
}
const monthLabels = monthRanges.map(r => r.label);
const rows = users.map(user => {
const row = { Username: user };
monthLabels.forEach(label => {
row[label] = monthlyContributions[user]?.[label] || 0;
});
row['Total'] = contributions[user] || 0;
return row;
});
// Summary rows
const totalRow = { Username: 'Total' };
const avgRow = { Username: 'Average' };
monthLabels.forEach(label => {
const colSum = rows.reduce((sum, r) => sum + (r[label] || 0), 0);
totalRow[label] = colSum;
avgRow[label] = (colSum / users.length).toFixed(2);
});
const grandTotal = rows.reduce((sum, r) => sum + (r['Total'] || 0), 0);
totalRow['Total'] = grandTotal;
avgRow['Total'] = (grandTotal / users.length).toFixed(2);
rows.push({});
rows.push(totalRow);
rows.push(avgRow);
const ws = XLSX.utils.json_to_sheet(rows);
ws['!cols'] = [{ wch: 25 }, ...monthLabels.map(() => ({ wch: 12 })), { wch: 12 }];
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Monthly Contributions');
XLSX.writeFile(wb, `${ORG_NAME}_monthly_contributions.xlsx`);
log('Exported monthly contributions to Excel!', 'success');
}
function clearConsole() {
consoleMessages = [];
document.getElementById('consoleOutput').innerHTML = '';
log('Console cleared', 'log');
}
// Initialize
log('GitHub Contributions Tracker Ready', 'info');
log(`Organization: ${ORG_NAME}`, 'log');
log('Click "Fetch Contributions" to load org members and their contributions', 'log');
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment