Skip to content

Commit 2efcdba

Browse files
akshara200829-lgtmjagdish-15github-actions[bot]
authored
feat: add rank change indicators to leaderboard (#95)
* feat: add rank change indicators to leaderboard * fix: address reviewer feedback on rank change indicators * style: format files with prettier * Use GitHub history for rank comparisons * Add DATA_REPO_TOKEN to sync-leaderboard workflow * style: auto-format code with Prettier (/format) --------- Co-authored-by: Jagdish Prajapati <jagadishdrp@gmail.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 8831f6f commit 2efcdba

3 files changed

Lines changed: 102 additions & 2 deletions

File tree

.github/workflows/sync-leaderboard.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ jobs:
5555
5656
# 2. Run sync
5757
export DATA_DIR=db-repo
58+
export DATA_REPO_TOKEN=${{ secrets.DATA_REPO_TOKEN }}
5859
node scripts/sync-leaderboard.js
5960
6061
# 3. Stage changes (everything except users.json)

frontend/leaderboard.html

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,25 @@
1111
rel="stylesheet"
1212
/>
1313
<link rel="stylesheet" href="styles/main.css" />
14+
<style>
15+
.rank-change {
16+
display: inline-block;
17+
font-size: 0.65rem;
18+
font-family: "Fira Code", monospace;
19+
margin-left: 0.4rem;
20+
vertical-align: middle;
21+
opacity: 0.85;
22+
}
23+
.rank-up {
24+
color: var(--green);
25+
}
26+
.rank-down {
27+
color: var(--red);
28+
}
29+
.rank-neutral {
30+
color: var(--text-muted);
31+
}
32+
</style>
1433
<link rel="icon" type="image/png" href="assets/logo.png" />
1534
<script src="js/navbar.js"></script>
1635
</head>
@@ -357,6 +376,16 @@ <h1 class="page-title">Leaderboard</h1>
357376
`;
358377
renderLeaderboard(filteredData);
359378
}
379+
function getRankChangeTag(rankChange) {
380+
if (!rankChange) return "";
381+
if (rankChange === "NEW")
382+
return `<span class="rank-change rank-neutral">[new]</span>`;
383+
if (rankChange === "=")
384+
return `<span class="rank-change rank-neutral">[==]</span>`;
385+
if (rankChange.startsWith("+"))
386+
return `<span class="rank-change rank-up">[${rankChange}]</span>`;
387+
return `<span class="rank-change rank-down">[${rankChange}]</span>`;
388+
}
360389
function getRankTag(rank) {
361390
switch (rank) {
362391
case 1:
@@ -416,7 +445,7 @@ <h1 class="page-title">Leaderboard</h1>
416445
row.className = "leaderboard-row";
417446
row.innerHTML = `
418447
<div class="rank">${rank}</div>
419-
<div class="name-cell">${tag}${user.name}</div>
448+
<div class="name-cell">${tag}${user.name}${getRankChangeTag(user.rankChange)}</div>
420449
<div class="username"><a href="https://leetcode.com/u/${user.id}" target="_blank" rel="noopener noreferrer" class="user-link">${user.id}
421450
<svg class="external-icon" width="12" height="12" viewBox="0 0 24 24">
422451
<path d="M14 3H21V10M21 3L10 14M21 14V21H3V3H10"
@@ -485,7 +514,7 @@ <h1 class="page-title">Leaderboard</h1>
485514
</div>
486515
487516
488-
<div class="mobile-name">${tag}${user.name}</div>
517+
<div class="mobile-name">${tag}${user.name}${getRankChangeTag(user.rankChange)}</div>
489518
<div class="mobile-username"><a href="https://leetcode.com/u/${user.id}" target="_blank" rel="noopener noreferrer" class="user-link">${user.id}
490519
<svg class="external-icon" width="12" height="12" viewBox="0 0 24 24">
491520
<path d="M14 3H21V10M21 3L10 14M21 14V21H3V3H10"

scripts/sync-leaderboard.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,72 @@ function getFileName(daysAgo) {
2929
return `${year}-${month}-${date}-${day}.json`;
3030
}
3131

32+
async function getYesterdaySnapshot(filePath) {
33+
const owner = "codepvg";
34+
const repo = "leetcode-ranking-data";
35+
const d = new Date();
36+
d.setDate(d.getDate() - 1);
37+
const targetDate = d.toISOString().split("T")[0];
38+
const until = `${targetDate}T23:59:59Z`;
39+
const commitsUrl = `https://api.github.com/repos/${owner}/${repo}/commits?path=${filePath}&until=${until}&per_page=1`;
40+
41+
const headers = { "User-Agent": "CodePVG-App" };
42+
if (process.env.DATA_REPO_TOKEN) {
43+
headers["Authorization"] = `token ${process.env.DATA_REPO_TOKEN}`;
44+
}
45+
46+
try {
47+
const commitResponse = await axios.get(commitsUrl, { headers });
48+
const commits = commitResponse.data;
49+
50+
if (!commits || commits.length === 0) {
51+
console.warn(
52+
`No commits found for ${filePath} on or before ${targetDate}.`,
53+
);
54+
return null;
55+
}
56+
57+
const yesterdaySHA = commits[0].sha;
58+
console.log(
59+
`📌 Using Commit for ${filePath}: "${commits[0].commit.message}" (SHA: ${yesterdaySHA})`,
60+
);
61+
62+
const rawFileUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${yesterdaySHA}/${filePath}`;
63+
const fileResponse = await axios.get(rawFileUrl);
64+
return fileResponse.data;
65+
} catch (error) {
66+
console.error(
67+
`Error fetching historical data for ${filePath}:`,
68+
error.message,
69+
);
70+
return null;
71+
}
72+
}
73+
74+
async function computeRankChanges(currentSorted, filename) {
75+
let previousRanks = {};
76+
const previousData = await getYesterdaySnapshot(filename);
77+
78+
if (previousData && Array.isArray(previousData)) {
79+
previousData.forEach((user, idx) => {
80+
previousRanks[user.id] = idx + 1;
81+
});
82+
}
83+
84+
currentSorted.forEach((user, idx) => {
85+
const currentRank = idx + 1;
86+
87+
if (previousRanks[user.id] === undefined) {
88+
user.rankChange = "NEW";
89+
} else {
90+
const delta = previousRanks[user.id] - currentRank;
91+
if (delta > 0) user.rankChange = `+${delta}`;
92+
else if (delta < 0) user.rankChange = `${delta}`;
93+
else user.rankChange = "=";
94+
}
95+
});
96+
}
97+
3298
(async () => {
3399
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, "..", "data");
34100
console.log(`Using data directory: ${DATA_DIR}`);
@@ -87,6 +153,7 @@ function getFileName(daysAgo) {
87153
overallData.sort((a, b) => b.score - a.score);
88154
console.log("Writing sorted daily data to overall file...");
89155
const overallFilepath = path.join(DATA_DIR, "overall.json");
156+
await computeRankChanges(overallData, "overall.json");
90157
try {
91158
fs.writeFileSync(
92159
overallFilepath,
@@ -144,6 +211,7 @@ function getFileName(daysAgo) {
144211

145212
console.log("Writing sorted daily data to daily.json...");
146213
const dailyFilepath = path.join(DATA_DIR, "daily.json");
214+
await computeRankChanges(dailyData, "daily.json");
147215
try {
148216
fs.writeFileSync(dailyFilepath, JSON.stringify(dailyData, null, 2), "utf8");
149217
console.log("Daily data saved successfully");
@@ -199,6 +267,7 @@ function getFileName(daysAgo) {
199267

200268
console.log("Writing sorted weekly data to weekly.json...");
201269
const weeklyFilepath = path.join(DATA_DIR, "weekly.json");
270+
await computeRankChanges(weeklyData, "weekly.json");
202271
try {
203272
fs.writeFileSync(
204273
weeklyFilepath,
@@ -258,6 +327,7 @@ function getFileName(daysAgo) {
258327

259328
console.log("Writing sorted monthly data to monthly.json...");
260329
const monthlyFilepath = path.join(DATA_DIR, "monthly.json");
330+
await computeRankChanges(monthlyData, "monthly.json");
261331
try {
262332
fs.writeFileSync(
263333
monthlyFilepath,

0 commit comments

Comments
 (0)