UI for leaderboard

pull/7292/head
Kamran Ahmed 2 weeks ago
parent 55f0eff569
commit 9f2efc5872
  1. 12
      src/api/leaderboard.ts
  2. 156
      src/components/Leaderboard/LeaderboardPage.tsx

@ -1,7 +1,7 @@
import { type APIContext } from 'astro'; import { type APIContext } from 'astro';
import { api } from './api.ts'; import { api } from './api.ts';
export type LeadeboardUserDetails = { export type LeaderboardUserDetails = {
id: string; id: string;
name: string; name: string;
avatar?: string; avatar?: string;
@ -10,15 +10,15 @@ export type LeadeboardUserDetails = {
export type ListLeaderboardStatsResponse = { export type ListLeaderboardStatsResponse = {
streaks: { streaks: {
active: LeadeboardUserDetails[]; active: LeaderboardUserDetails[];
lifetime: LeadeboardUserDetails[]; lifetime: LeaderboardUserDetails[];
}; };
projectSubmissions: { projectSubmissions: {
currentMonth: LeadeboardUserDetails[]; currentMonth: LeaderboardUserDetails[];
lifetime: LeadeboardUserDetails[]; lifetime: LeaderboardUserDetails[];
}; };
githubContributors: { githubContributors: {
currentMonth: LeadeboardUserDetails[]; currentMonth: LeaderboardUserDetails[];
}; };
}; };

@ -1,10 +1,10 @@
import { type ReactNode, useState } from 'react'; import { type ReactNode, useState } from 'react';
import type { import type {
LeadeboardUserDetails, LeaderboardUserDetails,
ListLeaderboardStatsResponse, ListLeaderboardStatsResponse,
} from '../../api/leaderboard'; } from '../../api/leaderboard';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { FolderKanban, GitPullRequest, Trophy, Zap } from 'lucide-react'; import { FolderKanban, GitPullRequest, Users2, Zap } from 'lucide-react';
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';
@ -17,74 +17,60 @@ export function LeaderboardPage(props: LeaderboardPageProps) {
const { stats } = props; const { stats } = props;
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-white">
<div className="container py-5 sm:py-10"> <div className="container pb-5 sm:pb-8">
<div className="mb-8 text-center"> <h1 className="my-5 flex items-center text-lg font-medium text-black sm:mb-4 sm:mt-8">
<div className="flex flex-col items-start sm:items-center justify-center"> <Users2 className="mr-2 size-5 text-black" />
<img Leaderboard
src={'/images/gifs/star.gif'} </h1>
alt="party-popper"
className="mb-4 mt-0 sm:mt-3 h-14 w-14 hidden sm:block" <div className="grid gap-2 sm:gap-3 md:grid-cols-2">
/> <LeaderboardLane
<div className="mb-0 sm:mb-4 flex flex-col items-start sm:items-center justify-start sm:justify-center"> title="Longest Visit Streak"
<h2 className="mb-1.5 sm:mb-2 text-2xl font-semibold sm:text-4xl"> tabs={[
Leaderboard {
</h2> title: 'Active',
<p className="max-w-2xl text-left sm:text-center text-balance text-sm text-gray-500 sm:text-base"> users: stats.streaks?.active || [],
Top users based on their activity on roadmap.sh emptyIcon: <Zap className="size-16 text-gray-300" />,
</p> emptyText: 'No users with streaks yet',
</div> },
</div> {
title: 'Lifetime',
<div className="mt-5 sm:mt-8 grid gap-2 md:grid-cols-2"> users: stats.streaks?.lifetime || [],
<LeaderboardLane emptyIcon: <Zap className="size-16 text-gray-300" />,
title="Longest Visit Streak" emptyText: 'No users with streaks yet',
tabs={[ },
{ ]}
title: 'Active', />
users: stats.streaks?.active || [], <LeaderboardLane
emptyIcon: <Zap className="size-16 text-gray-300" />, title="Projects Completed"
emptyText: 'No users with streaks yet', tabs={[
}, {
{ title: 'This Month',
title: 'Lifetime', users: stats.projectSubmissions.currentMonth,
users: stats.streaks?.lifetime || [], emptyIcon: <FolderKanban className="size-16 text-gray-300" />,
emptyIcon: <Zap className="size-16 text-gray-300" />, emptyText: 'No projects submitted this month',
emptyText: 'No users with streaks yet', },
}, {
]} title: 'Lifetime',
/> users: stats.projectSubmissions.lifetime,
<LeaderboardLane emptyIcon: <FolderKanban className="size-16 text-gray-300" />,
title="Projects Completed" emptyText: 'No projects submitted yet',
tabs={[ },
{ ]}
title: 'This Month', />
users: stats.projectSubmissions.currentMonth, <LeaderboardLane
emptyIcon: <FolderKanban className="size-16 text-gray-300" />, title="Top Contributors"
emptyText: 'No projects submitted this month', subtitle="Past 2 weeks"
}, tabs={[
{ {
title: 'Lifetime', title: 'This Month',
users: stats.projectSubmissions.lifetime, users: stats.githubContributors.currentMonth,
emptyIcon: <FolderKanban className="size-16 text-gray-300" />, emptyIcon: <GitPullRequest className="size-16 text-gray-300" />,
emptyText: 'No projects submitted yet', emptyText: 'No contributors this month',
}, },
]} ]}
/> />
<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> </div>
@ -93,24 +79,30 @@ export function LeaderboardPage(props: LeaderboardPageProps) {
type LeaderboardLaneProps = { type LeaderboardLaneProps = {
title: string; title: string;
subtitle?: string;
tabs: { tabs: {
title: string; title: string;
users: LeadeboardUserDetails[]; users: LeaderboardUserDetails[];
emptyIcon?: ReactNode; emptyIcon?: ReactNode;
emptyText?: string; emptyText?: string;
}[]; }[];
}; };
function LeaderboardLane(props: LeaderboardLaneProps) { function LeaderboardLane(props: LeaderboardLaneProps) {
const { title, tabs } = props; const { title, subtitle, tabs } = props;
const [activeTab, setActiveTab] = useState(tabs[0]); const [activeTab, setActiveTab] = useState(tabs[0]);
const { users: usersToShow, emptyIcon, emptyText } = activeTab; const { users: usersToShow, emptyIcon, emptyText } = activeTab;
return ( return (
<div className="flex flex-col overflow-hidden rounded-xl border bg-white min-h-[450px] "> <div className="flex min-h-[450px] flex-col overflow-hidden rounded-xl border bg-white">
<div className="mb-3 flex items-center justify-between gap-2 px-3 py-3"> <div className="mb-3 flex items-center justify-between gap-2 bg-gray-100 px-3 py-3">
<h3 className="text-base font-medium">{title}</h3> <h3 className="text-base font-medium">
{title}{' '}
{subtitle && (
<span className="text-sm font-normal text-gray-400 ml-1">{subtitle}</span>
)}
</h3>
{tabs.length > 1 && ( {tabs.length > 1 && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -181,7 +173,7 @@ function LeaderboardLane(props: LeaderboardLaneProps) {
/> />
{isGitHubUser ? ( {isGitHubUser ? (
<a <a
href={`https://github.com/${user.name}`} href={`https://github.com/kamranahmedse/developer-roadmap/pulls?q=is%3Apr+is%3Aclosed+author%3A${user.name}`}
target="_blank" target="_blank"
className="truncate font-medium underline underline-offset-2" className="truncate font-medium underline underline-offset-2"
> >
@ -201,17 +193,7 @@ function LeaderboardLane(props: LeaderboardLaneProps) {
)} )}
</div> </div>
{isGitHubUser ? ( <span className="text-sm text-gray-500">{user.count}</span>
<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>
)}
</li> </li>
); );
})} })}

Loading…
Cancel
Save