step 10.17: wip (not ticked)
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
// Leaderboard page: public leaderboard with tabs for Level, Gold, Kills.
|
||||
// Uses createElement + textContent only — DOM property is never overwritten.
|
||||
|
||||
const VALUE_LABELS = { level: 'Level', gold: 'Gold', kills: 'Kills' };
|
||||
|
||||
let currentType = 'level';
|
||||
let currentPage = 1;
|
||||
|
||||
// ─── API helper (no auth needed) ──────────────────────────────────────────
|
||||
|
||||
async function apiGet(path) {
|
||||
const res = await fetch(path);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ─── Render: leaderboard entries ──────────────────────────────────────────
|
||||
|
||||
function renderLeaderboard(entries, pagination) {
|
||||
const tbody = document.getElementById('leaderboard-body');
|
||||
tbody.replaceChildren();
|
||||
|
||||
if (entries.length === 0) {
|
||||
const tr = document.createElement('tr');
|
||||
const td = document.createElement('td');
|
||||
td.className = 'placeholder';
|
||||
td.textContent = 'No heroes yet.';
|
||||
td.setAttribute('colspan', '4');
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// Rank
|
||||
const rankTd = document.createElement('td');
|
||||
rankTd.className = 'rank-cell';
|
||||
rankTd.textContent = entry.rank;
|
||||
tr.appendChild(rankTd);
|
||||
|
||||
// Name (link to profile)
|
||||
const nameTd = document.createElement('td');
|
||||
nameTd.className = 'name-cell';
|
||||
const nameLink = document.createElement('a');
|
||||
nameLink.href = `/profile.html?name=${encodeURIComponent(entry.name)}`;
|
||||
nameLink.textContent = entry.name;
|
||||
nameTd.appendChild(nameLink);
|
||||
tr.appendChild(nameTd);
|
||||
|
||||
// Class
|
||||
const classTd = document.createElement('td');
|
||||
classTd.className = 'class-cell';
|
||||
classTd.textContent = entry.class;
|
||||
tr.appendChild(classTd);
|
||||
|
||||
// Value (level / gold / kills)
|
||||
const valueTd = document.createElement('td');
|
||||
valueTd.className = 'value-cell';
|
||||
valueTd.textContent = entry[currentType];
|
||||
tr.appendChild(valueTd);
|
||||
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Render: pagination ──────────────────────────────────────────────────
|
||||
|
||||
function renderPagination(page, totalPages) {
|
||||
const container = document.getElementById('pagination');
|
||||
container.replaceChildren();
|
||||
|
||||
if (totalPages <= 1) return;
|
||||
|
||||
// Previous button
|
||||
const prevBtn = document.createElement('button');
|
||||
prevBtn.textContent = '‹';
|
||||
prevBtn.disabled = page <= 1;
|
||||
prevBtn.addEventListener('click', () => {
|
||||
currentPage = page - 1;
|
||||
loadLeaderboard();
|
||||
});
|
||||
container.appendChild(prevBtn);
|
||||
|
||||
// Page number buttons (show up to 5 pages)
|
||||
let startPage = Math.max(1, page - 2);
|
||||
let endPage = Math.min(totalPages, startPage + 4);
|
||||
if (endPage - startPage < 4) startPage = Math.max(1, endPage - 4);
|
||||
|
||||
for (let p = startPage; p <= endPage; p++) {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = p;
|
||||
if (p === page) btn.className = 'active';
|
||||
btn.addEventListener('click', () => {
|
||||
currentPage = p;
|
||||
loadLeaderboard();
|
||||
});
|
||||
container.appendChild(btn);
|
||||
}
|
||||
|
||||
// Next button
|
||||
const nextBtn = document.createElement('button');
|
||||
nextBtn.textContent = '›';
|
||||
nextBtn.disabled = page >= totalPages;
|
||||
nextBtn.addEventListener('click', () => {
|
||||
currentPage = page + 1;
|
||||
loadLeaderboard();
|
||||
});
|
||||
container.appendChild(nextBtn);
|
||||
}
|
||||
|
||||
// ─── Load leaderboard data ───────────────────────────────────────────────
|
||||
|
||||
async function loadLeaderboard() {
|
||||
const tbody = document.getElementById('leaderboard-body');
|
||||
const tr = document.createElement('tr');
|
||||
const td = document.createElement('td');
|
||||
td.className = 'placeholder';
|
||||
td.textContent = 'Loading...';
|
||||
td.setAttribute('colspan', '4');
|
||||
tr.appendChild(td);
|
||||
tbody.replaceChildren(tr);
|
||||
|
||||
try {
|
||||
const data = await apiGet(`/api/leaderboard?type=${currentType}&page=${currentPage}`);
|
||||
if (!data || !data.ok) {
|
||||
throw new Error(data?.error?.message || 'Failed to load leaderboard.');
|
||||
}
|
||||
renderLeaderboard(data.data.entries, data.data.pagination);
|
||||
renderPagination(data.data.pagination.page, data.data.pagination.totalPages);
|
||||
} catch (err) {
|
||||
const errorTr = document.createElement('tr');
|
||||
const errorTd = document.createElement('td');
|
||||
errorTd.className = 'placeholder';
|
||||
errorTd.textContent = 'Failed to load leaderboard.';
|
||||
errorTd.setAttribute('colspan', '4');
|
||||
errorTr.appendChild(errorTd);
|
||||
tbody.replaceChildren(errorTr);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tab switching ───────────────────────────────────────────────────────
|
||||
|
||||
function switchTab(type) {
|
||||
currentType = type;
|
||||
currentPage = 1;
|
||||
|
||||
document.querySelectorAll('#leaderboard-tabs .tab').forEach((t) => {
|
||||
const isActive = t.dataset.type === type;
|
||||
t.classList.toggle('active', isActive);
|
||||
t.setAttribute('aria-selected', isActive);
|
||||
});
|
||||
|
||||
// Update header
|
||||
document.getElementById('value-header').textContent = VALUE_LABELS[type];
|
||||
loadLeaderboard();
|
||||
}
|
||||
|
||||
// ─── Init ─────────────────────────────────────────────────────────────────
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('#leaderboard-tabs .tab').forEach((tab) => {
|
||||
tab.addEventListener('click', () => switchTab(tab.dataset.type));
|
||||
});
|
||||
|
||||
loadLeaderboard();
|
||||
});
|
||||
|
||||
// Logout handler
|
||||
document.getElementById('logout-btn')?.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
} catch (_) {
|
||||
// Ignore
|
||||
}
|
||||
window.location.href = '/login.html';
|
||||
});
|
||||
Reference in New Issue
Block a user