feat: create leaderboard

This commit is contained in:
thomas
2024-03-27 12:37:07 +01:00
parent cd8ee3cff4
commit 00ff67f93b
12 changed files with 1371 additions and 88 deletions

View File

@@ -1,102 +1,113 @@
import starlight from '@astrojs/starlight';
import { defineConfig } from 'astro/config';
import svelte from '@astrojs/svelte';
export const locales = {
root: {
label: 'English',
lang: 'en',
lang: 'en'
},
es: {
label: 'Español',
lang: 'es',
lang: 'es'
},
fr: {
label: 'Français',
lang: 'fr',
lang: 'fr'
},
pt: {
label: 'Português',
lang: 'pt',
lang: 'pt'
},
ru: {
label: 'Русский',
lang: 'ru',
},
lang: 'ru'
}
};
// https://astro.build/config
export default defineConfig({
integrations: [
starlight({
title: 'Angular Challenges',
logo: {
src: './public/angular-challenge.webp',
alt: 'angular challenges logo',
integrations: [starlight({
title: 'Angular Challenges',
logo: {
src: './public/angular-challenge.webp',
alt: 'angular challenges logo'
},
favicon: './angular-challenge.ico',
social: {
github: 'https://github.com/tomalaforge/angular-challenges',
linkedin: 'https://www.linkedin.com/in/thomas-laforge-2b05a945/',
twitter: 'https://twitter.com/laforge_toma'
},
customCss: ['./src/styles/custom-css.css'],
sidebar: [{
label: 'Guides',
autogenerate: {
directory: 'guides'
},
favicon: './angular-challenge.ico',
social: {
github: 'https://github.com/tomalaforge/angular-challenges',
linkedin: 'https://www.linkedin.com/in/thomas-laforge-2b05a945/',
twitter: 'https://twitter.com/laforge_toma',
translations: {
es: 'Guías',
fr: 'Guides',
pt: 'Guias',
ru: 'Руководство'
}
},
// {
// label: 'Leaderboard',
// autogenerate: {
// directory: 'leaderboard',
// collapsed: true
// },
// translations: {
// es: 'Leaderboard',
// fr: 'Leaderboard',
// pt: 'Leaderboard',
// ru: 'Leaderboard'
// }
// },
{
label: 'Challenges',
autogenerate: {
directory: 'challenges'
},
customCss: ['./src/styles/custom-css.css'],
sidebar: [
{
label: 'Guides',
autogenerate: { directory: 'guides' },
translations: {
es: 'Guías',
fr: 'Guides',
pt: 'Guias',
ru: 'Руководство',
},
},
{
label: 'Challenges',
autogenerate: { directory: 'challenges' },
translations: {
es: 'Desafíos',
fr: 'Challenges',
pt: 'Desafios',
ru: 'Задачи',
},
},
],
head: [
{
tag: 'script',
attrs: {
src: 'https://www.googletagmanager.com/gtag/js?id=G-6BXJ62W6G5',
async: true,
},
},
{
tag: 'script',
content: `
translations: {
es: 'Desafíos',
fr: 'Challenges',
pt: 'Desafios',
ru: 'Задачи'
}
}],
head: [{
tag: 'script',
attrs: {
src: 'https://www.googletagmanager.com/gtag/js?id=G-6BXJ62W6G5',
async: true
}
}, {
tag: 'script',
content: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-6BXJ62W6G5');
`,
},
{
tag: 'script',
attrs: {
src: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2438923752868254',
async: true,
}
}
],
components: {
MarkdownContent: './src/components/Content.astro',
TableOfContents: './src/components/TableOfContents.astro',
PageTitle: './src/components/PageTitle.astro',
MobileMenuFooter: './src/components/MobileMenuFooter.astro',
SiteTitle: './src/components/SiteTitle.astro',
},
defaultLocale: 'root',
locales,
}),
],
`
}, {
tag: 'script',
attrs: {
src: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2438923752868254',
async: true
}
}],
components: {
MarkdownContent: './src/components/Content.astro',
TableOfContents: './src/components/TableOfContents.astro',
PageTitle: './src/components/PageTitle.astro',
MobileMenuFooter: './src/components/MobileMenuFooter.astro',
SiteTitle: './src/components/SiteTitle.astro'
},
defaultLocale: 'root',
locales
}), svelte()]
});

925
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,11 @@
},
"dependencies": {
"@astrojs/starlight": "^0.15.1",
"@astrojs/svelte": "^5.2.0",
"@fontsource/ibm-plex-serif": "^5.0.8",
"astro": "^4.0.0",
"sharp": "^0.32.5"
"sharp": "^0.32.5",
"svelte": "^4.2.12",
"typescript": "^5.4.3"
}
}

View File

@@ -0,0 +1,93 @@
<script>
import { onMount } from 'svelte';
import UserBox from './UserBox.svelte';
let users = [];
let loading = true;
let error = null;
async function fetchGitHubUsers() {
try {
const prCounts = {};
let page = 1;
while (true) {
const response = await fetch(`https://api.github.com/search/issues?q=repo:tomalaforge/angular-challenges+is:pr+label:%22answer%22&per_page=200&page=${page}`);
if (!response.ok) {
throw new Error('API rate limit exceeded. Please try again in a few minutes.');
}
const { total_count, items } = await response.json();
if (!items || items.length === 0) {
break;
}
items.forEach(pr => {
const userLogin = pr.user.login;
if (prCounts[userLogin]) {
prCounts[userLogin].count++;
prCounts[userLogin].challengeNumber.push(pr.labels.filter(l => !isNaN(Number(l.name))).map(l => Number(l.name))?.[0]);
} else {
prCounts[userLogin] = {
avatar: pr.user.avatar_url,
count: 1,
challengeNumber: [pr.labels.filter(l => !isNaN(Number(l.name))).map(l => Number(l.name))?.[0]]
};
}
});
if(total_count < page * 100) {
break;
}
page++;
}
users = Object.entries(prCounts).map(([login, pr]) => ({
login,
avatar: pr.avatar,
count: pr.count,
challengeNumber: pr.challengeNumber.sort((a, b) => a - b)
})).filter((r) => r.login !== 'allcontributors[bot]').sort((a, b) => b.count - a.count);
} catch (e) {
error = e.message;
} finally {
loading = false;
}
}
onMount(() => {
fetchGitHubUsers();
});
</script>
{#if loading}
<p>Loading...</p>
{:else if error}
<p>Error: {error}</p>
{:else}
<div class="box not-content">
{#each users as { avatar, count, login,challengeNumber }, index}
<UserBox {avatar} {login} {index}>
{count} Answers
<div slot="addon" class="challenge-number">{challengeNumber.join(', ')}</div>
</UserBox>
{/each}
</div>
{/if}
<style>
.box {
display: grid;
justify-items: center;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.challenge-number {
font-size: 0.7rem;
color: var(--sl-color-gray-3);
}
</style>

View File

@@ -0,0 +1,72 @@
<script>
import { onMount } from 'svelte';
import UserBox from './UserBox.svelte';
let users = [];
let loading = true;
let error = null;
const createUser = (items) => {
const prCounts = {};
items.forEach((pr) => {
const userLogin = pr.user.login;
if (prCounts[userLogin]) {
prCounts[userLogin].count++;
} else {
prCounts[userLogin] = {
avatar: pr.user.avatar_url,
count: 1
};
}
});
return Object.entries(prCounts).map(([login, pr]) => ({
login,
avatar: pr.avatar,
count: pr.count
})).filter((r) => r.login !== 'allcontributors[bot]').sort((a, b) => b.count - a.count);
};
async function fetchGitHubUsers() {
try {
const response = await fetch(`https://api.github.com/search/issues?q=repo:tomalaforge/angular-challenges+is:pr+label:%22challenge-creation%22`);
if (!response.ok) {
throw new Error('API rate limit exceeded. Please try again in a few minutes.');
}
const { items } = await response.json();
users = createUser(items);
} catch (e) {
error = e.message;
} finally {
loading = false;
}
}
onMount(() => {
fetchGitHubUsers();
});
</script>
{#if loading}
<p>Loading...</p>
{:else if error}
<p>Error: {error}</p>
{:else}
<div class="box not-content">
{#each users as { avatar, count, login, challengeNumber }, index}
<UserBox {avatar} {login} {index}>
{count} Challenges Created
</UserBox>
{/each}
</div>
{/if}
<style>
.box {
display: grid;
justify-items: center;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
</style>

View File

@@ -0,0 +1,88 @@
<script>
import { onMount } from 'svelte';
import UserBox from './UserBox.svelte';
let users = [];
let loading = true;
let error = null;
async function fetchGitHubUsers() {
try {
const prCounts = {};
let page = 1;
while (true) {
const response = await fetch(`https://api.github.com/search/issues?q=repo:tomalaforge/angular-challenges+is:pr+no:label&per_page=100&page=${page}`);
if (!response.ok) {
throw new Error('API rate limit exceeded. Please try again in a few minutes.');
}
const { total_count, items } = await response.json();
if (!items || items.length === 0) {
break;
}
items.forEach(pr => {
const userLogin = pr.user.login;
if (prCounts[userLogin]) {
prCounts[userLogin].count++;
prCounts[userLogin].challengeNumber.push(pr.labels.filter(l => !isNaN(Number(l.name))).map(l => l.name).join(', '));
} else {
prCounts[userLogin] = {
avatar: pr.user.avatar_url,
count: 1,
challengeNumber: [pr.labels.filter(l => !isNaN(Number(l.name))).map(l => l.name).join(', ')]
};
}
});
if(total_count < page * 100) {
break;
}
page++;
}
users = Object.entries(prCounts).map(([login, pr]) => ({
login,
avatar: pr.avatar,
count: pr.count,
challengeNumber: pr.challengeNumber
})).filter((r) => r.login !== 'allcontributors[bot]').sort((a, b) => b.count - a.count);
} catch (e) {
error = e.message;
} finally {
loading = false;
}
}
onMount(() => {
fetchGitHubUsers();
});
</script>
{#if loading}
<p>Loading...</p>
{:else if error}
<p>Error: {error}</p>
{:else}
<div class="box not-content">
{#each users as { avatar, count, login }, index}
<UserBox {avatar} {login} {index}>
{count} PRs merged
</UserBox>
{/each}
</div>
{/if}
<style>
.box {
display: grid;
justify-items: center;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
</style>

View File

@@ -0,0 +1,77 @@
<script>
export let avatar;
export let login;
export let index;
</script>
<div class="user-box">
<div class="user-info">
<img src={avatar} alt="" width="40" height="40" class="avatar" />
<div class="name-box">
<div class="user-name">{login}</div>
<div class="count"><slot /></div>
<slot name="addon" />
</div>
</div>
<div class="position">
#{index + 1}
</div>
</div>
<style>
.position {
font-size: 20px;
line-height: 20px;
height: 100%;
padding: 0.5rem;
color: var(--sl-color-gray-6);
background-color: var(--sl-color-gray-3);
}
.user-box {
display: flex;
gap: 5px;
border: 1px solid var(--sl-color-gray-3);
border-radius: 5px;
align-items: center;
width: 100%;
max-width: 300px;
justify-content: space-between;
}
.user-info {
display: flex;
padding: 1rem 0 1rem 1rem;
gap: 1rem;
justify-content: flex-start;
width: 100%;
align-items: center;
}
.avatar {
border-radius: 50%;
width: 40px;
height: 40px
}
.name-box {
display: flex;
flex-direction: column;
}
.user-name {
font-size: 24px;
color: red;
line-height: 24px;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.count {
font-size: var(--sl-text-xs);
color: var(--sl-color-gray-3);
}
</style>

View File

@@ -0,0 +1,11 @@
---
title: Challenges answered
description: leaderboard showing the number of challenges answered.
noCommentSection: true
prev: false
next: false
---
import LeaderboardAnswer from '../../../components/leaderboard/LeaderboardAnswer.svelte';
<LeaderboardAnswer client:load />

View File

@@ -0,0 +1,11 @@
---
title: Number of Challenges Created
description: leaderboard showing the number of challenges created.
noCommentSection: true
prev: false
next: false
---
import LeaderboardChallenge from '../../../components/leaderboard/LeaderboardChallenge.svelte';
<LeaderboardChallenge client:load />

View File

@@ -0,0 +1,11 @@
---
title: Number of contributions
description: leaderboard showing the number of contributions.
noCommentSection: true
prev: false
next: false
---
import LeaderboardCommit from '../../../components/leaderboard/LeaderboardCommit.svelte';
<LeaderboardCommit client:load />

5
docs/svelte.config.js Normal file
View File

@@ -0,0 +1,5 @@
import { vitePreprocess } from '@astrojs/svelte';
export default {
preprocess: vitePreprocess(),
};

View File

@@ -1,3 +1,7 @@
{
"extends": "astro/tsconfigs/strict"
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}