Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions frontend/js/leaderboard/mobile-scroll-top.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
document.addEventListener("DOMContentLoaded", () => {
const scrollTopBtn = document.getElementById("scrollTopBtn");

if (!scrollTopBtn) return;

window.addEventListener("scroll", () => {
if (window.innerWidth <= 768 && window.scrollY > 300) {
scrollTopBtn.style.display = "flex";
} else {
scrollTopBtn.style.display = "none";
}
});

scrollTopBtn.addEventListener("click", () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
});
});
308 changes: 279 additions & 29 deletions frontend/leaderboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,6 @@
<link rel="stylesheet" href="styles/main.css" />
<link rel="icon" type="image/png" href="assets/logo.png" />
<script src="js/navbar.js"></script>
<script src="js/leaderboard/tooltip.js"></script>
<script src="js/leaderboard/render.js"></script>
<script src="js/leaderboard/pagination.js"></script>
<script src="js/leaderboard/search.js"></script>

<meta
name="description"
content="Live LeetCode leaderboard for PVG students. See overall, monthly, weekly, and daily rankings scored by problem difficulty."
/>
<meta property="og:title" content="Leaderboard — CodePVG" />
<meta
property="og:description"
content="Live LeetCode leaderboard for PVG students. Scored by problem difficulty, updated every 5 minutes."
/>
<meta
property="og:url"
content="https://codepvg.onrender.com/leaderboard"
/>
<meta property="og:type" content="website" />
<meta
property="og:image"
content="https://codepvg.onrender.com/assets/logo.png"
/>
<meta property="og:site_name" content="CodePVG" />
</head>

<body>
Expand Down Expand Up @@ -158,16 +134,102 @@ <h1 class="page-title">Leaderboard</h1>
</div>
</main>

<script nonce="__NONCE__">
<button id="scrollTopBtn" aria-label="Scroll to top">
</button>

<script>
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".tab").forEach((tab) => {
tab.addEventListener("click", () => {
setActiveTab(tab.dataset.tab);
});
});

setupSearchListeners();
setupPaginationListeners();
const searchInput = document.getElementById("leaderboard-search");
const shortcutBadge = document.getElementById("search-shortcut");
const clearBtn = document.getElementById("clear-search");

searchInput.addEventListener("input", (e) => {
currentSearchTerm = e.target.value.toLowerCase().trim();

clearBtn.style.display =
e.target.value.trim() !== "" ? "flex" : "none";


clearBtn.style.display =
e.target.value.trim() !== "" ? "flex" : "none";

applyFiltersAndRender();
});
clearBtn.addEventListener("click", () => {
searchInput.value = "";
currentSearchTerm = "";

clearBtn.style.display = "none";

searchInput.focus();
applyFiltersAndRender();
});
clearBtn.addEventListener("click", () => {
searchInput.value = "";
currentSearchTerm = "";

clearBtn.style.display = "none";

searchInput.focus();
applyFiltersAndRender();
});

searchInput.addEventListener("focus", () => {
shortcutBadge.style.display = "none";
});

searchInput.addEventListener("blur", () => {
if (!searchInput.value) {
shortcutBadge.style.display = "inline-flex";
}
});

// Ctrl+K / Cmd+K keyboard shortcut
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
e.preventDefault();
searchInput.focus();
}
if (e.key === "Escape" && document.activeElement === searchInput) {
searchInput.value = "";
currentSearchTerm = "";
clearBtn.style.display = "none";
searchInput.blur();
applyFiltersAndRender();
}
});
document
.getElementById("prev-page-btn")
?.addEventListener("click", () => {
if (currentPage > 1) {
currentPage--;
applyFiltersAndRender();
window.scrollTo({ top: 0, behavior: "smooth" });
}
});

document
.getElementById("next-page-btn")
?.addEventListener("click", () => {
const totalPages = Math.ceil(
leaderboardData[activeDatasetType].length / itemsPerPage,
);

if (currentPage < totalPages) {
currentPage++;
applyFiltersAndRender();
window.scrollTo({ top: 0, behavior: "smooth" });
}
});
clearBtn.style.display = "none";
clearBtn.style.display = "none";
fetchLeaderboardData();
});

Expand Down Expand Up @@ -313,6 +375,18 @@ <h1 class="page-title">Leaderboard</h1>
`;
renderLeaderboard(filteredData);
}
function getRankTag(rank) {
switch (rank) {
case 1:
return '<span class="privilege-tag root-tag">[ROOT]</span>';
case 2:
return '<span class="privilege-tag sudo-tag">[SUDO]</span>';
case 3:
return '<span class="privilege-tag exec-tag">[EXEC]</span>';
default:
return "";
}
}

function renderLeaderboard(data) {
const body = document.getElementById("leaderboard-body");
Expand Down Expand Up @@ -347,13 +421,185 @@ <h1 class="page-title">Leaderboard</h1>

displayData.forEach((user, index) => {
const rank = user.originalRank || index + 1;
const row = renderLeaderboardRow(user, rank);
const card = renderMobileCard(user, rank);
const tag = getRankTag(rank);
const leetcodeUrl = `https://leetcode.com/u/${user.id}`;
const easyPoints = 1;
const mediumPoints = 3;
const hardPoints = 5;

