step 10.17: wip (not ticked)

This commit is contained in:
bepis
2026-10-23 07:01:49 +11:00
parent a475eaf8eb
commit e4ac3f49c5
9 changed files with 520 additions and 57 deletions
+6
View File
@@ -10,6 +10,12 @@
<div class="auth-container">
<h1>Emberfall</h1>
<h2>Create Your Hero</h2>
<nav class="nav-links" id="nav-links">
<a href="/game.html">Game</a>
<a href="/shop.html">Shop</a>
<a href="/leaderboard.html">Leaderboard</a>
<a href="/api/auth/logout" id="logout-btn">Logout</a>
</nav>
<div id="error" class="error" style="display:none;"></div>
<form id="create-hero-form">
<label for="hero-name">Hero Name</label>
+10
View File
@@ -94,6 +94,16 @@ button:hover { background: #c73650; }
.logo { color: #e94560; font-size: 1.25rem; font-weight: bold; }
.nav-links { display: flex; align-items: center; gap: 1rem; }
.nav-links a {
color: #a0a0a0;
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
transition: color 0.2s;
}
.nav-links a:hover { color: #e94560; }
.user-info { display: flex; align-items: center; gap: 1rem; }
.user-info span { color: #a0a0a0; }
.user-info a { color: #e94560; text-decoration: none; }
+6 -1
View File
@@ -11,9 +11,14 @@
<!-- Top bar -->
<header class="top-bar">
<div class="logo">Emberfall</div>
<nav class="nav-links" id="nav-links">
<a href="/game.html">Game</a>
<a href="/shop.html">Shop</a>
<a href="/leaderboard.html">Leaderboard</a>
<a href="/api/auth/logout" id="logout-btn">Logout</a>
</nav>
<div class="user-info">
<span id="player-name"></span>
<a href="/api/auth/logout" id="logout-btn">Logout</a>
</div>
</header>
+181
View File
@@ -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';
});
+112 -42
View File
@@ -7,62 +7,132 @@
<link rel="stylesheet" href="/css/main.css">
<style>
.leaderboard-container {
max-width: 600px;
margin: 2rem auto;
padding: 1rem;
text-align: center;
max-width: 700px;
margin: 0 auto;
}
.leaderboard-container h1 {
color: #e94560;
.leaderboard-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.leaderboard-container p {
color: #a59a8a;
.leaderboard-tabs .tab {
flex: 1;
}
.nav-links {
margin-top: 2rem;
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
.leaderboard-table {
width: 100%;
border-collapse: collapse;
}
.nav-links a {
display: inline-block;
padding: 0.5rem 1.25rem;
border-radius: 4px;
text-decoration: none;
font-size: 0.875rem;
font-weight: 600;
transition: opacity 0.2s;
}
.nav-links a:hover { opacity: 0.85; }
.btn-game {
background: #27ae60;
color: #fff;
}
.btn-home {
background: transparent;
.leaderboard-table th {
text-align: left;
color: #e94560;
border: 1px solid #e94560;
font-size: 0.75rem;
text-transform: uppercase;
padding: 0.5rem;
border-bottom: 1px solid #333;
}
.leaderboard-table td {
padding: 0.5rem;
border-bottom: 1px solid #1a1a2e;
font-size: 0.875rem;
color: #a0a0a0;
}
.leaderboard-table tr:hover td {
background: #1a1a2e;
}
.rank-cell { width: 60px; text-align: center; font-weight: bold; color: #e94560; }
.name-cell a { color: #88cc88; text-decoration: none; }
.name-cell a:hover { text-decoration: underline; }
.class-cell { text-transform: capitalize; }
.value-cell { text-align: right; font-weight: 600; }
.pagination {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-top: 1rem;
}
.pagination button {
padding: 0.25rem 0.75rem;
border: 1px solid #333;
background: #16213e;
color: #a0a0a0;
border-radius: 4px;
cursor: pointer;
}
.pagination button.active {
background: #e94560;
color: #fff;
border-color: #e94560;
}
.pagination button:disabled {
opacity: 0.4;
cursor: default;
}
.placeholder {
text-align: center;
color: #a0a0a0;
padding: 2rem;
}
</style>
</head>
<body>
<div class="leaderboard-container">
<h1>Leaderboard</h1>
<p>Coming soon — compete with heroes across the realm.</p>
<div class="nav-links">
<a href="/game.html" class="btn-game">Game</a>
<a href="/shop.html" class="btn-home">Shop</a>
<a href="/" class="btn-home">Home</a>
</div>
<div class="game-container">
<!-- Top bar -->
<header class="top-bar">
<div class="logo">Emberfall</div>
<nav class="nav-links" id="nav-links">
<a href="/game.html">Game</a>
<a href="/shop.html">Shop</a>
<a href="/leaderboard.html">Leaderboard</a>
<a href="/api/auth/logout" id="logout-btn">Logout</a>
</nav>
<div class="user-info">
<a href="/login.html">Login</a>
</div>
</header>
<main class="main-content">
<section class="panel" style="grid-column: 1 / -1;">
<h3>Leaderboard</h3>
<div class="leaderboard-tabs" id="leaderboard-tabs">
<button class="tab active" data-type="level" role="tab" aria-selected="true">Level</button>
<button class="tab" data-type="gold" role="tab" aria-selected="false">Gold</button>
<button class="tab" data-type="kills" role="tab" aria-selected="false">Kills</button>
</div>
<div id="leaderboard-error" class="error" style="display:none;"></div>
<table class="leaderboard-table" id="leaderboard-table">
<thead>
<tr>
<th class="rank-cell">Rank</th>
<th>Name</th>
<th class="class-cell">Class</th>
<th class="value-cell" id="value-header">Level</th>
</tr>
</thead>
<tbody id="leaderboard-body">
<tr><td colspan="4" class="placeholder">Loading...</td></tr>
</tbody>
</table>
<div class="pagination" id="pagination"></div>
</section>
</main>
</div>
<script src="/js/toast.js"></script>
<script src="/js/leaderboard.js"></script>
</body>
</html>
+6 -2
View File
@@ -11,11 +11,15 @@
<!-- Top bar -->
<header class="top-bar">
<div class="logo">Emberfall</div>
<nav class="nav-links" id="nav-links">
<a href="/game.html">Game</a>
<a href="/shop.html">Shop</a>
<a href="/leaderboard.html">Leaderboard</a>
<a href="/api/auth/logout" id="logout-btn">Logout</a>
</nav>
<div class="user-info">
<span id="player-name"></span>
<span id="player-gold" class="gold-display"></span>
<a href="/game.html">Game</a>
<a href="/api/auth/logout" id="logout-btn">Logout</a>
</div>
</header>