feat: add github contributors leaderboard (#7277)

* feat: add github contributors leaderboard

* Improve UI for leaderboard

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
pull/7292/head
Arik Chakma 2 weeks ago committed by GitHub
parent 47936801fd
commit 55f0eff569
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. BIN
      public/images/gifs/party-popper.gif
  2. 0
      public/images/gifs/rocket.gif
  3. BIN
      public/images/gifs/star.gif
  4. BIN
      public/images/gifs/starstruck.gif
  5. BIN
      public/images/gifs/sunglasses.gif
  6. 3
      src/api/leaderboard.ts
  7. 2
      src/components/Changelog/ChangelogLaunch.astro
  8. 2
      src/components/ChangelogBanner.astro
  9. 78
      src/components/Leaderboard/LeaderboardPage.tsx

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 KiB

@ -17,6 +17,9 @@ export type ListLeaderboardStatsResponse = {
currentMonth: LeadeboardUserDetails[]; currentMonth: LeadeboardUserDetails[];
lifetime: LeadeboardUserDetails[]; lifetime: LeadeboardUserDetails[];
}; };
githubContributors: {
currentMonth: LeadeboardUserDetails[];
};
}; };
export function leaderboardApi(context: APIContext) { export function leaderboardApi(context: APIContext) {

@ -23,7 +23,7 @@ const formattedDate = DateTime.fromISO('2024-09-13').toFormat('dd LLL, yyyy');
<div <div
class='flex flex-col items-center justify-center gap-2 sm:gap-2 rounded-xl border bg-white px-8 py-12 text-center' class='flex flex-col items-center justify-center gap-2 sm:gap-2 rounded-xl border bg-white px-8 py-12 text-center'
> >
<img src='/images/rocket.gif' class='w-[70px] mb-4' /> <img src='/images/gifs/rocket.gif' class='w-[70px] mb-4' />
<h2 class='text-balance text-xl font-medium'>Changelog page is launched</h2> <h2 class='text-balance text-xl font-medium'>Changelog page is launched</h2>
<p class='font-normal text-balance text-gray-400 text-sm sm:text-base'> <p class='font-normal text-balance text-gray-400 text-sm sm:text-base'>
We will be sharing a selected list of updates, improvements, and fixes made to We will be sharing a selected list of updates, improvements, and fixes made to

@ -10,7 +10,7 @@ const top10Changelogs = allChangelogs.slice(0, 10);
<div class='container !max-w-[650px]'> <div class='container !max-w-[650px]'>
<p class='text-2xl font-bold sm:text-5xl'> <p class='text-2xl font-bold sm:text-5xl'>
<img <img
src='/images/rocket.gif' src='/images/gifs/rocket.gif'
alt='Rocket' alt='Rocket'
class='mr-2 hidden sm:inline h-12 w-12' class='mr-2 hidden sm:inline h-12 w-12'
/> />

@ -1,11 +1,10 @@
import { useState, type ReactNode } from 'react'; import { type ReactNode, useState } from 'react';
import type { import type {
LeadeboardUserDetails, LeadeboardUserDetails,
ListLeaderboardStatsResponse, ListLeaderboardStatsResponse,
} from '../../api/leaderboard'; } from '../../api/leaderboard';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { FolderKanban, Zap, Trophy } from 'lucide-react'; import { FolderKanban, GitPullRequest, Trophy, Zap } from 'lucide-react';
import { RankBadgeIcon } from '../ReactIcons/RankBadgeIcon';
import { TrophyEmoji } from '../ReactIcons/TrophyEmoji'; import { TrophyEmoji } from '../ReactIcons/TrophyEmoji';
import { SecondPlaceMedalEmoji } from '../ReactIcons/SecondPlaceMedalEmoji'; import { SecondPlaceMedalEmoji } from '../ReactIcons/SecondPlaceMedalEmoji';
import { ThirdPlaceMedalEmoji } from '../ReactIcons/ThirdPlaceMedalEmoji'; import { ThirdPlaceMedalEmoji } from '../ReactIcons/ThirdPlaceMedalEmoji';
@ -19,17 +18,25 @@ export function LeaderboardPage(props: LeaderboardPageProps) {
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<div className="container py-10"> <div className="container py-5 sm:py-10">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<div className="mb-2 flex items-center justify-center gap-3"> <div className="flex flex-col items-start sm:items-center justify-center">
<Trophy className="size-8 text-yellow-500" /> <img
<h2 className="text-2xl font-bold sm:text-3xl">Leaderboard</h2> src={'/images/gifs/star.gif'}
</div> alt="party-popper"
<p className="mx-auto max-w-2xl text-balance text-sm text-gray-500 sm:text-base"> className="mb-4 mt-0 sm:mt-3 h-14 w-14 hidden sm:block"
/>
<div className="mb-0 sm:mb-4 flex flex-col items-start sm:items-center justify-start sm:justify-center">
<h2 className="mb-1.5 sm:mb-2 text-2xl font-semibold sm:text-4xl">
Leaderboard
</h2>
<p className="max-w-2xl text-left sm:text-center text-balance text-sm text-gray-500 sm:text-base">
Top users based on their activity on roadmap.sh Top users based on their activity on roadmap.sh
</p> </p>
</div>
</div>
<div className="mt-8 grid gap-2 md:grid-cols-2"> <div className="mt-5 sm:mt-8 grid gap-2 md:grid-cols-2">
<LeaderboardLane <LeaderboardLane
title="Longest Visit Streak" title="Longest Visit Streak"
tabs={[ tabs={[
@ -64,6 +71,19 @@ export function LeaderboardPage(props: LeaderboardPageProps) {
}, },
]} ]}
/> />
<LeaderboardLane
title="Top Contributors"
tabs={[
{
title: 'This Month',
users: stats.githubContributors.currentMonth,
emptyIcon: (
<GitPullRequest className="size-16 text-gray-300" />
),
emptyText: 'No contributors this month',
},
]}
/>
</div> </div>
</div> </div>
</div> </div>
@ -88,8 +108,8 @@ function LeaderboardLane(props: LeaderboardLaneProps) {
const { users: usersToShow, emptyIcon, emptyText } = activeTab; const { users: usersToShow, emptyIcon, emptyText } = activeTab;
return ( return (
<div className="overflow-hidden rounded-md border bg-white shadow-sm"> <div className="flex flex-col overflow-hidden rounded-xl border bg-white min-h-[450px] ">
<div className="flex items-center justify-between gap-2 bg-gray-100 px-3 py-3 mb-3"> <div className="mb-3 flex items-center justify-between gap-2 px-3 py-3">
<h3 className="text-base font-medium">{title}</h3> <h3 className="text-base font-medium">{title}</h3>
{tabs.length > 1 && ( {tabs.length > 1 && (
@ -118,7 +138,7 @@ function LeaderboardLane(props: LeaderboardLaneProps) {
</div> </div>
{usersToShow.length === 0 && emptyText && ( {usersToShow.length === 0 && emptyText && (
<div className="flex flex-col items-center justify-center p-8"> <div className="flex flex-grow flex-col items-center justify-center p-8">
{emptyIcon} {emptyIcon}
<p className="mt-4 text-sm text-gray-500">{emptyText}</p> <p className="mt-4 text-sm text-gray-500">{emptyText}</p>
</div> </div>
@ -128,19 +148,23 @@ function LeaderboardLane(props: LeaderboardLaneProps) {
<ul className="divide-y divide-gray-100 pb-4"> <ul className="divide-y divide-gray-100 pb-4">
{usersToShow.map((user, counter) => { {usersToShow.map((user, counter) => {
const avatar = user?.avatar const avatar = user?.avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${user.avatar}` ? user?.avatar?.startsWith('http')
? user?.avatar
: `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${user.avatar}`
: '/images/default-avatar.png'; : '/images/default-avatar.png';
const rank = counter + 1; const rank = counter + 1;
const isGitHubUser = avatar?.indexOf('github') > -1;
return ( return (
<li <li
key={user.id} key={user.id}
className="flex items-center justify-between gap-1 pl-2 pr-5 py-2.5 hover:bg-gray-50" className="flex items-center justify-between gap-1 py-2.5 pl-2 pr-5"
> >
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<span <span
className={cn( className={cn(
'relative text-xs mr-1 flex size-6 shrink-0 items-center justify-center rounded-full tabular-nums', 'relative mr-1 flex size-6 shrink-0 items-center justify-center rounded-full text-xs tabular-nums',
{ {
'text-black': rank <= 3, 'text-black': rank <= 3,
'text-gray-400': rank > 3, 'text-gray-400': rank > 3,
@ -153,9 +177,19 @@ function LeaderboardLane(props: LeaderboardLaneProps) {
<img <img
src={avatar} src={avatar}
alt={user.name} alt={user.name}
className="size-7 shrink-0 rounded-full" className="mr-1 size-7 shrink-0 rounded-full"
/> />
{isGitHubUser ? (
<a
href={`https://github.com/${user.name}`}
target="_blank"
className="truncate font-medium underline underline-offset-2"
>
{user.name}
</a>
) : (
<span className="truncate">{user.name}</span> <span className="truncate">{user.name}</span>
)}
{rank === 1 ? ( {rank === 1 ? (
<TrophyEmoji className="size-5" /> <TrophyEmoji className="size-5" />
) : rank === 2 ? ( ) : rank === 2 ? (
@ -167,7 +201,17 @@ function LeaderboardLane(props: LeaderboardLaneProps) {
)} )}
</div> </div>
{isGitHubUser ? (
<a
target={'_blank'}
href={`https://github.com/kamranahmedse/developer-roadmap/pulls/${user.name}`}
className="text-sm text-gray-500"
>
{user.count}
</a>
) : (
<span className="text-sm text-gray-500">{user.count}</span> <span className="text-sm text-gray-500">{user.count}</span>
)}
</li> </li>
); );
})} })}

Loading…
Cancel
Save