Created
April 13, 2026 06:15
-
-
Save nsdevaraj/0f4275b7fbcdec954368b70b77684c41 to your computer and use it in GitHub Desktop.
github productivity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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