diff --git a/src/api/leaderboard.ts b/src/api/leaderboard.ts new file mode 100644 index 000000000..9048cb876 --- /dev/null +++ b/src/api/leaderboard.ts @@ -0,0 +1,28 @@ +import { type APIContext } from 'astro'; +import { api } from './api.ts'; + +export type LeadeboardUserDetails = { + id: string; + name: string; + avatar?: string; + count: number; +}; + +export type ListLeaderboardStatsResponse = { + longestStreaks: LeadeboardUserDetails[]; + projectSubmissions: { + currentMonth: LeadeboardUserDetails[]; + lifetime: LeadeboardUserDetails[]; + }; +}; + +export function leaderboardApi(context: APIContext) { + return { + listLeaderboardStats: async function () { + return api(context).get( + `${import.meta.env.PUBLIC_API_URL}/v1-list-leaderboard-stats`, + {}, + ); + }, + }; +} diff --git a/src/components/Leaderboard/ErrorPage.tsx b/src/components/Leaderboard/ErrorPage.tsx new file mode 100644 index 000000000..edbc7915c --- /dev/null +++ b/src/components/Leaderboard/ErrorPage.tsx @@ -0,0 +1,26 @@ +import type { AppError } from '../../api/api'; +import { ErrorIcon } from '../ReactIcons/ErrorIcon'; + +type ErrorPageProps = { + error: AppError; +}; + +export function ErrorPage(props: ErrorPageProps) { + const { error } = props; + + return ( +
+
+
+ +

+ Oops! Something went wrong +

+

+ {error?.message || 'An error occurred while fetching'} +

+
+
+
+ ); +} diff --git a/src/components/Leaderboard/LeaderboardPage.tsx b/src/components/Leaderboard/LeaderboardPage.tsx new file mode 100644 index 000000000..7ef194209 --- /dev/null +++ b/src/components/Leaderboard/LeaderboardPage.tsx @@ -0,0 +1,118 @@ +import { useState } from 'react'; +import type { + LeadeboardUserDetails, + ListLeaderboardStatsResponse, +} from '../../api/leaderboard'; +import { cn } from '../../lib/classname'; + +type LeaderboardPageProps = { + stats: ListLeaderboardStatsResponse; +}; + +export function LeaderboardPage(props: LeaderboardPageProps) { + const { stats } = props; + + return ( +
+
+

+ Leaderboard +

+

+ Top users based on their activity on roadmap.sh +

+ +
+ + +
+
+
+ ); +} + +type LeaderboardLaneProps = { + title: string; + tabs: { + title: string; + users: LeadeboardUserDetails[]; + }[]; +}; + +function LeaderboardLane(props: LeaderboardLaneProps) { + const { title, tabs } = props; + + const [activeTab, setActiveTab] = useState(tabs[0]); + const usersToShow = activeTab.users; + + return ( +
+
+

{title}

+ + {tabs.length > 1 && ( +
+ {tabs.map((tab) => { + const isActive = tab === activeTab; + + return ( + + ); + })} +
+ )} +
+ + +
+ ); +} diff --git a/src/pages/leaderboard.astro b/src/pages/leaderboard.astro new file mode 100644 index 000000000..c7bf8f95f --- /dev/null +++ b/src/pages/leaderboard.astro @@ -0,0 +1,21 @@ +--- +import { LeaderboardPage } from '../components/Leaderboard/LeaderboardPage'; +import { ErrorPage } from '../components/Leaderboard/ErrorPage'; +import BaseLayout from '../layouts/BaseLayout.astro'; +import { leaderboardApi } from '../api/leaderboard'; + +export const prerender = false; + +const leaderboardClient = leaderboardApi(Astro); +const { response: leaderboardStats, error: leaderboardError } = + await leaderboardClient.listLeaderboardStats(); +--- + + + {leaderboardError && } + { + leaderboardStats && ( + + ) + } +