Skip to content

Commit 7cd346e

Browse files
committed
add leaderboards
1 parent d8b3ca3 commit 7cd346e

File tree

3 files changed

+181
-1
lines changed

3 files changed

+181
-1
lines changed

apps/leaderboard-frontend/src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Link, Route, BrowserRouter as Router, Routes } from "react-router-dom"
22
import GamesPage from "./components/GamesPage"
33
import GuildsPage from "./components/GuildsPage"
44
import HomeLogoButton from "./components/HomeLogoButton"
5+
import Leaderboards from "./components/Leaderboards"
56
import ProfilePage from "./components/ProfilePage"
67
import WalletConnect from "./components/WalletConnect"
78
import "./index.css"
@@ -34,7 +35,7 @@ function App() {
3435
element={
3536
<div className="home-welcome-box">
3637
<h1 className="home-welcome-title">Welcome to HappyChain Leaderboard!</h1>
37-
{/* Future: leaderboard grid/list goes here */}
38+
<Leaderboards />
3839
</div>
3940
}
4041
/>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type React from "react"
2+
import { useEffect, useState } from "react"
3+
import "../index.css"
4+
import "./leaderboards.css"
5+
6+
export interface GlobalLeaderboardEntry {
7+
user_id: string
8+
username: string
9+
primary_wallet: string
10+
total_score: number
11+
}
12+
13+
export interface GuildLeaderboardEntry {
14+
guild_id: string
15+
guild_name: string
16+
icon_url: string | null
17+
total_score: number
18+
member_count: number
19+
}
20+
21+
const fetchLeaderboard = async <T,>(endpoint: string, limit = 10): Promise<T[]> => {
22+
const res = await fetch(`/api/leaderboards/${endpoint}?limit=${limit}`)
23+
if (!res.ok) throw new Error("Failed to fetch leaderboard")
24+
const json = await res.json()
25+
if (!json.ok) throw new Error(json.error || "Unknown error")
26+
return json.data as T[]
27+
}
28+
29+
const LeaderboardTable: React.FC<{
30+
title: string
31+
entries: GlobalLeaderboardEntry[] | GuildLeaderboardEntry[]
32+
type: "global" | "guilds"
33+
}> = ({ title, entries, type }) => (
34+
<div className="leaderboard-card">
35+
<h2 className="leaderboard-title">{title}</h2>
36+
<table className="leaderboard-table">
37+
<thead>
38+
<tr>
39+
<th>Rank</th>
40+
{type === "global" ? <th>Player</th> : <th>Guild</th>}
41+
<th>Score</th>
42+
{type === "guilds" && <th>Members</th>}
43+
</tr>
44+
</thead>
45+
<tbody>
46+
{entries.length === 0 ? (
47+
<tr>
48+
<td colSpan={type === "guilds" ? 4 : 3}>No data</td>
49+
</tr>
50+
) : (
51+
entries.map((entry, idx) => (
52+
<tr
53+
key={
54+
type === "global"
55+
? (entry as GlobalLeaderboardEntry).user_id
56+
: (entry as GuildLeaderboardEntry).guild_id
57+
}
58+
>
59+
<td>{idx + 1}</td>
60+
{type === "global" ? (
61+
<td>{(entry as GlobalLeaderboardEntry).username}</td>
62+
) : (
63+
<td>
64+
{(entry as GuildLeaderboardEntry).icon_url && (
65+
<img
66+
src={(entry as GuildLeaderboardEntry).icon_url!}
67+
alt=""
68+
style={{
69+
width: 24,
70+
height: 24,
71+
marginRight: 8,
72+
verticalAlign: "middle",
73+
borderRadius: "50%",
74+
}}
75+
/>
76+
)}
77+
{(entry as GuildLeaderboardEntry).guild_name}
78+
</td>
79+
)}
80+
<td>
81+
{type === "global"
82+
? (entry as GlobalLeaderboardEntry).total_score
83+
: (entry as GuildLeaderboardEntry).total_score}
84+
</td>
85+
{type === "guilds" && <td>{(entry as GuildLeaderboardEntry).member_count}</td>}
86+
</tr>
87+
))
88+
)}
89+
</tbody>
90+
</table>
91+
</div>
92+
)
93+
94+
const Leaderboards: React.FC = () => {
95+
const [global, setGlobal] = useState<GlobalLeaderboardEntry[]>([])
96+
const [guilds, setGuilds] = useState<GuildLeaderboardEntry[]>([])
97+
const [loading, setLoading] = useState(true)
98+
const [error, setError] = useState<string | null>(null)
99+
100+
useEffect(() => {
101+
setLoading(true)
102+
setError(null)
103+
Promise.all([
104+
fetchLeaderboard<GlobalLeaderboardEntry>("global", 10),
105+
fetchLeaderboard<GuildLeaderboardEntry>("guilds", 10),
106+
])
107+
.then(([globalData, guildData]) => {
108+
setGlobal(globalData)
109+
setGuilds(guildData)
110+
})
111+
.catch((e) => setError(e.message))
112+
.finally(() => setLoading(false))
113+
}, [])
114+
115+
if (loading) return <div className="leaderboards-loading">Loading leaderboards...</div>
116+
if (error) return <div className="leaderboards-error">{error}</div>
117+
118+
return (
119+
<div className="leaderboards-container">
120+
<LeaderboardTable title="Top Players" entries={global} type="global" />
121+
<LeaderboardTable title="Top Guilds" entries={guilds} type="guilds" />
122+
</div>
123+
)
124+
}
125+
126+
export default Leaderboards
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
.leaderboards-container {
2+
display: flex;
3+
gap: 2rem;
4+
justify-content: center;
5+
margin-top: 2rem;
6+
flex-wrap: wrap;
7+
}
8+
9+
.leaderboard-card {
10+
background: #fff;
11+
border-radius: 1rem;
12+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.07);
13+
padding: 1.5rem 2rem;
14+
min-width: 320px;
15+
max-width: 400px;
16+
width: 100%;
17+
margin-bottom: 2rem;
18+
}
19+
20+
.leaderboard-title {
21+
font-size: 1.3rem;
22+
font-weight: 600;
23+
margin-bottom: 1rem;
24+
text-align: center;
25+
}
26+
27+
.leaderboard-table {
28+
width: 100%;
29+
border-collapse: collapse;
30+
}
31+
32+
.leaderboard-table th,
33+
.leaderboard-table td {
34+
padding: 0.5rem 0.75rem;
35+
text-align: center;
36+
}
37+
38+
.leaderboard-table th {
39+
background: #f5f5f5;
40+
font-weight: 500;
41+
}
42+
43+
.leaderboard-table tr:nth-child(even) {
44+
background: #fafbfc;
45+
}
46+
47+
.leaderboards-loading,
48+
.leaderboards-error {
49+
text-align: center;
50+
margin: 2rem 0;
51+
color: #888;
52+
font-size: 1.1rem;
53+
}

0 commit comments

Comments
 (0)