diff --git a/index.css b/index.css index 25d6e86f..6036382c 100644 --- a/index.css +++ b/index.css @@ -530,6 +530,7 @@ body.dark-mode .quote-box { color: var(--primary-text-color); font-weight: bold; align-items: center; + justify-content: center; text-align:center; padding: 10px 20px; margin: 30px; diff --git a/js/team-script.js b/js/team-script.js index 8f034bab..dc393b32 100644 --- a/js/team-script.js +++ b/js/team-script.js @@ -27,7 +27,8 @@ const teamData = [ description: "A growing community of student developers working together to improve the platform.", image: null, social: [ - { platform: "team", url: "pr-contributors.html", icon: "fas fa-users" } + { platform: "team", url: "pages/pr-contribution/pr-contributors.html", icon: "fas fa-users" } + ] } ]; diff --git a/pages/pr-contribution/pr-contributor.css b/pages/pr-contribution/pr-contributor.css index 3d9d2005..b94b8394 100644 --- a/pages/pr-contribution/pr-contributor.css +++ b/pages/pr-contribution/pr-contributor.css @@ -213,7 +213,8 @@ nav #click { } /* Paragraph */ -.para { + +.para-pr { color: var(--primary-text-color); font-weight: 500; text-align: center; @@ -231,7 +232,7 @@ nav #click { animation: slideUp 0.8s ease 0.2s forwards; } -.para span { +.para-pr span { color: var(--accent-color-dark); font-weight: 700; background: linear-gradient(135deg, var(--accent-color-dark), var(--main-color)); @@ -670,12 +671,245 @@ nav #click { grid-template-columns: 1fr; } + +@media (max-width: 768px) { + nav { + padding: 0 20px; + } + + nav .menu-btn { + display: block; + color: var(--primary-text-color); + font-size: 20px; + cursor: pointer; + } + + nav ul { + position: fixed; + top: 80px; + left: -100%; + width: 100%; + height: calc(100vh - 80px); + background: var(--nav-bgcolor); + flex-direction: column; + align-items: center; + justify-content: flex-start; + padding-top: 50px; + transition: left 0.3s ease; + } + + nav #click:checked ~ ul { + left: 0; + } + + nav ul li { + margin: 10px 0; + } + + .container { + padding: 0 15px; + } + + .service-heading { + font-size: 2.5rem; + margin-bottom: 30px; + } + + .para-pr { + padding: 25px 20px; + font-size: 1rem; + } + + .pr-header h2 { + font-size: 2rem; + } + + .pr-header { + padding: 30px 20px; + } + + .section-title { + font-size: 2rem; + } + + @media (max-width: 768px) { + /* ... existing nav styles ... */ + + .ranking-podium { + flex-direction: column; + align-items: center; + gap: 30px; + padding: 40px 20px; + min-height: auto; + } + + .podium-item { + width: 260px; + min-height: 380px; + padding: 30px 20px; + margin-bottom: 0 !important; + } + + .position-1, + .position-2, + .position-3 { + order: unset; + min-height: 380px; + } +} + .contributor-rank-item { + flex-direction: column; + text-align: center; + gap: 20px; + padding: 25px 20px; + } + + .contributor-stats { + padding: 0; + } + + .rank-number { + order: -1; + font-size: 1.5rem; + min-width: 40px; + padding: 8px; + } + + .contributors-section, + .prs-section { + padding: 30px 20px; + } + + .contact-section .contact-item { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 20px; + } + + .social-media a { + width: 3rem; + height: 3rem; + font-size: 1.5rem; + margin: 2rem 1rem 0 0; + } + + .stat-card { + padding: 30px 20px; + } + + .pr-header-item { + flex-direction: column; + align-items: flex-start; + gap: 10px; + .dark-mode .avatar { width: 70px; height: 70px; + } + +@media (max-width: 480px) { + .service-heading { + font-size: 2rem; + } + + .para-pr { + padding: 20px 15px; + font-size: 0.95rem; + } + + .podium-item { + width: 240px; + min-height: 360px; + padding: 25px 15px; + } + + .podium-avatar img { + width: 80px; + height: 80px; + } + + .crown { + font-size: 2rem; + top: -15px; + } + + .contributor-rank-item { + padding: 20px 15px; + } + + .rank-number { + font-size: 1.3rem; + } + + .contributor-avatar img { + width: 60px; + height: 60px; + } + + .pr-item { + padding: 20px 15px; + } + + .contributors-section, + .prs-section { + padding: 25px 15px; + } + + .stats { + padding: 25px 15px; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + } + + .stat-card { + padding: 25px 15px; + } +} +/* Desktop podium specific adjustments */ +/* Desktop podium specific adjustments */ +@media (min-width: 769px) { + .ranking-podium { + align-items: flex-end; + justify-content: center; + gap: 50px; + flex-wrap: nowrap; + } + + .podium-item { + flex-shrink: 0; + } + + /* 2nd place (left side, shorter) */ + .position-2 { + order: 1; + margin-bottom: 80px; + } + + /* 1st place (center, tallest) */ + .position-1 { + order: 2; + margin-bottom: 0; + } + + /* 3rd place (right side, shortest) */ + .position-3 { + order: 3; + margin-bottom: 120px; + .dark-mode .contributor-card { padding: 15px; + } -} \ No newline at end of file +} + +.para-pr { + color: var(--primary-text-color); + font-weight: bold; + text-align: center; /* keeps text justified */ + padding: 10px; + margin: 30px auto; /* centers the paragraph block */ + max-width: 800px; /* optional: keeps it readable */ + display: block; /* makes sure it behaves like a block */ +} diff --git a/pages/pr-contribution/pr-contributors.html b/pages/pr-contribution/pr-contributors.html index 0a80a7e5..d492a6e0 100644 --- a/pages/pr-contribution/pr-contributors.html +++ b/pages/pr-contribution/pr-contributors.html @@ -38,7 +38,7 @@ - + @@ -58,7 +58,7 @@ - Change mode + Change mode @@ -66,7 +66,7 @@

