Skip to content

Commit 85900d8

Browse files
committed
Add initial leaderboard
1 parent 6aaa668 commit 85900d8

5 files changed

Lines changed: 288 additions & 20 deletions

File tree

assets/app.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { User } from './user.js';
55
import { Stats } from './stats.js';
66
import { Robots } from './robots.js';
77
import { Changelog } from './changelog.js';
8+
import { Leaderboard } from './leaderboard.js';
89

910
const App = (() => {
1011
"use strict";
@@ -37,6 +38,24 @@ const App = (() => {
3738
};
3839
}
3940

41+
// Check for leaderboard page patterns: /leaderboard or /leaderboard/gh/org
42+
if (path === '/leaderboard') {
43+
return {
44+
org: null,
45+
username: state.currentUser?.login,
46+
isLeaderboard: true,
47+
};
48+
}
49+
const leaderboardMatch = path.match(/^\/leaderboard\/gh\/([^\/]+)$/);
50+
if (leaderboardMatch) {
51+
const [, org] = leaderboardMatch;
52+
return {
53+
org: org === '*' ? null : org,
54+
username: state.currentUser?.login,
55+
isLeaderboard: true,
56+
};
57+
}
58+
4059
// Check for robot page patterns: /robots or /robots/gh/org
4160
if (path === '/robots') {
4261
return {
@@ -128,6 +147,7 @@ const App = (() => {
128147
const settingsLink = $("settingsLink");
129148
const notificationsLink = $("notificationsLink");
130149
const changelogLink = $("changelogLink");
150+
const leaderboardLink = $("leaderboardLink");
131151
const orgSelect = $("orgSelect");
132152
const urlContext = parseURL();
133153
const { username, org: urlOrg } = urlContext || {};
@@ -166,6 +186,10 @@ const App = (() => {
166186
changelogLink.href = '/changelog/gh/*';
167187
}
168188
}
189+
190+
if (leaderboardLink) {
191+
leaderboardLink.href = selectedOrg ? `/leaderboard/gh/${selectedOrg}` : '/leaderboard';
192+
}
169193
};
170194

171195
const setupHamburgerMenu = () => {
@@ -261,6 +285,19 @@ const App = (() => {
261285
});
262286
}
263287

288+
const leaderboardLink = $("leaderboardLink");
289+
if (leaderboardLink) {
290+
if (path.startsWith('/leaderboard')) {
291+
leaderboardLink.classList.add("active");
292+
}
293+
294+
leaderboardLink.addEventListener("click", (e) => {
295+
e.preventDefault();
296+
closeMenu();
297+
window.location.href = leaderboardLink.href;
298+
});
299+
}
300+
264301
const settingsLink = $("settingsLink");
265302
if (settingsLink) {
266303
if (path.startsWith('/robots')) {
@@ -316,6 +353,8 @@ const App = (() => {
316353
} else {
317354
window.location.href = '/changelog/gh/*';
318355
}
356+
} else if (path.startsWith('/leaderboard')) {
357+
window.location.href = selectedOrg ? `/leaderboard/gh/${selectedOrg}` : '/leaderboard';
319358
} else {
320359
// For root or unknown pages, go to user dashboard
321360
const currentUser = state.currentUser || state.viewingUser;
@@ -617,6 +656,17 @@ const App = (() => {
617656
await Changelog.showChangelogPage(state, githubAPI, parseURL);
618657
return;
619658
}
659+
660+
// Handle leaderboard page routing
661+
if (urlContext && urlContext.isLeaderboard) {
662+
updateSearchInputVisibility();
663+
await Leaderboard.showLeaderboardPage(state, githubAPI, loadCurrentUser,
664+
() => User.updateUserDisplay(state, initiateLogin),
665+
setupHamburgerMenu,
666+
() => User.updateOrgFilter(state, parseURL, githubAPI),
667+
handleOrgChange, handleSearch, parseURL, User.loadUserOrganizations);
668+
return;
669+
}
620670
// Handle notifications page routing
621671
const path = window.location.pathname;
622672
if (path === '/notifications' || path.match(/^\/notifications\/gh\/[^\/]+$/)) {

assets/changelog.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,12 @@ export const Changelog = (() => {
174174
// Check if a user is a bot
175175
const isBot = (user) => {
176176
if (!user) return false;
177+
const login = user.login.toLowerCase();
177178
return user.type === 'Bot' ||
178-
user.login.includes('[bot]') ||
179-
user.login.endsWith('-bot') ||
180-
user.login.includes('dependabot');
179+
login.endsWith('[bot]') ||
180+
login.endsWith('-bot') ||
181+
login.endsWith('-robot') ||
182+
login.includes('dependabot');
181183
};
182184

183185
const showChangelogPage = async (state, githubAPI, parseURL) => {
@@ -194,6 +196,8 @@ export const Changelog = (() => {
194196
const changelogSummary = $('changelogSummary');
195197
const includeBots = $('includeBots');
196198
const clearCacheLink = $('clearChangelogCache');
199+
const changelogOrgLink = $('changelogOrgLink');
200+
const changelogOrgLinkAnchor = $('changelogOrgLinkAnchor');
197201

198202
if (!changelogContent) return;
199203

@@ -232,6 +236,14 @@ export const Changelog = (() => {
232236
hide(changelogBotToggle);
233237
}
234238

239+
// Show org link only when viewing a specific user in an org
240+
if (username && org) {
241+
show(changelogOrgLink);
242+
changelogOrgLinkAnchor.href = `/changelog/gh/${org}`;
243+
} else {
244+
hide(changelogOrgLink);
245+
}
246+
235247
if (username && org) {
236248
// Specific user in specific org
237249
titleText = `${username} in ${org}`;
@@ -249,7 +261,9 @@ export const Changelog = (() => {
249261
searchQuery = `type:pr is:merged author:${username} merged:>=${oneWeekAgoISO}`;
250262
}
251263

252-
if (org) {
264+
if (username && org) {
265+
changelogTitleText.textContent = `${username}'s changes to ${org}`;
266+
} else if (org) {
253267
changelogTitleText.textContent = `What's New in ${org}`;
254268
} else if (username) {
255269
changelogTitleText.textContent = `What's New from ${username}`;

assets/stats.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,5 +1156,6 @@ export const Stats = (() => {
11561156
showStatsPage,
11571157
loadStatsData,
11581158
clearStatsCache,
1159+
githubSearchAll,
11591160
};
11601161
})();

assets/styles.css

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2943,6 +2943,20 @@ a {
29432943
margin: 0 0 1.5rem 0;
29442944
}
29452945

2946+
.changelog-org-link {
2947+
margin: -0.5rem 0 1.5rem 0;
2948+
}
2949+
2950+
.changelog-org-link a {
2951+
color: var(--color-blue);
2952+
text-decoration: none;
2953+
font-size: 0.9375rem;
2954+
}
2955+
2956+
.changelog-org-link a:hover {
2957+
text-decoration: underline;
2958+
}
2959+
29462960
/* Executive Summary */
29472961
.changelog-summary {
29482962
max-width: 980px;
@@ -3331,3 +3345,163 @@ a {
33313345
font-size: 0.875rem;
33323346
}
33333347
}
3348+
3349+
/* Leaderboard Styles */
3350+
.leaderboard-content {
3351+
padding: var(--space-8) 0;
3352+
}
3353+
3354+
.leaderboard-loading {
3355+
display: flex;
3356+
flex-direction: column;
3357+
align-items: center;
3358+
gap: 1.5rem;
3359+
padding: 6rem 0;
3360+
color: var(--color-text-secondary);
3361+
}
3362+
3363+
.leaderboard-loading .spinner {
3364+
width: 48px;
3365+
height: 48px;
3366+
border: 3px solid rgba(0, 0, 0, 0.1);
3367+
border-top-color: var(--color-primary);
3368+
border-radius: 50%;
3369+
animation: spin 1s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
3370+
}
3371+
3372+
.leaderboard-data {
3373+
max-width: 980px;
3374+
margin: 0 auto;
3375+
padding: 0 1rem;
3376+
}
3377+
3378+
.leaderboard-container {
3379+
background: var(--color-bg-elevated);
3380+
border-radius: 12px;
3381+
padding: 2rem;
3382+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05),
3383+
0 2px 8px rgba(0, 0, 0, 0.04);
3384+
}
3385+
3386+
.leaderboard-header {
3387+
text-align: center;
3388+
margin-bottom: 2rem;
3389+
}
3390+
3391+
.leaderboard-title {
3392+
font-size: 1.75rem;
3393+
font-weight: 700;
3394+
margin: 0 0 0.5rem 0;
3395+
color: var(--color-text);
3396+
}
3397+
3398+
.leaderboard-subtitle {
3399+
font-size: 1rem;
3400+
color: var(--color-text-secondary);
3401+
margin: 0;
3402+
}
3403+
3404+
.leaderboard-chart {
3405+
display: flex;
3406+
flex-direction: column;
3407+
gap: 0.75rem;
3408+
}
3409+
3410+
.leaderboard-row {
3411+
display: flex;
3412+
align-items: center;
3413+
gap: 1rem;
3414+
padding: 0.75rem;
3415+
border-radius: 8px;
3416+
transition: background 0.15s ease;
3417+
}
3418+
3419+
.leaderboard-row:hover {
3420+
background: rgba(0, 0, 0, 0.03);
3421+
}
3422+
3423+
@media (prefers-color-scheme: dark) {
3424+
.leaderboard-row:hover {
3425+
background: rgba(255, 255, 255, 0.05);
3426+
}
3427+
}
3428+
3429+
.leaderboard-rank {
3430+
font-size: 1.125rem;
3431+
font-weight: 600;
3432+
color: var(--color-text-secondary);
3433+
width: 2rem;
3434+
text-align: center;
3435+
}
3436+
3437+
.leaderboard-avatar {
3438+
width: 40px;
3439+
height: 40px;
3440+
border-radius: 50%;
3441+
border: 2px solid var(--color-bg);
3442+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
3443+
}
3444+
3445+
.leaderboard-name {
3446+
font-weight: 500;
3447+
color: var(--color-text);
3448+
text-decoration: none;
3449+
min-width: 150px;
3450+
}
3451+
3452+
.leaderboard-name:hover {
3453+
color: var(--color-primary);
3454+
}
3455+
3456+
.leaderboard-bar-container {
3457+
flex: 1;
3458+
background: rgba(0, 0, 0, 0.05);
3459+
border-radius: 4px;
3460+
height: 32px;
3461+
position: relative;
3462+
overflow: hidden;
3463+
}
3464+
3465+
@media (prefers-color-scheme: dark) {
3466+
.leaderboard-bar-container {
3467+
background: rgba(255, 255, 255, 0.1);
3468+
}
3469+
}
3470+
3471+
.leaderboard-bar {
3472+
height: 100%;
3473+
background: linear-gradient(135deg, #007AFF, #0051D5);
3474+
border-radius: 4px;
3475+
display: flex;
3476+
align-items: center;
3477+
justify-content: flex-end;
3478+
padding-right: 0.75rem;
3479+
transition: width 0.5s ease-out;
3480+
}
3481+
3482+
.leaderboard-count {
3483+
font-size: 0.875rem;
3484+
font-weight: 600;
3485+
color: white;
3486+
}
3487+
3488+
@media (max-width: 768px) {
3489+
.leaderboard-container {
3490+
padding: 1.5rem;
3491+
}
3492+
3493+
.leaderboard-row {
3494+
flex-wrap: wrap;
3495+
gap: 0.5rem;
3496+
}
3497+
3498+
.leaderboard-name {
3499+
min-width: auto;
3500+
flex: 1;
3501+
}
3502+
3503+
.leaderboard-bar-container {
3504+
width: 100%;
3505+
order: 1;
3506+
}
3507+
}

0 commit comments

Comments
 (0)