mirror of
https://github.com/baz-scm/awesome-reviewers.git
synced 2025-08-20 18:58:52 +03:00
378 lines
16 KiB
HTML
378 lines
16 KiB
HTML
---
|
|
layout: default
|
|
title: Leaderboard
|
|
description: Top contributors powering Awesome Reviewers
|
|
---
|
|
|
|
<section class="hero leaderboard-hero">
|
|
<div class="container">
|
|
<a href="/" class="back-link">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
|
|
</svg>
|
|
Back to all reviewers
|
|
</a>
|
|
<h1>Leaderboard</h1>
|
|
<p>Top contributors behind the Awesome Reviewers prompts.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="library-header">
|
|
<div class="container">
|
|
<h2>Contributors (<span id="leader-count">{{ site.data.leaderboard | size }}</span>)</h2>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="leaderboard">
|
|
<div class="container">
|
|
<table id="leaderboard-table" class="leaderboard-table">
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Name</th>
|
|
<th>Reviewers</th>
|
|
<th>Repositories</th>
|
|
<th>Location</th>
|
|
<th>Company</th>
|
|
<th>Links</th>
|
|
<th>Last Contribution</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for user in site.data.leaderboard %}
|
|
<tr data-contributor="{{ user.name }}">
|
|
<td>{{ forloop.index }}</td>
|
|
<td class="lb-name">
|
|
{% if forloop.index == 1 %}🥇 {% elsif forloop.index == 2 %}🥈 {% elsif forloop.index == 3 %}🥉 {% endif %}
|
|
<img class="lb-avatar" src="https://github.com/{{ user.name }}.png?size=32" alt="{{ user.name }} avatar">
|
|
<a href="https://github.com/{{ user.name }}" target="_blank" rel="noopener noreferrer">{{ user.name }}</a>
|
|
</td>
|
|
<td data-sort="{{ user.reviewers_count }}">{{ user.reviewers_count }}</td>
|
|
<td data-sort="{{ user.repos_count }}">{{ user.repos_count }}</td>
|
|
<td class="loc-cell"></td>
|
|
<td class="company-cell"></td>
|
|
<td class="links-cell"></td>
|
|
<td data-sort="{{ user.last_contribution }}">{{ user.last_contribution | date: '%Y-%m-%d' }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<div id="drawer" class="drawer">
|
|
<div class="drawer-overlay" onclick="closeDrawer()"></div>
|
|
<div class="drawer-panel">
|
|
<div class="drawer-header">
|
|
<button class="drawer-close" onclick="closeDrawer()">×</button>
|
|
</div>
|
|
<div id="drawer-content"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function sortTable(col) {
|
|
const table = document.getElementById('leaderboard-table');
|
|
const tbody = table.tBodies[0];
|
|
const rows = Array.from(tbody.rows);
|
|
const asc = table.dataset.sortCol == col && table.dataset.sortDir === 'asc' ? false : true;
|
|
rows.sort(function(a, b) {
|
|
let aVal = a.cells[col].dataset.sort || a.cells[col].innerText;
|
|
let bVal = b.cells[col].dataset.sort || b.cells[col].innerText;
|
|
const numA = parseFloat(aVal); const numB = parseFloat(bVal);
|
|
if(!isNaN(numA) && !isNaN(numB)) {
|
|
return asc ? numA - numB : numB - numA;
|
|
}
|
|
return asc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
|
});
|
|
rows.forEach(r => tbody.appendChild(r));
|
|
table.dataset.sortCol = col;
|
|
table.dataset.sortDir = asc ? 'asc' : 'desc';
|
|
}
|
|
|
|
document.querySelectorAll('#leaderboard-table th').forEach((th, i) => {
|
|
th.addEventListener('click', () => sortTable(i));
|
|
});
|
|
|
|
let contributors = {};
|
|
fetch('/assets/data/contributors.json')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
contributors = data;
|
|
const hash = location.hash.slice(1);
|
|
if (hash && contributors[hash]) {
|
|
openContributorDrawer(hash);
|
|
}
|
|
loadProfileData();
|
|
})
|
|
.catch(err => {
|
|
console.error('Failed to load contributors dataset', err);
|
|
});
|
|
|
|
attachContributorEvents();
|
|
|
|
function attachContributorEvents() {
|
|
document.querySelectorAll('tr[data-contributor]').forEach(row => {
|
|
row.addEventListener('click', e => {
|
|
if (e.target.tagName.toLowerCase() === 'a') return;
|
|
const user = row.dataset.contributor;
|
|
openContributorDrawer(user);
|
|
});
|
|
});
|
|
}
|
|
|
|
function loadProfileData() {
|
|
const rows = document.querySelectorAll('tr[data-contributor]');
|
|
rows.forEach((row, index) => {
|
|
const user = row.dataset.contributor;
|
|
const stored = contributors[user] && contributors[user].profile;
|
|
if (stored) {
|
|
applyProfile(row, stored);
|
|
return;
|
|
}
|
|
setTimeout(() => {
|
|
fetch(`https://api.github.com/users/${user}`, {
|
|
headers: {
|
|
'Accept': 'application/vnd.github+json',
|
|
'X-GitHub-Api-Version': '2022-11-28'
|
|
}
|
|
})
|
|
.then(r => (r.ok ? r.json() : null))
|
|
.then(data => {
|
|
if (!data) return;
|
|
if (contributors[user]) contributors[user].profile = data;
|
|
applyProfile(row, data);
|
|
})
|
|
.catch(() => {});
|
|
}, index * 100);
|
|
});
|
|
}
|
|
|
|
function applyProfile(row, data) {
|
|
const locCell = row.querySelector('.loc-cell');
|
|
const compCell = row.querySelector('.company-cell');
|
|
const linksCell = row.querySelector('.links-cell');
|
|
if (data.location && locCell) {
|
|
locCell.dataset.sort = data.location;
|
|
locCell.innerHTML = `<svg class="meta-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg> ${data.location}`;
|
|
}
|
|
if (data.company && compCell) {
|
|
compCell.dataset.sort = data.company;
|
|
compCell.innerHTML = `<svg class="meta-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg> ${data.company}`;
|
|
}
|
|
if (linksCell) {
|
|
const links = [];
|
|
if (data.blog) {
|
|
links.push(`<a href="${data.blog}" target="_blank" rel="noopener noreferrer" title="Website"><svg class="meta-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></a>`);
|
|
}
|
|
if (data.twitter_username) {
|
|
links.push(`<a href="https://twitter.com/${data.twitter_username}" target="_blank" rel="noopener noreferrer" title="Twitter"><svg class="meta-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"/></svg></a>`);
|
|
}
|
|
if (data.email) {
|
|
links.push(`<a href="mailto:${data.email}" title="Email"><svg class="meta-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg></a>`);
|
|
}
|
|
linksCell.innerHTML = links.join(' ');
|
|
}
|
|
}
|
|
|
|
function openContributorDrawer(user) {
|
|
const info = contributors[user];
|
|
if (!info) return;
|
|
const drawer = document.getElementById('drawer');
|
|
const content = document.getElementById('drawer-content');
|
|
content.innerHTML = '';
|
|
|
|
const header = document.createElement('div');
|
|
header.className = 'contrib-header';
|
|
const avatar = document.createElement('img');
|
|
avatar.className = 'contrib-avatar';
|
|
avatar.src = `https://github.com/${user}.png?size=80`;
|
|
avatar.alt = `${user} avatar`;
|
|
header.appendChild(avatar);
|
|
const infoBox = document.createElement('div');
|
|
const nameEl = document.createElement('div');
|
|
nameEl.className = 'contrib-name';
|
|
nameEl.textContent = user;
|
|
infoBox.appendChild(nameEl);
|
|
const userLink = document.createElement('a');
|
|
userLink.href = `https://github.com/${user}`;
|
|
userLink.target = '_blank';
|
|
userLink.rel = 'noopener noreferrer';
|
|
userLink.className = 'contrib-username';
|
|
userLink.textContent = '@' + user;
|
|
infoBox.appendChild(userLink);
|
|
|
|
const stats = document.createElement('div');
|
|
stats.className = 'contrib-stats';
|
|
const totalComments = info.comments ? Object.values(info.comments).reduce((a,b) => a + b.length, 0) : 0;
|
|
stats.innerHTML = `
|
|
<div class="contrib-stat"><span class="contrib-stat-value">${info.repos.length}</span>Repos</div>
|
|
<div class="contrib-stat"><span class="contrib-stat-value">${info.entries.length}</span>Entries</div>
|
|
<div class="contrib-stat"><span class="contrib-stat-value">${totalComments}</span>Comments</div>
|
|
`;
|
|
infoBox.appendChild(stats);
|
|
|
|
const metaList = document.createElement('ul');
|
|
metaList.className = 'profile-meta';
|
|
infoBox.appendChild(metaList);
|
|
header.appendChild(infoBox);
|
|
content.appendChild(header);
|
|
|
|
const existing = info.profile;
|
|
const load = existing ? Promise.resolve(existing) : fetch(`https://api.github.com/users/${user}`, {
|
|
headers: {
|
|
'Accept': 'application/vnd.github+json',
|
|
'X-GitHub-Api-Version': '2022-11-28'
|
|
}
|
|
}).then(r => r.ok ? r.json() : null);
|
|
load.then(async data => {
|
|
if (!data) return;
|
|
if (!existing) info.profile = data;
|
|
if (data.site_admin) {
|
|
const badge = document.createElement('span');
|
|
badge.className = 'verified-badge';
|
|
badge.textContent = '✓';
|
|
nameEl.appendChild(badge);
|
|
|
|
const row = document.querySelector(`tr[data-contributor="${user}"] td:nth-child(2)`);
|
|
if (row && !row.querySelector('.verified-badge')) {
|
|
const badgeClone = badge.cloneNode(true);
|
|
row.appendChild(badgeClone);
|
|
}
|
|
}
|
|
const items = [];
|
|
if (typeof data.followers === 'number' && typeof data.following === 'number') {
|
|
const fmt = n => n >= 1000 ? (n/1000).toFixed(1).replace(/\.0$/, '') + 'k' : n;
|
|
items.push(`${fmt(data.followers)} followers \u2022 ${fmt(data.following)} following`);
|
|
}
|
|
if (data.location) {
|
|
try {
|
|
if (!window._tzList) {
|
|
const l = await fetch('https://worldtimeapi.org/api/timezone');
|
|
window._tzList = l.ok ? await l.json() : null;
|
|
}
|
|
if (window._tzList) {
|
|
const zone = window._tzList.find(z => z.toLowerCase().includes(data.location.toLowerCase()));
|
|
if (zone) {
|
|
const tzRes = await fetch(`https://worldtimeapi.org/api/timezone/${encodeURIComponent(zone)}`);
|
|
if (tzRes.ok) {
|
|
const tz = await tzRes.json();
|
|
const dt = new Date(tz.datetime);
|
|
const time = dt.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
|
|
const offset = tz.utc_offset.replace(':00','');
|
|
items.push(`<svg class="meta-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg> ${data.location.split(',')[0]} \u2013 ${time} (GMT${offset})`);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
if (data.company) {
|
|
items.push(`<svg class="meta-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg> ${data.company}`);
|
|
}
|
|
const links = [];
|
|
if (data.blog) {
|
|
links.push(`<a href="${data.blog}" target="_blank" rel="noopener noreferrer" title="Website"><svg class="meta-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></a>`);
|
|
}
|
|
if (data.twitter_username) {
|
|
links.push(`<a href="https://twitter.com/${data.twitter_username}" target="_blank" rel="noopener noreferrer" title="Twitter"><svg class="meta-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"/></svg></a>`);
|
|
}
|
|
if (data.email) {
|
|
links.push(`<a href="mailto:${data.email}" title="Email"><svg class="meta-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg></a>`);
|
|
}
|
|
if (links.length) {
|
|
items.push(links.join(' '));
|
|
}
|
|
items.forEach(html => {
|
|
const li = document.createElement('li');
|
|
li.innerHTML = html;
|
|
metaList.appendChild(li);
|
|
});
|
|
})
|
|
.catch(() => {});
|
|
|
|
function createSection(title, count) {
|
|
const details = document.createElement('details');
|
|
details.className = 'contrib-section';
|
|
const summary = document.createElement('summary');
|
|
summary.textContent = `${title} (${count})`;
|
|
details.appendChild(summary);
|
|
return details;
|
|
}
|
|
|
|
const reposDetails = createSection('Repositories', info.repos.length);
|
|
const repoList = document.createElement('ul');
|
|
info.repos.forEach(r => {
|
|
const li = document.createElement('li');
|
|
const a = document.createElement('a');
|
|
a.href = `/?repos=${encodeURIComponent(r)}`;
|
|
a.target = '_blank';
|
|
a.rel = 'noopener noreferrer';
|
|
a.textContent = r;
|
|
li.appendChild(a);
|
|
repoList.appendChild(li);
|
|
});
|
|
reposDetails.appendChild(repoList);
|
|
content.appendChild(reposDetails);
|
|
|
|
const entriesDetails = createSection('Reviewer Entries', info.entries.length);
|
|
const entriesList = document.createElement('ul');
|
|
info.entries.forEach(e => {
|
|
const li = document.createElement('li');
|
|
const a = document.createElement('a');
|
|
a.href = `/reviewers/${e.slug}/`;
|
|
a.target = '_blank';
|
|
a.rel = 'noopener noreferrer';
|
|
a.textContent = e.title;
|
|
li.appendChild(a);
|
|
entriesList.appendChild(li);
|
|
});
|
|
entriesDetails.appendChild(entriesList);
|
|
content.appendChild(entriesDetails);
|
|
|
|
const commentsDetails = createSection('Comments', info.comments ? Object.keys(info.comments).length : 0);
|
|
if (info.comments) {
|
|
Object.keys(info.comments).forEach(slug => {
|
|
const group = document.createElement('details');
|
|
const sum = document.createElement('summary');
|
|
sum.textContent = `${slug} (${info.comments[slug].length})`;
|
|
group.appendChild(sum);
|
|
const thread = document.createElement('div');
|
|
thread.className = 'chat-thread';
|
|
info.comments[slug].forEach(text => {
|
|
const msg = document.createElement('div');
|
|
msg.className = 'chat-message';
|
|
const bubble = document.createElement('div');
|
|
bubble.className = 'chat-bubble';
|
|
const body = document.createElement('div');
|
|
body.className = 'chat-body';
|
|
body.innerHTML = escapeHtml(text).replace(/\n/g, '<br>');
|
|
bubble.appendChild(body);
|
|
msg.appendChild(bubble);
|
|
thread.appendChild(msg);
|
|
});
|
|
group.appendChild(thread);
|
|
commentsDetails.appendChild(group);
|
|
});
|
|
}
|
|
content.appendChild(commentsDetails);
|
|
|
|
drawer.classList.add('open');
|
|
history.replaceState(null, '', `#${user}`);
|
|
}
|
|
|
|
function closeDrawer() {
|
|
const drawer = document.getElementById('drawer');
|
|
drawer.classList.remove('open');
|
|
history.replaceState(null, '', location.pathname);
|
|
}
|
|
|
|
window.addEventListener('hashchange', () => {
|
|
const h = location.hash.slice(1);
|
|
if (!h) {
|
|
closeDrawer();
|
|
} else if (contributors[h]) {
|
|
openContributorDrawer(h);
|
|
}
|
|
});
|
|
</script>
|