🏆 PR Contributors

-

Welcome to the PR Contributors section! This page tracks and celebrates +

Welcome to the PR Contributors section!
This page tracks and celebrates all the amazing contributors who have helped improve Nitra Mitra through their pull requests. Here you can view contributor rankings, track merged PRs, and see the impact each contributor has made to our platform. Thank you to all our contributors for making @@ -138,7 +138,7 @@

Contact Us

Privacy Policy | Terms & Conditions | Contact | - Contributors + Contributors

Contact Us

+ + + + + + -->
@@ -292,6 +298,7 @@

${c.username}

+ diff --git a/pages/pr-contribution/pr.js b/pages/pr-contribution/pr.js new file mode 100644 index 00000000..7cbb08ff --- /dev/null +++ b/pages/pr-contribution/pr.js @@ -0,0 +1,118 @@ +(async function () { + 'use strict'; + + const REPO = 'VAIBHAVBABELE/vaibhavbabele.github.io'; // Replace with your repo + const API_TOKEN = ''; // Optional: GitHub token to avoid rate limits + const HEADERS = { Accept: 'application/vnd.github.v3+json' }; + if (API_TOKEN) HEADERS['Authorization'] = `token ${API_TOKEN}`; + + const contentDiv = document.getElementById('content'); + + function showLoading(msg) { + contentDiv.innerHTML = `
${msg}
`; + } + + function showError(msg) { + contentDiv.innerHTML = `
❌ ${msg}
`; + } + + function escapeHtml(s) { + return String(s || '').replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); + } + + // Fetch all merged PRs with pagination + async function fetchAllMergedPRs() { + let page = 1, perPage = 100, mergedPRs = []; + while (true) { + const res = await fetch(`https://api.github.com/repos/${REPO}/pulls?state=closed&per_page=${perPage}&page=${page}`, { headers: HEADERS }); + if (!res.ok) throw new Error(`GitHub API error: ${res.status}`); + const prs = await res.json(); + const merged = prs.filter(p => p.merged_at); + mergedPRs = mergedPRs.concat(merged); + if (prs.length < perPage) break; + page++; + } + return mergedPRs; + } + + // Fallback: fetch leaderboard.md + async function fetchLeaderboardMD() { + const paths = ['./leaderboard.md', '../leaderboard.md', '../../leaderboard.md']; + for (const p of paths) { + try { + const res = await fetch(p); + if (!res.ok) continue; + const text = await res.text(); + const map = parseLeaderboard(text); + if (map.size) return map; + } catch { continue; } + } + return null; + } + + function parseLeaderboard(mdText) { + const map = new Map(); + const lines = mdText.split(/\r?\n/).map(l => l.trim()).filter(Boolean); + for (const line of lines) { + const m = line.match(/@?([A-Za-z0-9\-_]+)\s*[:\-–]\s*(\d+)/); + if (m) map.set(m[1], parseInt(m[2])); + } + return map; + } + + function render(contributors, mergedPRs) { + const mergedCount = mergedPRs.length; + const contribCount = contributors.length; + + let html = ` +
+
${mergedCount}
Merged PRs
+
${contribCount}
Contributors
+
+
`; + + contributors.forEach(c => { + html += `
+ ${c.name} +
${escapeHtml(c.name)}
+
⭐ ${c.score || 0}
+
📊 PRs: ${c.totalPRs || 0}
+
`; + }); + + html += `
`; + contentDiv.innerHTML = html; + } + + async function main() { + showLoading('🔄 Loading contributors...'); + try { + let contributors = []; + let mergedPRs = []; + + try { + mergedPRs = await fetchAllMergedPRs(); + const map = new Map(); + mergedPRs.forEach(pr => { + const name = pr.user.login; + if (!map.has(name)) map.set(name, { name, avatar: pr.user.avatar_url, totalPRs: 0, score: 0 }); + const c = map.get(name); + c.totalPRs++; + c.score = c.totalPRs * 5; // simple scoring + }); + contributors = Array.from(map.values()); + } catch (err) { + console.warn('GitHub API failed, using leaderboard.md', err); + const lb = await fetchLeaderboardMD(); + if (!lb) throw new Error('Leaderboard not found.'); + contributors = Array.from(lb.entries()).map(([name, score]) => ({ name, score, totalPRs: 0, avatar: '' })); + } + + render(contributors, mergedPRs); + } catch (err) { + showError(err.message); + } + } + + window.addEventListener('load', main); +})(); diff --git a/pages/pr-contribution/pr_contributor.js b/pages/pr-contribution/pr_contributor.js new file mode 100644 index 00000000..5019328d --- /dev/null +++ b/pages/pr-contribution/pr_contributor.js @@ -0,0 +1,698 @@ + + +(function () { + 'use strict'; + + // ----- Configuration ----- + const API_TOKEN = ''; // Optional: add a GitHub token to increase rate limits + let useApiToken = Boolean(API_TOKEN && API_TOKEN.length > 0); + + const CACHE_DURATION = 60 * 60 * 1000; // 1 hour + const CACHE_KEYS = { + PRS: 'github_prs_cache', + CONTRIBUTORS: 'github_contributors_cache', + TIMESTAMP: 'github_cache_timestamp' + }; + + // ----- Utilities ----- + function debugLog(...args) { + if (window.location.search.includes('debug')) console.log(...args); + } + + function saveToCache(key, data) { + try { + sessionStorage.setItem(key, JSON.stringify(data)); + sessionStorage.setItem(CACHE_KEYS.TIMESTAMP, Date.now().toString()); + debugLog('Saved to cache', key); + } catch (e) { + console.warn('Cache save failed', e); + } + } + + function getFromCache(key) { + try { + const timestamp = sessionStorage.getItem(CACHE_KEYS.TIMESTAMP); + if (!timestamp) return null; + const age = Date.now() - parseInt(timestamp, 10); + if (age > CACHE_DURATION) { + clearCache(); + return null; + } + const raw = sessionStorage.getItem(key); + return raw ? JSON.parse(raw) : null; + } catch (e) { + console.warn('Cache read failed', e); + return null; + } + } + + function clearCache() { + try { + Object.values(CACHE_KEYS).forEach(k => sessionStorage.removeItem(k)); + console.log('🗑️ Cache cleared'); + } catch (e) { + console.warn('Cache clear failed', e); + } + } + + function isCacheValid() { + const ts = sessionStorage.getItem(CACHE_KEYS.TIMESTAMP); + if (!ts) return false; + return (Date.now() - parseInt(ts, 10)) < CACHE_DURATION; + } + + function displayCacheStatus() { + const ts = sessionStorage.getItem(CACHE_KEYS.TIMESTAMP); + if (!ts) return; + const age = Date.now() - parseInt(ts, 10); + const remaining = Math.max(0, CACHE_DURATION - age); + const minutes = Math.floor(remaining / 60000); + const seconds = Math.floor((remaining % 60000) / 1000); + const statusDiv = document.createElement('div'); + statusDiv.className = 'cache-status'; + statusDiv.style.cssText = ` + position: fixed; + bottom: 12px; + right: 12px; + background: rgba(0,128,0,0.85); + color: white; + padding: 8px; + border-radius: 6px; + font-size: 12px; + z-index: 10000; + `; + statusDiv.textContent = `💾 Cache: ${minutes}m ${seconds}s left`; + document.body.appendChild(statusDiv); + setTimeout(() => statusDiv.remove(), 5000); + } + + function checkRateLimitFromHeaders(response) { + if (!response || !response.headers) return null; + const info = { + limit: response.headers.get('X-RateLimit-Limit'), + remaining: response.headers.get('X-RateLimit-Remaining'), + reset: response.headers.get('X-RateLimit-Reset') + }; + if (info.reset) info.resetDate = new Date(parseInt(info.reset, 10) * 1000).toLocaleString(); + return info; + } + + function displayRateLimitOnPage(rateLimitInfo) { + if (!rateLimitInfo) return; + const el = document.querySelector('.rate-limit-info'); + if (el) el.remove(); + const div = document.createElement('div'); + div.className = 'rate-limit-info'; + div.style.cssText = ` + position: fixed; + top: 12px; + right: 12px; + background: rgba(0,0,0,0.8); + color: white; + padding: 8px; + border-radius: 6px; + font-size: 12px; + z-index: 10000; + `; + div.innerHTML = `GitHub API
Remaining: ${rateLimitInfo.remaining||'N/A'}/${rateLimitInfo.limit||'N/A'}
Reset: ${rateLimitInfo.resetDate || 'N/A'}`; + document.body.appendChild(div); + setTimeout(()=>div.remove(), 8000); + } + + function getContrastColor(hexColor) { + if (!hexColor) return '#000'; + hexColor = hexColor.replace('#', ''); + if (hexColor.length !== 6) return '#000'; + const r = parseInt(hexColor.substr(0,2),16); + const g = parseInt(hexColor.substr(2,2),16); + const b = parseInt(hexColor.substr(4,2),16); + const luminance = (0.299*r + 0.587*g + 0.114*b) / 255; + return luminance > 0.5 ? '#000000' : '#ffffff'; + } + + function getRepoFromDocOrHost() { + // 1) explicit override via + const bodyRepo = document.body.getAttribute('data-repo'); + if (bodyRepo && bodyRepo.includes('/')) return bodyRepo.trim(); + + // 2) if running on GitHub Pages (owner.github.io / repo in path) + const hostname = window.location.hostname.toLowerCase(); + const pathParts = window.location.pathname.split('/').filter(Boolean); + if (hostname.endsWith('github.io')) { + const owner = hostname.split('.')[0]; + const repo = pathParts.length > 0 ? pathParts[0] : `${owner}.github.io`; + return `${owner}/${repo}`; + } + + // 3) fallback (edit this if you want a different default) + console.warn('Could not auto-detect repo — falling back to default. Set to override.'); + return 'VAIBHAVBABELE/vaibhavbabele.github.io'; + } + + // Try fetching a local or raw GitHub leaderboard.md. Returns a Map(usernameLower -> points) or null. + async function tryLoadLeaderboard(ownerRepo) { + const [owner, repo] = ownerRepo.split('/'); + const candidatePaths = [ + './leaderboard.md', + '../leaderboard.md', + '../../leaderboard.md', + '../../../leaderboard.md' + ]; + + // 1) try relative local files + for (const p of candidatePaths) { + try { + const r = await fetch(p); + if (r.ok) { + const text = await r.text(); + const parsed = parseLeaderboardMarkdown(text); + if (parsed && parsed.size) { + console.log('✅ Loaded leaderboard.md from relative path', p); + return parsed; + } + } + } catch (e) { + // ignore + } + } + + // 2) try raw.githubusercontent (main, then master) + const rawCandidates = [ + `https://raw.githubusercontent.com/${owner}/${repo}/main/leaderboard.md`, + `https://raw.githubusercontent.com/${owner}/${repo}/master/leaderboard.md` + ]; + for (const url of rawCandidates) { + try { + const r = await fetch(url); + if (r.ok) { + const text = await r.text(); + const parsed = parseLeaderboardMarkdown(text); + if (parsed && parsed.size) { + console.log('✅ Loaded leaderboard.md from', url); + return parsed; + } + } + } catch (e) { + // ignore + } + } + + return null; + } + + // Lightweight markdown table or list parser for leaderboard.md + function parseLeaderboardMarkdown(mdText) { + if (!mdText) return null; + const lines = mdText.split(/\r?\n/).map(l => l.trim()).filter(Boolean); + const tableRows = lines.filter(l => l.startsWith('|') && l.includes('|')); + const map = new Map(); + + if (tableRows.length >= 2) { + // Try to parse header and data rows + // header e.g. | Rank | Contributor | Points | + const headerCols = tableRows[0].split('|').map(s => s.trim()).filter(Boolean); + const pointsIdx = headerCols.findIndex(h => /points|score/i.test(h)); + const nameIdx = headerCols.findIndex(h => /contributor|name|github|user/i.test(h)); + // default fallbacks + const pIdx = pointsIdx >= 0 ? pointsIdx : headerCols.length - 1; + const nIdx = nameIdx >= 0 ? nameIdx : 1; + + // data rows start after header and separator + for (let i = 1; i < tableRows.length; i++) { + const cols = tableRows[i].split('|').map(s=>s.trim()).filter(Boolean); + if (cols.length < 2) continue; + const rawName = (cols[nIdx] || cols[0] || '').replace(/["'`]/g, '').trim(); + const rawPoints = (cols[pIdx] || cols[cols.length - 1] || '').replace(/[^0-9\-]/g, '').trim(); + const points = parseInt(rawPoints, 10); + const username = sanitizeLeaderboardName(rawName); + if (username && !Number.isNaN(points)) { + map.set(username.toLowerCase(), points); + } + } + if (map.size) return map; + } + + // Fallback: parse bullet lines like "- username - 42" or "username: 42" + for (const line of lines) { + // skip header lines + if (/^#/.test(line) || /^\|/.test(line)) continue; + const match1 = line.match(/@?([A-Za-z0-9\-_]+)\s*[:\-–]\s*(\d+)/); + if (match1) { + map.set(match1[1].toLowerCase(), parseInt(match1[2], 10)); + continue; + } + const match2 = line.match(/([A-Za-z0-9\-_]+)\s+(\d+)\s*pts?/i); + if (match2) { + map.set(match2[1].toLowerCase(), parseInt(match2[2], 10)); + continue; + } + } + + return map.size ? map : null; + } + + function sanitizeLeaderboardName(raw) { + if (!raw) return ''; + // if it's a markdown link: [name](https://github.com/username) -> try extract username + const linkMatch = raw.match(/\((https?:\/\/github\.com\/([A-Za-z0-9\-_]+))\)/i); + if (linkMatch) return linkMatch[2]; + // if there's a slash like owner/username or @username + const atMatch = raw.match(/@?([A-Za-z0-9\-_]+)/); + return atMatch ? atMatch[1] : raw.replace(/[^A-Za-z0-9\-_]/g,''); + } + + // ----- Main logic ----- + async function fetchAllMergedPRs(repo) { + const headers = { 'Accept': 'application/vnd.github.v3+json' }; + if (useApiToken && API_TOKEN) headers['Authorization'] = `token ${API_TOKEN}`; + + let page = 1; + const perPage = 100; + let all = []; + while (true) { + const url = `https://api.github.com/repos/${repo}/pulls?state=closed&per_page=${perPage}&page=${page}&sort=updated&direction=desc`; + const resp = await fetch(url, { headers }); + const rl = checkRateLimitFromHeaders(resp); + displayRateLimitOnPage(rl); + if (!resp.ok) { + if (resp.status === 403 && !useApiToken && API_TOKEN) { + useApiToken = true; + return await fetchAllMergedPRs(repo); + } + throw new Error(`GitHub API error fetching PRs: ${resp.status} ${resp.statusText}`); + } + const pageData = await resp.json(); + if (!Array.isArray(pageData)) break; + all = all.concat(pageData.filter(p => p.merged_at)); + if (pageData.length < perPage) break; + page++; + await new Promise(r => setTimeout(r, 300)); // small delay between pages + } + return all; + } + + // Compute initial contributor entries from PRs and then fetch profile details in batches + async function processContributors(prs, repo, leaderboardMap) { + const contributors = new Map(); + prs.forEach(pr => { + const username = (pr.user && pr.user.login) ? pr.user.login : 'unknown'; + if (!contributors.has(username)) { + contributors.set(username, { + name: username, + avatar: pr.user ? pr.user.avatar_url : '', + profileUrl: pr.user ? pr.user.html_url : '', + prs: [], + totalPRs: 0, + levelPRs: {1:0,2:0,3:0}, + maxLevel: 1, + level: 1, + score: 0, + bio: '', + company: '', + location: '', + linkedinUrl: '', + twitterUrl: '', + websiteUrl: '' + }); + } + const c = contributors.get(username); + c.prs.push(pr); + c.totalPRs++; + const lvl = extractLevelFromLabels(pr.labels || []); + c.levelPRs[lvl] = (c.levelPRs[lvl] || 0) + 1; + if (lvl > c.maxLevel) c.maxLevel = lvl; + }); + + // Batch fetch profiles for contributors (skip some well-known bots or yourself if needed) + const excludedUsers = ['dependabot[bot]']; // add any additional excludes (case-sensitive) + const usernames = Array.from(contributors.keys()).filter(u => !excludedUsers.includes(u)); + const batchSize = 10; + for (let i = 0; i < usernames.length; i += batchSize) { + const batch = usernames.slice(i, i + batchSize); + const promises = batch.map(async (username, idx) => { + try { + // small stagger inside batch + await new Promise(r => setTimeout(r, idx * 150)); + const headers = {'Accept': 'application/vnd.github.v3+json'}; + if (useApiToken && API_TOKEN) headers['Authorization'] = `token ${API_TOKEN}`; + const res = await fetch(`https://api.github.com/users/${username}`, { headers }); + if (!res.ok) { + if (res.status === 403) { + console.warn('Rate limited fetching user', username); + return; + } + console.warn('Failed to fetch user', username, res.status); + return; + } + const ud = await res.json(); + const c = contributors.get(username); + if (!c) return; + c.bio = ud.bio || ''; + c.company = ud.company || ''; + c.location = ud.location || ''; + c.websiteUrl = ud.blog || ''; + c.twitterUrl = ud.twitter_username ? `https://twitter.com/${ud.twitter_username}` : ''; + // try to detect linkedin from bio + const bioLower = (ud.bio || '').toLowerCase(); + const liMatch = (ud.blog || '').match(/linkedin\.com\/in\/[A-Za-z0-9\-_]+/i) || bioLower.match(/linkedin\.com\/in\/([a-z0-9\-_]+)/i); + if (liMatch) { + c.linkedinUrl = liMatch[0].startsWith('http') ? liMatch[0] : `https://${liMatch[0]}`; + } + } catch (e) { + console.warn('Error fetching user profile', username, e.message); + } + }); + await Promise.all(promises); + // small pause between batches + if (i + batchSize < usernames.length) await new Promise(r => setTimeout(r, 700)); + } + + // compute baseline scores from labels + contributors.forEach(c => { + c.level = c.maxLevel || 1; + c.score = ( (c.levelPRs[1] || 0) * 3 ) + ( (c.levelPRs[2] || 0) * 7 ) + ( (c.levelPRs[3] || 0) * 10 ); + }); + + // if a leaderboard.md override exists, apply those points (override score) + if (leaderboardMap && leaderboardMap.size) { + leaderboardMap.forEach((points, usernameLower) => { + // find matching contributor by lowercase username + const found = Array.from(contributors.values()).find(cc => cc.name.toLowerCase() === usernameLower); + if (found) { + found.score = points; + found._scoreSource = 'leaderboard.md'; + } else { + // If not found, add a placeholder contributor (so leaderboard matches) + contributors.set(usernameLower, { + name: usernameLower, + avatar: '', + profileUrl: `https://github.com/${usernameLower}`, + prs: [], + totalPRs: 0, + levelPRs: {1:0,2:0,3:0}, + maxLevel: 1, + level: 1, + score: points, + bio: '', + company: '', + location: '', + linkedinUrl: '', + twitterUrl: '', + websiteUrl: '', + _scoreSource: 'leaderboard.md' + }); + } + }); + } + + return contributors; + } + + function extractLevelFromLabels(labels) { + if (!Array.isArray(labels)) return 1; + for (const label of labels) { + const n = (label && label.name) ? label.name.toString().toLowerCase() : ''; + if (/level\s*3|level3|hard|critical/.test(n)) return 3; + if (/level\s*2|level2|medium|moderate/.test(n)) return 2; + if (/level\s*1|level1|easy|low/.test(n)) return 1; + } + return 1; + } + + // ----- Render helpers ----- + function createCertificateLink(obj, rank) { + const params = new URLSearchParams({ + name: obj.name || '', + level: obj.level || '', + prs: obj.totalPRs || 0, + rank: rank || '', + score: obj.score || 0, + avatar: encodeURIComponent(obj.avatar || '') + }); + return `certificate.html?${params.toString()}`; + } + + function displayResults(prs, contributorsMap) { + const contentDiv = document.getElementById('content'); + if (!contentDiv) { + console.error('content element not found'); + return; + } + + const contributorsCount = contributorsMap.size; + const level3Count = Array.from(contributorsMap.values()).filter(c => c.level === 3).length; + const level2Count = Array.from(contributorsMap.values()).filter(c => c.level === 2).length; + + const statsHTML = ` +
+
${prs.length}
Merged PRs
+
${contributorsCount}
Contributors
+
${level3Count}
Level 3
+
${level2Count}
Level 2
+
+ `; + + const contributorsHTML = renderContributorsSection(contributorsMap); + const prsHTML = renderPRsSection(prs); + + contentDiv.innerHTML = statsHTML + contributorsHTML + prsHTML; + } + + function renderContributorsSection(contributorsMap) { + const excludedUsersLower = ['vaibhavbabele', 'dependabot[bot]'].map(s => s.toLowerCase()); + + const arr = Array.from(contributorsMap.values()).filter(c => !excludedUsersLower.includes((c.name || '').toLowerCase())); + arr.sort((a, b) => { + if (b.score !== a.score) return b.score - a.score; + return (b.totalPRs || 0) - (a.totalPRs || 0); + }); + + const topThree = arr.slice(0, 3); + const rest = arr.slice(3); + + // podium + let podiumHTML = `
`; + const podiumItem = (item, pos) => { + if (!item) return ''; + const crown = pos === 1 ? '👑' : pos === 2 ? '🥈' : '🥉'; + const rank = pos; + const certLink = createCertificateLink(item, rank); + return ` +
+
${item.name}
${crown}
+
${item.name}
+
⭐ ${item.score}
+
Level ${item.level}
+
📊 ${item.totalPRs || 0} PRs
+ + +
+ `; + }; + + // order visually as 2,1,3 if you want center-first look — keeping simple: 1,2,3 + podiumHTML += podiumItem(topThree[0], 1); + podiumHTML += podiumItem(topThree[1], 2); + podiumHTML += podiumItem(topThree[2], 3); + podiumHTML += `
`; + + // list the rest + const listHTML = rest.map((c, i) => { + const rank = i + 4; + const levelTextArr = ['Beginner','Intermediate','Advanced']; + const levelText = levelTextArr[(c.level || 1) - 1] || 'Beginner'; + const certLink = createCertificateLink(c, rank); + return ` +
+
${rank}
+
${c.name}
+
+
${c.name}
+
Level ${c.level || 1} - ${levelText}
+ ${c.bio ? `
${c.bio.substring(0,120)}${c.bio.length>120 ? '...' : ''}
` : ''} +
+
+
⭐ ${c.score}
+
📊 ${c.totalPRs || 0} PRs
+
+
+ + +
+
+ `; + }).join(''); + + return ` +
+

🏆 Top Contributors Ranking

+ ${podiumHTML} +
+ ${listHTML} +
+
+ `; + } + + function renderPRsSection(prs) { + const prsHTML = prs.slice(0, 10).map(pr => { + const labels = (pr.labels || []).map(label => `${label.name}`).join(''); + const mergedDate = pr.merged_at ? new Date(pr.merged_at).toLocaleDateString() : ''; + const level = extractLevelFromLabels(pr.labels || []); + const emoji = level === 3 ? '💎' : level === 2 ? '⭐' : '🔰'; + return ` +
+
+
${emoji} ${escapeHtml(pr.title || '')}
+ #${pr.number} +
+
👤 ${pr.user.login} 📅 Merged: ${mergedDate}
+ ${labels ? `
${labels}
` : ''} +
+ `; + }).join(''); + return ` +
+

📋 Recent Merged Pull Requests

+
${prsHTML}
+
+ `; + } + + function escapeHtml(s) { + return String(s || '').replace(/[&<>"']/g, function(m){ return ({'&':'&','<':'<','>':'>','"':'"',"'":"'"})[m]; }); + } + + function showError(message) { + const contentDiv = document.getElementById('content'); + if (contentDiv) contentDiv.innerHTML = `
❌ ${escapeHtml(message)}
`; + } + + // Add a small refresh button for manual reload / clearing cache + function addRefreshButton() { + const container = document.createElement('div'); + container.style.cssText = 'position:fixed; top:100px; left:12px; z-index:10000;'; + container.innerHTML = ``; + document.body.appendChild(container); + const btn = document.getElementById('pr-refresh-btn'); + btn.addEventListener('click', () => { + clearCache(); + fetchPRs(); // re-run main flow + }); + } + + // ----- Main entrypoint ----- + async function fetchPRs() { + const contentDiv = document.getElementById('content'); + if (!contentDiv) { + console.error('No #content container. Aborting.'); + return; + } + contentDiv.innerHTML = `
🔄 Fetching contributors — please wait...
`; + + const repo = getRepoFromDocOrHost(); + if (!repo) { + showError('Could not determine repository. Add data-repo="owner/repo" to .'); + return; + } + + // Try cache + const cachedPRs = getFromCache(CACHE_KEYS.PRS); + const cachedContributors = getFromCache(CACHE_KEYS.CONTRIBUTORS); + + if (cachedPRs && cachedContributors && isCacheValid()) { + console.log('Using cached data'); + const prs = cachedPRs; + const contributorsMap = new Map(cachedContributors); + displayResults(prs, contributorsMap); + displayCacheStatus(); + return; + } + + try { + // Try loading leaderboard.md (local or raw) + const leaderboardMap = await tryLoadLeaderboard(repo); + + let mergedPRs = []; + let contributorsMap = new Map(); + + try { + // Fetch all merged PRs (paginated) + mergedPRs = await fetchAllMergedPRs(repo); + + // Process contributors and fetch profile details + contributorsMap = await processContributors(mergedPRs, repo, leaderboardMap); + + // Cache results + saveToCache(CACHE_KEYS.PRS, mergedPRs); + saveToCache(CACHE_KEYS.CONTRIBUTORS, Array.from(contributorsMap.entries())); + + console.log('✅ Contributors data fetched via GitHub API.'); + + } catch (apiErr) { + console.warn('GitHub API failed, falling back to leaderboard.md', apiErr); + + if (!leaderboardMap || leaderboardMap.size === 0) { + showError('Could not fetch contributors from GitHub API and leaderboard.md is empty.'); + return; + } + + // Build contributorsMap directly from leaderboard + leaderboardMap.forEach((points, usernameLower) => { + contributorsMap.set(usernameLower, { + name: usernameLower, + avatar: '', + profileUrl: `https://github.com/${usernameLower}`, + prs: [], + totalPRs: 0, + levelPRs: {1:0,2:0,3:0}, + maxLevel: 1, + level: 1, + score: points, + bio: '', + company: '', + location: '', + linkedinUrl: '', + twitterUrl: '', + websiteUrl: '', + _scoreSource: 'leaderboard.md' + }); + }); + + console.log('✅ Contributors data loaded from leaderboard.md fallback.'); + } + + // Finally, display whatever we have + displayResults(mergedPRs, contributorsMap); + displayCacheStatus(); + +} catch (err) { + console.error(err); + showError(err.message || 'Unknown error while fetching contributors.'); +} + + } + + // Initialize on load + window.addEventListener('load', () => { + try { + fetchPRs(); + addRefreshButton(); + } catch (e) { + console.error(e); + } + }); + +})(); + + diff --git a/pages/quantum.html b/pages/quantum.html index cc444945..4bcb0d79 100644 --- a/pages/quantum.html +++ b/pages/quantum.html @@ -272,7 +272,7 @@

Contact Us

Privacy Policy | Terms & Conditions | Contact | - Contributors + Contributors