Skip to content

Commit d090977

Browse files
committed
Add KillsLeaderboardTable component
1 parent 23cdaac commit d090977

File tree

12 files changed

+207
-49
lines changed

12 files changed

+207
-49
lines changed

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"cSpell.words": [
3+
"Leaderboard"
4+
]
5+
}

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@
1414
"dependencies": {
1515
"@tanstack/react-query": "^5.55.4",
1616
"classnames": "^2.5.1",
17+
"lodash": "^4.17.21",
1718
"react": "^18.3.1",
18-
"react-dom": "^18.3.1"
19+
"react-dom": "^18.3.1",
20+
"use-custom-compare": "^1.4.0",
21+
"usehooks-ts": "^3.1.0"
1922
},
2023
"devDependencies": {
2124
"@eslint/js": "^9.9.0",
25+
"@types/lodash": "^4.17.7",
2226
"@types/react": "^18.3.3",
2327
"@types/react-dom": "^18.3.0",
2428
"@vitejs/plugin-react": "^4.3.1",

src/components/GlobalLeaderBoard/GlobalLeaderBoard.tsx

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,28 @@
1-
import Table from "../Table";
1+
import { useAppContext } from "../../context/useAppContext";
2+
import LeaderboardTable from "../LeaderboardTable";
23
import Tabs from "../Tabs";
34

45
const GlobalLeaderBoard = () => {
6+
const { globalQuery } = useAppContext();
7+
const kills = globalQuery?.data?.kills_leaderboard;
8+
const items = globalQuery?.data?.items_leaderboard;
9+
const secrets = globalQuery?.data?.secrets_leaderboard;
10+
511
const tabs = [
612
{
713
id: 0,
814
title: "Kills",
9-
content: (
10-
<Table
11-
columns={[{ name: "player" }, { name: "score" }]}
12-
data={[
13-
{ player: "Player 1", score: "1000" },
14-
{ player: "Player 2", score: "900" },
15-
{ player: "Player 3", score: "800" },
16-
{ player: "Player 4", score: "700" },
17-
{ player: "Player 5", score: "600" },
18-
{ player: "Player 6", score: "500" },
19-
{ player: "Player 7", score: "400" },
20-
{ player: "Player 8", score: "300" },
21-
{ player: "Player 9", score: "200" },
22-
{ player: "Player 10", score: "100" },
23-
]}
24-
/>
25-
),
15+
content: kills && <LeaderboardTable data={kills} />,
2616
},
2717
{
2818
id: 1,
2919
title: "Items",
30-
content: (
31-
<div className="p-6 text-white bg-stone-900">
32-
Leaderboard content for this week
33-
</div>
34-
),
20+
content: items && <LeaderboardTable data={items} />,
3521
},
3622
{
3723
id: 2,
3824
title: "Secret",
39-
content: (
40-
<div className="p-6 text-white bg-stone-900">
41-
Leaderboard content for today
42-
</div>
43-
),
25+
content: secrets && <LeaderboardTable data={secrets} />,
4426
},
4527
];
4628

src/components/Leaderboard/Leaderboard.tsx

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1+
import { useAppContext } from "../../context/useAppContext";
2+
import LeaderboardTable from "../LeaderboardTable";
13
import Table from "../Table";
24
import Tabs from "../Tabs";
35

46
const Leaderboard = () => {
7+
const { globalQuery } = useAppContext();
8+
const kills = globalQuery?.data?.kills_leaderboard;
9+
const items = globalQuery?.data?.items_leaderboard;
10+
const secrets = globalQuery?.data?.secrets_leaderboard;
11+
12+
const allTimeData = [
13+
{ title: "Kills", data: kills },
14+
{ title: "Items", data: items },
15+
{ title: "Secrets", data: secrets },
16+
];
17+
518
const tabs = [
619
{
720
id: 0,
@@ -36,24 +49,10 @@ const Leaderboard = () => {
3649
title: "All Time",
3750
content: (
3851
<div className="grid grid-cols-3 gap-6 pt-6">
39-
{[1, 2, 3].map((i) => (
40-
<div key={i}>
41-
<h2 className="text-2xl text-center mb-6">Kills</h2>
42-
<Table
43-
columns={[{ name: "player" }, { name: "score" }]}
44-
data={[
45-
{ player: "Player 1", score: "1000" },
46-
{ player: "Player 2", score: "900" },
47-
{ player: "Player 3", score: "800" },
48-
{ player: "Player 4", score: "700" },
49-
{ player: "Player 5", score: "600" },
50-
{ player: "Player 6", score: "500" },
51-
{ player: "Player 7", score: "400" },
52-
{ player: "Player 8", score: "300" },
53-
{ player: "Player 9", score: "200" },
54-
{ player: "Player 10", score: "100" },
55-
]}
56-
/>
52+
{allTimeData.map(({ title, data }) => (
53+
<div key={title}>
54+
<h2 className="text-2xl text-center mb-6">{title}</h2>
55+
{data && <LeaderboardTable data={data} />}
5756
</div>
5857
))}
5958
</div>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { FC, useCallback, useEffect, useState } from "react";
2+
import Table, { TableData } from "../Table/Table";
3+
import usePlayersHandle from "../../hooks/usePlayersHandle";
4+
import { getMap, getTableData } from "../../utils/leaderboard";
5+
import { LeaderboardEntry } from "../../types";
6+
7+
interface LeaderboardTableProps {
8+
data: LeaderboardEntry[];
9+
}
10+
11+
const LeaderboardTable: FC<LeaderboardTableProps> = ({ data }) => {
12+
const [tableData, setTableData] = useState<TableData[]>([]);
13+
const { fetchPlayerHandles } = usePlayersHandle();
14+
15+
const updateTableData = useCallback(async () => {
16+
try {
17+
const map = getMap(data);
18+
19+
if (!map) return;
20+
21+
const players = Object.keys(map);
22+
const handles = await fetchPlayerHandles(players);
23+
24+
setTableData(getTableData(map, handles));
25+
} catch (error) {
26+
console.error("Failed to fetch player handles:", error);
27+
}
28+
}, [data, fetchPlayerHandles]);
29+
30+
useEffect(() => {
31+
updateTableData();
32+
}, [updateTableData]);
33+
34+
if (!tableData.length) return null;
35+
36+
return (
37+
<Table columns={[{ name: "player" }, { name: "score" }]} data={tableData} />
38+
);
39+
};
40+
41+
export default LeaderboardTable;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from "./LeaderboardTable";

src/components/Table/Table.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ interface TableColumn {
44
name: string;
55
}
66

7-
interface TableData {
8-
[key: string]: string;
7+
export interface TableData {
8+
[key: string]: string | number;
99
}
1010

1111
interface TableProps {

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export const TIC_RATE_MAGIC = 35; // 35 is the ticrate in DOOM WASM they use to calculate time.
22
export const SERVER_URL = import.meta.env.VITE_SERVER_URL;
3+
export const HANDLE_CACHE_KEY = "playerHandleCache";

src/hooks/usePlayersHandle.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useCustomCompareCallback } from "use-custom-compare";
2+
import { HANDLE_CACHE_KEY } from "../constants";
3+
import { truncateString } from "../utils/string";
4+
import { useLocalStorage } from "usehooks-ts";
5+
import isEqual from "lodash/isEqual";
6+
7+
const usePlayersHandle = () => {
8+
const [handles, setHandles] = useLocalStorage<{ [key: string]: string }>(
9+
HANDLE_CACHE_KEY,
10+
{},
11+
);
12+
13+
const updateCache = useCustomCompareCallback(
14+
(results: { [key: string]: string }) => {
15+
setHandles((currentHandles) => ({ ...currentHandles, ...results }));
16+
},
17+
[setHandles],
18+
isEqual,
19+
);
20+
21+
const fetchPlayerHandles = useCustomCompareCallback(
22+
async (players: string[]) => {
23+
const results: { [key: string]: string } = {};
24+
const promises = players.map(async (player) => {
25+
if (handles[player]) {
26+
results[player] = handles[player];
27+
return;
28+
}
29+
30+
try {
31+
const response = await fetch(
32+
`https://auth.hydradoom.fun/v1/session/${player}`,
33+
);
34+
if (!response.ok) {
35+
throw new Error(`HTTP error! status: ${response.status}`);
36+
}
37+
const data = await response.json();
38+
39+
results[player] = data.handle
40+
? data.handle
41+
: truncateString(player, 7, 7);
42+
} catch {
43+
results[player] = truncateString(player, 7, 7);
44+
}
45+
});
46+
47+
await Promise.all(promises);
48+
updateCache(results);
49+
return results;
50+
},
51+
[handles, updateCache],
52+
isEqual,
53+
);
54+
55+
return { fetchPlayerHandles };
56+
};
57+
58+
export default usePlayersHandle;

src/utils/leaderboard.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { orderBy } from "lodash";
2+
import { LeaderboardEntry } from "../types";
3+
import { TableData } from "../components/Table/Table";
4+
5+
export const getMap = (items?: LeaderboardEntry[]) => {
6+
return items?.reduce((acc: { [key: string]: number }, [playerId, kills]) => {
7+
acc[playerId] = kills;
8+
return acc;
9+
}, {});
10+
};
11+
12+
export const getTableData = (
13+
map: ReturnType<typeof getMap>,
14+
handles: { [key: string]: string },
15+
): TableData[] => {
16+
if (!map) return [];
17+
const players = Object.keys(map);
18+
return orderBy(
19+
players.map((player) => ({
20+
player: handles[player],
21+
score: Number(
22+
new Intl.NumberFormat("en-US", { notation: "compact" }).format(
23+
map[player],
24+
),
25+
),
26+
})),
27+
"score",
28+
"desc",
29+
);
30+
};

0 commit comments

Comments
 (0)