const easyScore = user.data.easySolved * easyPoints;
const mediumScore = user.data.mediumSolved * mediumPoints;
const hardScore = user.data.hardSolved * hardPoints;
const row = document.createElement("div");
row.className = "leaderboard-row";
row.innerHTML = `
<div class="rank">${rank}</div>
<div class="name-cell">${tag}${user.name}</div>
<div class="username"><a href="https://leetcode.com/u/${user.id}" target="_blank" rel="noopener noreferrer" class="user-link">${user.id}
<svg class="external-icon" width="12" height="12" viewBox="0 0 24 24">
<path d="M14 3H21V10M21 3L10 14M21 14V21H3V3H10"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"/>
</svg></a></div>
<div class="easy">${user.data.easySolved}</div>
<div class="medium">${user.data.mediumSolved}</div>
<div class="hard">${user.data.hardSolved}</div>
<div class="total">${user.data.totalSolved}</div>
<div class="mobile-score tooltip-score">
<span>${user.score}</span>
<span class="score-caret"></span>
<div class="score-tooltip">
<div>
Easy: ${user.data.easySolved} × ${easyPoints} = ${easyScore}
</div>
<div>
Medium: ${user.data.mediumSolved} × ${mediumPoints} = ${mediumScore}
</div>
<div>
Hard: ${user.data.hardSolved} × ${hardPoints} = ${hardScore}
</div>
<div>
Questions Solved: ${user.data.totalSolved}
</div>
<hr>
<div>
<strong>Total: ${user.score}</strong>
</div>
</div>
</div>
`;
body.appendChild(row);

const card = document.createElement("div");
card.className = "mobile-card";
card.innerHTML = `
<div class="mobile-card-header">
<div class="mobile-rank">#${rank}</div>
<div class="mobile-score tooltip-score">
<span> ${user.score}</span>
<span class="score-caret"></span>
<div class="score-tooltip">
<div>
Easy: ${user.data.easySolved} × ${easyPoints} = ${easyScore}
</div>
<div>
Medium: ${user.data.mediumSolved} × ${mediumPoints} = ${mediumScore}
</div>
<div>
Hard: ${user.data.hardSolved} × ${hardPoints} = ${hardScore}
</div>
<div>
Questions Solved: ${user.data.totalSolved}
</div>
<hr>
<div>
<strong>Total: ${user.score}</strong>
</div>
</div>
</div>
</div>


<div class="mobile-name">${tag}${user.name}</div>
<div class="mobile-username"><a href="https://leetcode.com/u/${user.id}" target="_blank" rel="noopener noreferrer" class="user-link">${user.id}
<svg class="external-icon" width="12" height="12" viewBox="0 0 24 24">
<path d="M14 3H21V10M21 3L10 14M21 14V21H3V3H10"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"/>
</svg></a></div>
<div class="mobile-stats">
<div class="mobile-stat">
<div class="mobile-stat-number easy">${user.data.easySolved}</div>
<div class="mobile-stat-label">Easy</div>
</div>
<div class="mobile-stat">
<div class="mobile-stat-number medium">${user.data.mediumSolved}</div>
<div class="mobile-stat-label">Medium</div>
</div>
<div class="mobile-stat">
<div class="mobile-stat-number hard">${user.data.hardSolved}</div>
<div class="mobile-stat-label">Hard</div>
</div>
<div class="mobile-stat">
<div class="mobile-stat-number total">${user.data.totalSolved}</div>
<div class="mobile-stat-label">Total</div>
</div>
</div>
`;
mobileCards.appendChild(card);
});
renderPagination(data.length);
}
function renderPagination(totalItems) {
const pageNumbers = document.getElementById("page-numbers");

if (!pageNumbers) return;

const totalPages = Math.ceil(totalItems / itemsPerPage);
pageNumbers.innerHTML = "";

if (totalPages === 0) return;

function createPageBtn(i) {
const btn = document.createElement("button");
btn.classList.add("page-btn");
btn.textContent = i;

if (i === currentPage) {
btn.classList.add("active-page");
}

btn.onclick = () => {
currentPage = i;
applyFiltersAndRender();
window.scrollTo({ top: 0, behavior: "smooth" });
};

return btn;
}

const pagesToRender = [];
const range = 1; // Number of pages to show around the current page

pagesToRender.push(1);

if (currentPage - range > 2) {
pagesToRender.push("..");
}

for (
let i = Math.max(2, currentPage - range);
i <= Math.min(totalPages - 1, currentPage + range);
i++
) {
pagesToRender.push(i);
}

if (currentPage + range < totalPages - 1) {
pagesToRender.push("..");
}

if (totalPages > 1) {
pagesToRender.push(totalPages);
}

pagesToRender.forEach((item) => {
if (item === "..") {
const ellipsis = document.createElement("span");
ellipsis.className = "page-ellipsis";
ellipsis.textContent = "..";
pageNumbers.appendChild(ellipsis);
} else {
pageNumbers.appendChild(createPageBtn(item));
}
});
}
function setActiveTab(activeTab) {
document.querySelectorAll(".tab").forEach((tab) => {
tab.classList.remove("active");
Expand All @@ -374,5 +620,9 @@ <h1 class="page-title">Leaderboard</h1>
<script src="js/footer.js"></script>
<script src="js/matrix.js"></script>
<script src="js/terminal_ui.js"></script>
<script src="js/leaderboard/mobile-scroll-top.js"></script>



</body>
</html>
Loading
Loading