feat: add display name functionality for users and update related views
Build Images and Deploy / Update-PROD-Stack (push) Successful in 29s

This commit is contained in:
2026-03-20 12:58:30 -04:00
parent 051e35c581
commit ea537ff293
14 changed files with 81 additions and 39 deletions
+2 -1
View File
@@ -226,7 +226,8 @@ const ready = new Promise(resolve => { _readyResolve = resolve; });
const migrations = [ const migrations = [
'ALTER TABLE users ADD COLUMN is_organizer INTEGER DEFAULT 0', 'ALTER TABLE users ADD COLUMN is_organizer INTEGER DEFAULT 0',
'ALTER TABLE hunts ADD COLUMN start_date DATETIME', 'ALTER TABLE hunts ADD COLUMN start_date DATETIME',
'ALTER TABLE hunts ADD COLUMN hidden_until_start INTEGER DEFAULT 0' 'ALTER TABLE hunts ADD COLUMN hidden_until_start INTEGER DEFAULT 0',
'ALTER TABLE users ADD COLUMN display_name TEXT'
]; ];
for (const m of migrations) { for (const m of migrations) {
try { _db.run(m); } catch (e) { /* column already exists */ } try { _db.run(m); } catch (e) { /* column already exists */ }
+1
View File
@@ -36,6 +36,7 @@ function loadUser(req, res, next) {
res.locals.currentUser = { res.locals.currentUser = {
id: req.session.userId, id: req.session.userId,
username: req.session.username, username: req.session.username,
displayName: req.session.displayName || req.session.username,
isAdmin: req.session.isAdmin, isAdmin: req.session.isAdmin,
isOrganizer: req.session.isOrganizer isOrganizer: req.session.isOrganizer
}; };
+25 -18
View File
@@ -24,8 +24,8 @@ function getPointsForScanNumber(scanNumber) {
const Users = { const Users = {
create(username, password) { create(username, password) {
const hash = bcrypt.hashSync(password, 12); const hash = bcrypt.hashSync(password, 12);
const stmt = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)'); const stmt = db.prepare('INSERT INTO users (username, password_hash, display_name) VALUES (?, ?, ?)');
const result = stmt.run(username, hash); const result = stmt.run(username, hash, username);
return result.lastInsertRowid; return result.lastInsertRowid;
}, },
@@ -34,7 +34,7 @@ const Users = {
}, },
findById(id) { findById(id) {
return db.prepare('SELECT id, username, is_admin, is_organizer, created_at FROM users WHERE id = ?').get(id); return db.prepare('SELECT id, username, display_name, is_admin, is_organizer, created_at FROM users WHERE id = ?').get(id);
}, },
verifyPassword(user, password) { verifyPassword(user, password) {
@@ -58,6 +58,10 @@ const Users = {
db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(hash, userId); db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(hash, userId);
}, },
setDisplayName(userId, displayName) {
db.prepare('UPDATE users SET display_name = ? WHERE id = ?').run(displayName, userId);
},
createPasswordResetToken(userId) { createPasswordResetToken(userId) {
const token = crypto.randomBytes(32).toString('hex'); const token = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); // 24 hours const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); // 24 hours
@@ -80,7 +84,7 @@ const Users = {
}, },
getAllUsers() { getAllUsers() {
return db.prepare("SELECT id, username, is_admin, is_organizer, created_at FROM users WHERE username NOT LIKE '[deleted_%]' ORDER BY username ASC").all(); return db.prepare("SELECT id, username, COALESCE(display_name, username) as display_name, is_admin, is_organizer, created_at FROM users WHERE password_hash != '' ORDER BY COALESCE(display_name, username) ASC").all();
}, },
getTotalPoints(userId) { getTotalPoints(userId) {
@@ -139,9 +143,8 @@ const Users = {
}, },
deleteUser(userId) { deleteUser(userId) {
const placeholder = '[deleted_' + userId + ']'; db.prepare('UPDATE users SET display_name = ?, password_hash = ?, is_admin = 0, is_organizer = 0 WHERE id = ?')
db.prepare('UPDATE users SET username = ?, password_hash = ?, is_admin = 0, is_organizer = 0 WHERE id = ?') .run('[deleted]', '', userId);
.run(placeholder, '', userId);
db.prepare('UPDATE password_reset_tokens SET used = 1 WHERE user_id = ?').run(userId); db.prepare('UPDATE password_reset_tokens SET used = 1 WHERE user_id = ?').run(userId);
db.prepare("DELETE FROM sessions WHERE sess LIKE ?").run('%"userId":' + userId + '%'); db.prepare("DELETE FROM sessions WHERE sess LIKE ?").run('%"userId":' + userId + '%');
} }
@@ -184,7 +187,7 @@ const Hunts = {
}, },
getAll() { getAll() {
return db.prepare('SELECT h.*, u.username as creator_name FROM hunts h JOIN users u ON h.created_by = u.id ORDER BY h.created_at DESC').all(); return db.prepare('SELECT h.*, COALESCE(u.display_name, u.username) as creator_name FROM hunts h JOIN users u ON h.created_by = u.id ORDER BY h.created_at DESC').all();
}, },
getByCreator(userId) { getByCreator(userId) {
@@ -198,7 +201,7 @@ const Hunts = {
getLeaderboard(huntId, limit = null, offset = 0) { getLeaderboard(huntId, limit = null, offset = 0) {
let sql = ` let sql = `
SELECT u.id, u.username, SUM(s.points_awarded) as total_points, COUNT(s.id) as scans SELECT u.id, u.username, COALESCE(u.display_name, u.username) as display_name, SUM(s.points_awarded) as total_points, COUNT(s.id) as scans
FROM scans s FROM scans s
JOIN users u ON s.user_id = u.id JOIN users u ON s.user_id = u.id
JOIN packages p ON s.package_id = p.id JOIN packages p ON s.package_id = p.id
@@ -269,7 +272,7 @@ const Hunts = {
const totalPackages = db.prepare('SELECT package_count FROM hunts WHERE id = ?').get(huntId).package_count; const totalPackages = db.prepare('SELECT package_count FROM hunts WHERE id = ?').get(huntId).package_count;
const totalPoints = db.prepare('SELECT COALESCE(SUM(s.points_awarded), 0) as total FROM scans s JOIN packages p ON s.package_id = p.id WHERE p.hunt_id = ?').get(huntId).total; const totalPoints = db.prepare('SELECT COALESCE(SUM(s.points_awarded), 0) as total FROM scans s JOIN packages p ON s.package_id = p.id WHERE p.hunt_id = ?').get(huntId).total;
const topFinders = db.prepare(` const topFinders = db.prepare(`
SELECT u.username, SUM(s.points_awarded) as points, COUNT(s.id) as finds SELECT u.username, COALESCE(u.display_name, u.username) as display_name, SUM(s.points_awarded) as points, COUNT(s.id) as finds
FROM scans s FROM scans s
JOIN users u ON s.user_id = u.id JOIN users u ON s.user_id = u.id
JOIN packages p ON s.package_id = p.id JOIN packages p ON s.package_id = p.id
@@ -277,7 +280,7 @@ const Hunts = {
GROUP BY u.id ORDER BY points DESC LIMIT 5 GROUP BY u.id ORDER BY points DESC LIMIT 5
`).all(huntId); `).all(huntId);
const recentScans = db.prepare(` const recentScans = db.prepare(`
SELECT s.scanned_at, s.points_awarded, u.username, p.card_number SELECT s.scanned_at, s.points_awarded, u.username, COALESCE(u.display_name, u.username) as display_name, p.card_number
FROM scans s FROM scans s
JOIN users u ON s.user_id = u.id JOIN users u ON s.user_id = u.id
JOIN packages p ON s.package_id = p.id JOIN packages p ON s.package_id = p.id
@@ -315,8 +318,10 @@ const Packages = {
getByHunt(huntId) { getByHunt(huntId) {
return db.prepare(` return db.prepare(`
SELECT p.*, SELECT p.*,
u1.username as first_scanner_name, COALESCE(u1.display_name, u1.username) as first_scanner_name,
u2.username as last_scanner_name u1.username as first_scanner_username,
COALESCE(u2.display_name, u2.username) as last_scanner_name,
u2.username as last_scanner_username
FROM packages p FROM packages p
LEFT JOIN users u1 ON p.first_scanned_by = u1.id LEFT JOIN users u1 ON p.first_scanned_by = u1.id
LEFT JOIN users u2 ON p.last_scanned_by = u2.id LEFT JOIN users u2 ON p.last_scanned_by = u2.id
@@ -329,8 +334,10 @@ const Packages = {
return db.prepare(` return db.prepare(`
SELECT p.*, SELECT p.*,
h.name as hunt_name, h.short_name as hunt_short_name, h.id as hunt_id, h.name as hunt_name, h.short_name as hunt_short_name, h.id as hunt_id,
u1.username as first_scanner_name, COALESCE(u1.display_name, u1.username) as first_scanner_name,
u2.username as last_scanner_name u1.username as first_scanner_username,
COALESCE(u2.display_name, u2.username) as last_scanner_name,
u2.username as last_scanner_username
FROM packages p FROM packages p
JOIN hunts h ON p.hunt_id = h.id JOIN hunts h ON p.hunt_id = h.id
LEFT JOIN users u1 ON p.first_scanned_by = u1.id LEFT JOIN users u1 ON p.first_scanned_by = u1.id
@@ -341,7 +348,7 @@ const Packages = {
getScanHistory(packageId) { getScanHistory(packageId) {
return db.prepare(` return db.prepare(`
SELECT s.*, u.username SELECT s.*, u.username, COALESCE(u.display_name, u.username) as display_name
FROM scans s FROM scans s
JOIN users u ON s.user_id = u.id JOIN users u ON s.user_id = u.id
WHERE s.package_id = ? WHERE s.package_id = ?
@@ -410,7 +417,7 @@ const Scans = {
getGlobalLeaderboard(limit = null, offset = 0) { getGlobalLeaderboard(limit = null, offset = 0) {
let sql = ` let sql = `
SELECT u.id, u.username, SUM(s.points_awarded) as total_points, COUNT(s.id) as scans SELECT u.id, u.username, COALESCE(u.display_name, u.username) as display_name, SUM(s.points_awarded) as total_points, COUNT(s.id) as scans
FROM scans s FROM scans s
JOIN users u ON s.user_id = u.id JOIN users u ON s.user_id = u.id
WHERE s.points_awarded > 0 WHERE s.points_awarded > 0
@@ -429,7 +436,7 @@ const Scans = {
getRecentActivity(limit = 5) { getRecentActivity(limit = 5) {
return db.prepare(` return db.prepare(`
SELECT s.points_awarded, s.scanned_at, SELECT s.points_awarded, s.scanned_at,
u.username, u.username, COALESCE(u.display_name, u.username) as display_name,
p.card_number, p.card_number,
h.name as hunt_name, h.short_name as hunt_short_name, h.package_count h.name as hunt_name, h.short_name as hunt_short_name, h.package_count
FROM scans s FROM scans s
+3
View File
@@ -20,6 +20,7 @@ router.post('/login', (req, res) => {
req.session.userId = user.id; req.session.userId = user.id;
req.session.username = user.username; req.session.username = user.username;
req.session.displayName = user.display_name || user.username;
req.session.isAdmin = !!user.is_admin; req.session.isAdmin = !!user.is_admin;
req.session.isOrganizer = !!user.is_organizer; req.session.isOrganizer = !!user.is_organizer;
@@ -63,6 +64,7 @@ router.post('/register', (req, res) => {
const userId = Users.create(username, password); const userId = Users.create(username, password);
req.session.userId = userId; req.session.userId = userId;
req.session.username = username; req.session.username = username;
req.session.displayName = username;
req.session.isAdmin = false; req.session.isAdmin = false;
req.session.isOrganizer = false; req.session.isOrganizer = false;
req.session.flash = { type: 'success', message: 'Account created! Welcome to Loot Hunt.' }; req.session.flash = { type: 'success', message: 'Account created! Welcome to Loot Hunt.' };
@@ -113,6 +115,7 @@ router.post('/reset/:token', (req, res) => {
// Log the user in // Log the user in
req.session.userId = tokenRecord.user_id; req.session.userId = tokenRecord.user_id;
req.session.username = tokenRecord.username; req.session.username = tokenRecord.username;
req.session.displayName = tokenRecord.username;
req.session.isAdmin = false; // they can re-check on next load req.session.isAdmin = false; // they can re-check on next load
res.redirect('/'); res.redirect('/');
}); });
+19
View File
@@ -136,6 +136,25 @@ router.post('/player/:username/password', requireAuth, (req, res) => {
res.redirect(`/player/${user.username}`); res.redirect(`/player/${user.username}`);
}); });
// ─── Update display name ──────────────────────────────────
router.post('/player/:username/display-name', requireAuth, (req, res) => {
const user = Users.findByUsername(req.params.username);
if (!user || user.id !== req.session.userId) {
return res.status(403).render('error', { title: 'Forbidden', message: 'You can only change your own display name.' });
}
const displayName = (req.body.display_name || '').trim();
if (displayName.length < 2 || displayName.length > 32) {
req.session.flash = { type: 'danger', message: 'Display name must be 2-32 characters.' };
return res.redirect(`/player/${user.username}`);
}
Users.setDisplayName(user.id, displayName);
req.session.displayName = displayName;
req.session.flash = { type: 'success', message: 'Display name updated.' };
res.redirect(`/player/${user.username}`);
});
// ─── Delete own account ─────────────────────────────────── // ─── Delete own account ───────────────────────────────────
router.post('/player/:username/delete', requireAuth, (req, res) => { router.post('/player/:username/delete', requireAuth, (req, res) => {
const user = Users.findByUsername(req.params.username); const user = Users.findByUsername(req.params.username);
+2 -2
View File
@@ -44,7 +44,7 @@
<select name="username" class="form-control" required> <select name="username" class="form-control" required>
<option value="">Select user...</option> <option value="">Select user...</option>
<% if (typeof users !== 'undefined' && users) { users.forEach(u => { %> <% if (typeof users !== 'undefined' && users) { users.forEach(u => { %>
<option value="<%= u.username %>"><%= u.username %><%= u.is_admin ? ' (admin)' : u.is_organizer ? ' (organizer)' : '' %></option> <option value=\"<%= u.username %>\"><%= u.display_name %><%= u.is_admin ? ' (admin)' : u.is_organizer ? ' (organizer)' : '' %></option>
<% }); } %> <% }); } %>
</select> </select>
</div> </div>
@@ -74,7 +74,7 @@
<tbody> <tbody>
<% if (typeof users !== 'undefined' && users) { users.filter(u => !u.is_admin).forEach(u => { %> <% if (typeof users !== 'undefined' && users) { users.filter(u => !u.is_admin).forEach(u => { %>
<tr> <tr>
<td><a href="/player/<%= u.username %>"><%= u.username %></a></td> <td><a href="/player/<%= u.username %>"><%= u.display_name %></a></td>
<td><%= u.is_organizer ? 'Organizer' : 'Player' %></td> <td><%= u.is_organizer ? 'Organizer' : 'Player' %></td>
<td> <td>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;"> <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
+4 -4
View File
@@ -65,7 +65,7 @@
<tbody> <tbody>
<% stats.topFinders.forEach(f => { %> <% stats.topFinders.forEach(f => { %>
<tr> <tr>
<td><a href="/player/<%= f.username %>"><%= f.username %></a></td> <td><a href="/player/<%= f.username %>"><%= f.display_name %></a></td>
<td><%= f.finds %></td> <td><%= f.finds %></td>
<td><span class="points-badge">+<%= f.points %></span></td> <td><span class="points-badge">+<%= f.points %></span></td>
</tr> </tr>
@@ -84,7 +84,7 @@
<% stats.recentScans.forEach(s => { %> <% stats.recentScans.forEach(s => { %>
<tr> <tr>
<td><%= s.card_number %></td> <td><%= s.card_number %></td>
<td><a href="/player/<%= s.username %>"><%= s.username %></a></td> <td><a href="/player/<%= s.username %>"><%= s.display_name %></a></td>
<td><span class="points-badge">+<%= s.points_awarded %></span></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> <td style="font-size: 0.85rem; color: var(--muted);"><time datetime="<%= s.scanned_at %>"><%= new Date(s.scanned_at).toLocaleString() %></time></td>
</tr> </tr>
@@ -117,8 +117,8 @@
<td class="hide-mobile"><strong><%= pkg.card_number %></strong></td> <td class="hide-mobile"><strong><%= pkg.card_number %></strong></td>
<td style="font-family: monospace;"><%= pkg.unique_code %></td> <td style="font-family: monospace;"><%= pkg.unique_code %></td>
<td class="hide-mobile"><%= pkg.scan_count %></td> <td class="hide-mobile"><%= pkg.scan_count %></td>
<td class="hide-mobile"><% if (pkg.first_scanner_name) { %><a href="/player/<%= pkg.first_scanner_name %>"><%= pkg.first_scanner_name %></a><% } else { %>---<% } %></td> <td class="hide-mobile"><% if (pkg.first_scanner_name) { %><a href="/player/<%= pkg.first_scanner_username %>"><%= pkg.first_scanner_name %></a><% } else { %>---<% } %></td>
<td><% if (pkg.last_scanner_name) { %><a href="/player/<%= pkg.last_scanner_name %>"><%= pkg.last_scanner_name %></a><% } else { %>---<% } %></td> <td><% if (pkg.last_scanner_name) { %><a href="/player/<%= pkg.last_scanner_username %>"><%= pkg.last_scanner_name %></a><% } else { %>---<% } %></td>
<td> <td>
<div style="display: flex; gap: 0.4rem; align-items: stretch;"> <div style="display: flex; gap: 0.4rem; align-items: stretch;">
<a href="/hunt/<%= hunt.short_name %>/<%= pkg.card_number %>" class="btn btn-sm btn-outline">View</a> <a href="/hunt/<%= hunt.short_name %>/<%= pkg.card_number %>" class="btn btn-sm btn-outline">View</a>
+1 -1
View File
@@ -74,7 +74,7 @@
<div style="display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid #eee;"> <div style="display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid #eee;">
<span class="points-badge" style="font-size: 0.85rem; padding: 0.25rem 0.6rem;">+<%= a.points_awarded %></span> <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="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-weight: 600;"><a href="/player/<%= a.username %>" style="text-decoration: none; color: inherit;"><%= a.display_name %></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);"><time datetime="<%= a.scanned_at %>"><%= new Date(a.scanned_at).toLocaleString() %></time></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>
</div> </div>
+1 -1
View File
@@ -28,7 +28,7 @@
<% const rank = (typeof offset !== 'undefined' ? offset : 0) + i + 1; %> <% const rank = (typeof offset !== 'undefined' ? offset : 0) + i + 1; %>
<tr> <tr>
<td class="rank-cell rank-<%= rank %>"><%= rank %></td> <td class="rank-cell rank-<%= rank %>"><%= rank %></td>
<td><strong><a href="/player/<%= entry.username %>"><%= entry.username %></a></strong></td> <td><strong><a href="/player/<%= entry.username %>"><%= entry.display_name %></a></strong></td>
<td><span class="points-badge"><%= entry.total_points %></span></td> <td><span class="points-badge"><%= entry.total_points %></span></td>
<td><%= entry.scans %></td> <td><%= entry.scans %></td>
</tr> </tr>
+1 -1
View File
@@ -26,7 +26,7 @@
<td class="rank-cell rank-<%= rank %>"> <td class="rank-cell rank-<%= rank %>">
<% if (rank === 1) { %>&#x1F947;<% } else if (rank === 2) { %>&#x1F948;<% } else if (rank === 3) { %>&#x1F949;<% } else { %><%= rank %><% } %> <% if (rank === 1) { %>&#x1F947;<% } else if (rank === 2) { %>&#x1F948;<% } else if (rank === 3) { %>&#x1F949;<% } else { %><%= rank %><% } %>
</td> </td>
<td><strong><a href="/player/<%= entry.username %>"><%= entry.username %></a></strong></td> <td><strong><a href="/player/<%= entry.username %>"><%= entry.display_name %></a></strong></td>
<td><span class="points-badge"><%= entry.total_points %></span></td> <td><span class="points-badge"><%= entry.total_points %></span></td>
<td><%= entry.scans %></td> <td><%= entry.scans %></td>
</tr> </tr>
+4 -4
View File
@@ -17,11 +17,11 @@
<div class="label">Total Scans</div> <div class="label">Total Scans</div>
</div> </div>
<div class="stat-box"> <div class="stat-box">
<div class="value"><% if (pkg.first_scanner_name) { %><a href="/player/<%= pkg.first_scanner_name %>"><%= pkg.first_scanner_name %></a><% } else { %>---<% } %></div> <div class="value"><% if (pkg.first_scanner_name) { %><a href="/player/<%= pkg.first_scanner_username %>"><%= pkg.first_scanner_name %></a><% } else { %>---<% } %></div>
<div class="label">First Finder</div> <div class="label">First Finder</div>
</div> </div>
<div class="stat-box"> <div class="stat-box">
<div class="value"><% if (pkg.last_scanner_name) { %><a href="/player/<%= pkg.last_scanner_name %>"><%= pkg.last_scanner_name %></a><% } else { %>---<% } %></div> <div class="value"><% if (pkg.last_scanner_name) { %><a href="/player/<%= pkg.last_scanner_username %>"><%= pkg.last_scanner_name %></a><% } else { %>---<% } %></div>
<div class="label">Most Recent</div> <div class="label">Most Recent</div>
</div> </div>
</div> </div>
@@ -67,7 +67,7 @@
<div class="card-header">&#x1F4AC; Package Hint</div> <div class="card-header">&#x1F4AC; Package Hint</div>
<% if (pkg.last_scan_hint) { %> <% if (pkg.last_scan_hint) { %>
<p style="font-style: italic; font-size: 1.05rem;">"<%= pkg.last_scan_hint %>"</p> <p style="font-style: italic; font-size: 1.05rem;">"<%= pkg.last_scan_hint %>"</p>
<p style="font-size: 0.8rem; color: var(--muted);">Left by <a href="/player/<%= pkg.last_scanner_name %>"><%= pkg.last_scanner_name %></a></p> <p style="font-size: 0.8rem; color: var(--muted);">Left by <a href="/player/<%= pkg.last_scanner_username %>"><%= pkg.last_scanner_name %></a></p>
<% } else { %> <% } else { %>
<p style="color: var(--muted);">No hint has been left yet.</p> <p style="color: var(--muted);">No hint has been left yet.</p>
<% } %> <% } %>
@@ -105,7 +105,7 @@
<% scanHistory.forEach((scan, i) => { %> <% scanHistory.forEach((scan, i) => { %>
<tr> <tr>
<td><%= i + 1 %></td> <td><%= i + 1 %></td>
<td><a href="/player/<%= scan.username %>"><%= scan.username %></a></td> <td><a href="/player/<%= scan.username %>"><%= scan.display_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><% 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);"><time datetime="<%= scan.scanned_at %>"><%= new Date(scan.scanned_at).toLocaleString() %></time></td> <td style="font-size: 0.85rem; color: var(--muted);"><time datetime="<%= scan.scanned_at %>"><%= new Date(scan.scanned_at).toLocaleString() %></time></td>
</tr> </tr>
+4 -4
View File
@@ -31,11 +31,11 @@
<div class="label">Total Scans</div> <div class="label">Total Scans</div>
</div> </div>
<div class="stat-box"> <div class="stat-box">
<div class="value"><% if (pkg.first_scanner_name) { %><a href="/player/<%= pkg.first_scanner_name %>"><%= pkg.first_scanner_name %></a><% } else { %>---<% } %></div> <div class="value"><% if (pkg.first_scanner_name) { %><a href="/player/<%= pkg.first_scanner_username %>"><%= pkg.first_scanner_name %></a><% } else { %>---<% } %></div>
<div class="label">First Finder</div> <div class="label">First Finder</div>
</div> </div>
<div class="stat-box"> <div class="stat-box">
<div class="value"><% if (pkg.last_scanner_name) { %><a href="/player/<%= pkg.last_scanner_name %>"><%= pkg.last_scanner_name %></a><% } else { %>---<% } %></div> <div class="value"><% if (pkg.last_scanner_name) { %><a href="/player/<%= pkg.last_scanner_username %>"><%= pkg.last_scanner_name %></a><% } else { %>---<% } %></div>
<div class="label">Most Recent</div> <div class="label">Most Recent</div>
</div> </div>
</div> </div>
@@ -82,7 +82,7 @@
<div class="card-header">&#x1F4AC; Package Hint</div> <div class="card-header">&#x1F4AC; Package Hint</div>
<% if (pkg.last_scan_hint) { %> <% if (pkg.last_scan_hint) { %>
<p style="font-style: italic; font-size: 1.05rem;">"<%= pkg.last_scan_hint %>"</p> <p style="font-style: italic; font-size: 1.05rem;">"<%= pkg.last_scan_hint %>"</p>
<p style="font-size: 0.8rem; color: var(--muted);">Left by <a href="/player/<%= pkg.last_scanner_name %>"><%= pkg.last_scanner_name %></a></p> <p style="font-size: 0.8rem; color: var(--muted);">Left by <a href="/player/<%= pkg.last_scanner_username %>"><%= pkg.last_scanner_name %></a></p>
<% } else { %> <% } else { %>
<p style="color: var(--muted);">No hint has been left yet.</p> <p style="color: var(--muted);">No hint has been left yet.</p>
<% } %> <% } %>
@@ -121,7 +121,7 @@
<% scanHistory.forEach((scan, i) => { %> <% scanHistory.forEach((scan, i) => { %>
<tr> <tr>
<td><%= i + 1 %></td> <td><%= i + 1 %></td>
<td><a href="/player/<%= scan.username %>"><%= scan.username %></a></td> <td><a href="/player/<%= scan.username %>"><%= scan.display_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><% 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);"><time datetime="<%= scan.scanned_at %>"><%= new Date(scan.scanned_at).toLocaleString() %></time></td> <td style="font-size: 0.85rem; color: var(--muted);"><time datetime="<%= scan.scanned_at %>"><%= new Date(scan.scanned_at).toLocaleString() %></time></td>
</tr> </tr>
+1 -1
View File
@@ -35,7 +35,7 @@
<% } else if (currentUser.isOrganizer) { %> <% } else if (currentUser.isOrganizer) { %>
<li><a href="/admin">Organizer</a></li> <li><a href="/admin">Organizer</a></li>
<% } %> <% } %>
<li><a href="/auth/logout">Logout (<%= currentUser.username %>)</a></li> <li><a href="/auth/logout">Logout (<%= currentUser.displayName %>)</a></li>
<% } else { %> <% } else { %>
<li><a href="/auth/login">Login</a></li> <li><a href="/auth/login">Login</a></li>
<li><a href="/auth/register">Register</a></li> <li><a href="/auth/register">Register</a></li>
+13 -2
View File
@@ -2,8 +2,8 @@
<div class="container"> <div class="container">
<div style="text-align: center; margin-bottom: 1.5rem;"> <div style="text-align: center; margin-bottom: 1.5rem;">
<h1 style="margin-bottom: 0.25rem;">&#x1F464; <%= profile.username %></h1> <h1 style="margin-bottom: 0.25rem;">&#x1F464; <%= profile.display_name || profile.username %></h1>
<p style="color: var(--muted); margin: 0;">Joined <%= new Date(profile.created_at).toLocaleDateString() %></p> <p style="color: var(--muted); margin: 0;"><%= profile.username %> &middot; Joined <%= new Date(profile.created_at).toLocaleDateString() %></p>
</div> </div>
<div class="stats-row"> <div class="stats-row">
@@ -88,6 +88,17 @@
</div> </div>
<% if (typeof isOwnProfile !== 'undefined' && isOwnProfile) { %> <% if (typeof isOwnProfile !== 'undefined' && isOwnProfile) { %>
<div class="card" style="margin-top: 1.5rem;">
<div class="card-header">&#x270F;&#xFE0F; Display Name</div>
<p style="color: var(--muted); font-size: 0.9rem;">This is the name shown on leaderboards, scan history, and your profile. Your username (<strong><%= profile.username %></strong>) is used for login and URLs.</p>
<form method="POST" action="/player/<%= profile.username %>/display-name">
<div class="form-group">
<input type="text" name="display_name" class="form-control" value="<%= profile.display_name || profile.username %>" required maxlength="32" minlength="2" placeholder="Display Name">
</div>
<button type="submit" class="btn btn-primary btn-sm">Update Display Name</button>
</form>
</div>
<div class="card" style="margin-top: 1.5rem;"> <div class="card" style="margin-top: 1.5rem;">
<div class="card-header">&#x1F512; Change Password</div> <div class="card-header">&#x1F512; Change Password</div>
<form method="POST" action="/player/<%= profile.username %>/password"> <form method="POST" action="/player/<%= profile.username %>/password">