more QOL improvements
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 29s
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 29s
This commit is contained in:
52
src/views/admin/edit-hunt.ejs
Normal file
52
src/views/admin/edit-hunt.ejs
Normal file
@@ -0,0 +1,52 @@
|
||||
<%- include('../partials/header') %>
|
||||
|
||||
<div class="container-narrow" style="padding-top: 2rem;">
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="/admin/hunts/<%= hunt.id %>" style="color: var(--muted); text-decoration: none;">← Back to <%= hunt.name %></a>
|
||||
</div>
|
||||
|
||||
<h1 style="margin-bottom: 1.5rem;">Edit Hunt</h1>
|
||||
|
||||
<div class="card">
|
||||
<% if (error) { %>
|
||||
<div class="alert alert-danger error"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/hunts/<%= hunt.id %>/edit">
|
||||
<div class="form-group">
|
||||
<label for="name">Hunt Name</label>
|
||||
<input type="text" id="name" name="name" class="form-control" required value="<%= hunt.name %>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Short Name</label>
|
||||
<input type="text" class="form-control" value="<%= hunt.short_name %>" disabled style="font-family: monospace; opacity: 0.6;">
|
||||
<div class="form-hint">Short name cannot be changed (used in QR codes).</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" class="form-control" rows="3"><%= hunt.description || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="expiry_date">Expiry Date (optional)</label>
|
||||
<input type="datetime-local" id="expiry_date" name="expiry_date" class="form-control"
|
||||
value="<%= hunt.expiry_date ? new Date(hunt.expiry_date).toISOString().slice(0, 16) : '' %>">
|
||||
<div class="form-hint">Leave blank for no expiry.</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center;">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="border: 2px solid var(--danger); margin-top: 1.5rem;">
|
||||
<div class="card-header" style="color: var(--danger);">Danger Zone</div>
|
||||
<p style="color: var(--muted); font-size: 0.9rem;">Permanently delete this hunt, all its packages, and all scan data. This cannot be undone.</p>
|
||||
<form method="POST" action="/admin/hunts/<%= hunt.id %>/delete" onsubmit="return confirm('Are you sure you want to permanently delete "<%= hunt.name %>"? This will delete ALL packages and scan data. This cannot be undone.')">
|
||||
<button type="submit" class="btn btn-danger">Delete Hunt</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('../partials/footer') %>
|
||||
@@ -7,6 +7,7 @@
|
||||
<span style="color: var(--muted); font-family: monospace; font-size: 1rem;"><%= hunt.short_name %></span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<a href="/admin/hunts/<%= hunt.id %>/edit" class="btn btn-outline">✏️ Edit</a>
|
||||
<a href="/admin/hunts/<%= hunt.id %>/pdf" class="btn btn-success">📥 Download PDF</a>
|
||||
<a href="/hunt/<%= hunt.short_name %>" class="btn btn-outline">View Public Page</a>
|
||||
<a href="/hunt/<%= hunt.short_name %>/leaderboard" class="btn btn-outline">Leaderboard</a>
|
||||
@@ -32,12 +33,62 @@
|
||||
<div class="value"><%= packages.reduce((sum, p) => sum + p.scan_count, 0) %></div>
|
||||
<div class="label">Total Scans</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="value"><%= typeof stats !== 'undefined' ? stats.uniquePlayers : 0 %></div>
|
||||
<div class="label">Players</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="value"><%= typeof stats !== 'undefined' ? stats.discoveryRate + '%' : '0%' %></div>
|
||||
<div class="label">Discovery Rate</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="value"><%= hunt.expiry_date ? new Date(hunt.expiry_date).toLocaleDateString() : '—' %></div>
|
||||
<div class="label">Expires</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (typeof stats !== 'undefined' && stats.topFinders.length > 0) { %>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<div class="card" style="flex: 1; min-width: 280px;">
|
||||
<div class="card-header">Top Finders</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead><tr><th>Player</th><th>Finds</th><th>Points</th></tr></thead>
|
||||
<tbody>
|
||||
<% stats.topFinders.forEach(f => { %>
|
||||
<tr>
|
||||
<td><a href="/player/<%= f.username %>"><%= f.username %></a></td>
|
||||
<td><%= f.finds %></td>
|
||||
<td><span class="points-badge">+<%= f.points %></span></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<% if (stats.recentScans.length > 0) { %>
|
||||
<div class="card" style="flex: 1; min-width: 280px;">
|
||||
<div class="card-header">Recent Scans</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>Player</th><th>Points</th><th>When</th></tr></thead>
|
||||
<tbody>
|
||||
<% stats.recentScans.forEach(s => { %>
|
||||
<tr>
|
||||
<td><%= s.card_number %></td>
|
||||
<td><a href="/player/<%= s.username %>"><%= s.username %></a></td>
|
||||
<td><span class="points-badge">+<%= s.points_awarded %></span></td>
|
||||
<td style="font-size: 0.85rem; color: var(--muted);"><time datetime="<%= s.scanned_at %>"><%= new Date(s.scanned_at).toLocaleString() %></time></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<h2 style="margin-top: 1.5rem; margin-bottom: 1rem;">All Packages</h2>
|
||||
|
||||
<div class="table-wrapper">
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<span class="points-badge" style="font-size: 0.85rem; padding: 0.25rem 0.6rem;">+<%= a.points_awarded %></span>
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div style="font-weight: 600;"><a href="/player/<%= a.username %>" style="text-decoration: none; color: inherit;"><%= a.username %></a> found <a href="/hunt/<%= a.hunt_short_name %>/<%= a.card_number %>" style="color: var(--primary);">#<%= a.card_number %></a> in <a href="/hunt/<%= a.hunt_short_name %>" style="color: var(--primary);"><%= a.hunt_name %></a></div>
|
||||
<div style="font-size: 0.8rem; color: var(--muted);"><%= new Date(a.scanned_at).toLocaleString() %></div>
|
||||
<div style="font-size: 0.8rem; color: var(--muted);"><time datetime="<%= a.scanned_at %>"><%= new Date(a.scanned_at).toLocaleString() %></time></div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
|
||||
@@ -25,8 +25,9 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<% leaderboard.forEach((entry, i) => { %>
|
||||
<% const rank = (typeof offset !== 'undefined' ? offset : 0) + i + 1; %>
|
||||
<tr>
|
||||
<td class="rank-cell rank-<%= i + 1 %>"><%= i + 1 %></td>
|
||||
<td class="rank-cell rank-<%= rank %>"><%= rank %></td>
|
||||
<td><strong><a href="/player/<%= entry.username %>"><%= entry.username %></a></strong></td>
|
||||
<td><span class="points-badge"><%= entry.total_points %></span></td>
|
||||
<td><%= entry.scans %></td>
|
||||
@@ -36,6 +37,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('../partials/pagination') %>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -21,9 +21,10 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<% leaderboard.forEach((entry, i) => { %>
|
||||
<% const rank = (typeof offset !== 'undefined' ? offset : 0) + i + 1; %>
|
||||
<tr>
|
||||
<td class="rank-cell rank-<%= i + 1 %>">
|
||||
<% if (i === 0) { %>🥇<% } else if (i === 1) { %>🥈<% } else if (i === 2) { %>🥉<% } else { %><%= i + 1 %><% } %>
|
||||
<td class="rank-cell rank-<%= rank %>">
|
||||
<% if (rank === 1) { %>🥇<% } else if (rank === 2) { %>🥈<% } else if (rank === 3) { %>🥉<% } else { %><%= rank %><% } %>
|
||||
</td>
|
||||
<td><strong><a href="/player/<%= entry.username %>"><%= entry.username %></a></strong></td>
|
||||
<td><span class="points-badge"><%= entry.total_points %></span></td>
|
||||
@@ -34,6 +35,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('../partials/pagination') %>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<td><%= i + 1 %></td>
|
||||
<td><a href="/player/<%= scan.username %>"><%= scan.username %></a></td>
|
||||
<td><% if (scan.points_awarded > 0) { %><span class="points-badge">+<%= scan.points_awarded %></span><% } else { %><span style="color: var(--muted);">0</span><% } %></td>
|
||||
<td style="font-size: 0.85rem; color: var(--muted);"><%= new Date(scan.scanned_at).toLocaleString() %></td>
|
||||
<td style="font-size: 0.85rem; color: var(--muted);"><time datetime="<%= scan.scanned_at %>"><%= new Date(scan.scanned_at).toLocaleString() %></time></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
<td><%= i + 1 %></td>
|
||||
<td><a href="/player/<%= scan.username %>"><%= scan.username %></a></td>
|
||||
<td><% if (scan.points_awarded > 0) { %><span class="points-badge">+<%= scan.points_awarded %></span><% } else { %><span style="color: var(--muted);">0</span><% } %></td>
|
||||
<td style="font-size: 0.85rem; color: var(--muted);"><%= new Date(scan.scanned_at).toLocaleString() %></td>
|
||||
<td style="font-size: 0.85rem; color: var(--muted);"><time datetime="<%= scan.scanned_at %>"><%= new Date(scan.scanned_at).toLocaleString() %></time></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
<footer class="footer">
|
||||
© <%= new Date().getFullYear() %> Loot Hunt — Find. Scan. Conquer.
|
||||
</footer>
|
||||
<script src="/js/timeago.js"></script>
|
||||
<script>
|
||||
function toggleTheme() {
|
||||
var html = document.documentElement;
|
||||
var current = html.getAttribute('data-theme');
|
||||
var next = current === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
// Update toggle icon
|
||||
var btn = document.querySelector('.theme-toggle');
|
||||
if (btn) btn.textContent = next === 'dark' ? '\u2600\uFE0F' : '\uD83C\uDF19';
|
||||
}
|
||||
// Set correct icon on load
|
||||
(function() {
|
||||
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
var btn = document.querySelector('.theme-toggle');
|
||||
if (btn) btn.textContent = isDark ? '\u2600\uFE0F' : '\uD83C\uDF19';
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,13 +5,25 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= typeof title !== 'undefined' ? title + ' | Loot Hunt' : 'Loot Hunt' %></title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<script>
|
||||
// Apply theme before render to prevent flash
|
||||
(function() {
|
||||
var t = localStorage.getItem('theme');
|
||||
if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<a href="/" class="navbar-brand">🎯 Loot Hunt</a>
|
||||
<button class="nav-toggle" aria-label="Toggle menu" onclick="document.querySelector('.navbar-nav').classList.toggle('open')">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
||||
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode" title="Toggle dark mode">🌙</button>
|
||||
<button class="nav-toggle" aria-label="Toggle menu" onclick="document.querySelector('.navbar-nav').classList.toggle('open')">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="navbar-nav">
|
||||
<li><a href="/hunts">Hunts</a></li>
|
||||
<li><a href="/leaderboard">Leaderboard</a></li>
|
||||
|
||||
19
src/views/partials/pagination.ejs
Normal file
19
src/views/partials/pagination.ejs
Normal file
@@ -0,0 +1,19 @@
|
||||
<% if (typeof totalPages !== 'undefined' && totalPages > 1) { %>
|
||||
<div style="display: flex; justify-content: center; align-items: center; gap: 0.5rem; margin-top: 1.5rem; flex-wrap: wrap;">
|
||||
<% if (page > 1) { %>
|
||||
<a href="?page=<%= page - 1 %>" class="btn btn-sm btn-outline">← Prev</a>
|
||||
<% } %>
|
||||
<% for (let p = 1; p <= totalPages; p++) { %>
|
||||
<% if (p === page) { %>
|
||||
<span class="btn btn-sm btn-primary" style="pointer-events: none;"><%= p %></span>
|
||||
<% } else if (Math.abs(p - page) <= 2 || p === 1 || p === totalPages) { %>
|
||||
<a href="?page=<%= p %>" class="btn btn-sm btn-outline"><%= p %></a>
|
||||
<% } else if (Math.abs(p - page) === 3) { %>
|
||||
<span style="color: var(--muted);">…</span>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<% if (page < totalPages) { %>
|
||||
<a href="?page=<%= page + 1 %>" class="btn btn-sm btn-outline">Next →</a>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
@@ -66,7 +66,7 @@
|
||||
<td><a href="/hunt/<%= scan.hunt_short_name %>/<%= scan.card_number %>"><%= scan.card_number %> of <%= scan.package_count %></a></td>
|
||||
<td><a href="/hunt/<%= scan.hunt_short_name %>"><%= scan.hunt_name %></a></td>
|
||||
<td><% if (scan.points_awarded > 0) { %><span class="points-badge">+<%= scan.points_awarded %></span><% } else { %><span style="color: var(--muted);">0</span><% } %></td>
|
||||
<td style="font-size: 0.85rem; color: var(--muted);"><%= new Date(scan.scanned_at).toLocaleString() %></td>
|
||||
<td style="font-size: 0.85rem; color: var(--muted);"><time datetime="<%= scan.scanned_at %>"><%= new Date(scan.scanned_at).toLocaleString() %></time></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
@@ -75,7 +75,11 @@
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="card" style="text-align: center; color: var(--muted); padding: 2rem;">
|
||||
No scans yet. Get out there and find some loot!
|
||||
<% if (typeof isOwnProfile !== 'undefined' && isOwnProfile) { %>
|
||||
No scans yet. Get out there and find some loot!
|
||||
<% } else { %>
|
||||
This player hasn't found any loot yet.
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user