mirror of
https://github.com/Raghu-Ch/angular-challenges.git
synced 2026-02-10 04:43:03 -05:00
feat: create leaderboard
This commit is contained in:
@@ -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
925
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
93
docs/src/components/leaderboard/LeaderboardAnswer.svelte
Normal file
93
docs/src/components/leaderboard/LeaderboardAnswer.svelte
Normal 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>
|
||||
72
docs/src/components/leaderboard/LeaderboardChallenge.svelte
Normal file
72
docs/src/components/leaderboard/LeaderboardChallenge.svelte
Normal 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>
|
||||
88
docs/src/components/leaderboard/LeaderboardCommit.svelte
Normal file
88
docs/src/components/leaderboard/LeaderboardCommit.svelte
Normal 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>
|
||||
77
docs/src/components/leaderboard/UserBox.svelte
Normal file
77
docs/src/components/leaderboard/UserBox.svelte
Normal 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>
|
||||
11
docs/src/content/docs/leaderboard/answers.mdx
Normal file
11
docs/src/content/docs/leaderboard/answers.mdx
Normal 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 />
|
||||
11
docs/src/content/docs/leaderboard/challenges.mdx
Normal file
11
docs/src/content/docs/leaderboard/challenges.mdx
Normal 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 />
|
||||
11
docs/src/content/docs/leaderboard/commit.mdx
Normal file
11
docs/src/content/docs/leaderboard/commit.mdx
Normal 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
5
docs/svelte.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { vitePreprocess } from '@astrojs/svelte';
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
};
|
||||
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user