This commit is contained in:
+76
-5
@@ -52,6 +52,26 @@ body {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hamburger toggle — hidden on desktop */
|
||||||
|
.nav-toggle {
|
||||||
|
display: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-toggle span {
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 2px;
|
||||||
|
background: #b2bec3;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-nav {
|
.navbar-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -488,30 +508,59 @@ tr:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Responsive ──────────────────────────────────────── */
|
/* ─── Responsive ──────────────────────────────────────── */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 700px) {
|
||||||
|
/* Hamburger visible on mobile */
|
||||||
|
.nav-toggle {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.75rem;
|
||||||
height: 52px;
|
height: 52px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dropdown menu */
|
||||||
.navbar-nav {
|
.navbar-nav {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--darker);
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.08);
|
||||||
|
padding: 0.5rem 0;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-nav a {
|
.navbar-nav.open {
|
||||||
padding: 0.4rem 0.5rem;
|
display: flex;
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-nav li {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav a {
|
||||||
|
display: block;
|
||||||
|
padding: 0.7rem 1rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav a:hover {
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
.container,
|
.container,
|
||||||
.container-narrow {
|
.container-narrow {
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hero */
|
||||||
.hero {
|
.hero {
|
||||||
padding: 1.5rem 0.5rem;
|
padding: 1.5rem 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -524,6 +573,7 @@ tr:hover {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
.card {
|
.card {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -533,6 +583,7 @@ tr:hover {
|
|||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Stats */
|
||||||
.stats-row {
|
.stats-row {
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -550,6 +601,7 @@ tr:hover {
|
|||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Package grid */
|
||||||
.package-grid {
|
.package-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
@@ -563,6 +615,7 @@ tr:hover {
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hunt cards */
|
||||||
.hunt-card {
|
.hunt-card {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -570,6 +623,7 @@ tr:hover {
|
|||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scan result */
|
||||||
.scan-result h1 {
|
.scan-result h1 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
@@ -587,11 +641,13 @@ tr:hover {
|
|||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
.btn {
|
.btn {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tables — horizontal scroll on mobile */
|
||||||
table {
|
table {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
@@ -605,8 +661,23 @@ tr:hover {
|
|||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
.footer {
|
.footer {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Touch-friendly form controls */
|
||||||
|
.form-control {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.7rem 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,48 @@ const Users = {
|
|||||||
const totalPoints = this.getTotalPoints(userId);
|
const totalPoints = this.getTotalPoints(userId);
|
||||||
const scanCount = db.prepare('SELECT COUNT(*) as count FROM scans WHERE user_id = ? AND points_awarded > 0').get(userId).count;
|
const scanCount = db.prepare('SELECT COUNT(*) as count FROM scans WHERE user_id = ? AND points_awarded > 0').get(userId).count;
|
||||||
return { ...user, totalPoints, scanCount };
|
return { ...user, totalPoints, scanCount };
|
||||||
|
},
|
||||||
|
|
||||||
|
getRecentScans(userId, limit = 20) {
|
||||||
|
return db.prepare(`
|
||||||
|
SELECT s.points_awarded, s.scanned_at,
|
||||||
|
p.card_number, p.unique_code,
|
||||||
|
h.name as hunt_name, h.short_name as hunt_short_name, h.package_count
|
||||||
|
FROM scans s
|
||||||
|
JOIN packages p ON s.package_id = p.id
|
||||||
|
JOIN hunts h ON p.hunt_id = h.id
|
||||||
|
WHERE s.user_id = ?
|
||||||
|
ORDER BY s.scanned_at DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(userId, limit);
|
||||||
|
},
|
||||||
|
|
||||||
|
getHuntBreakdown(userId) {
|
||||||
|
return db.prepare(`
|
||||||
|
SELECT h.name as hunt_name, h.short_name as hunt_short_name,
|
||||||
|
COUNT(s.id) as scans, SUM(s.points_awarded) as points
|
||||||
|
FROM scans s
|
||||||
|
JOIN packages p ON s.package_id = p.id
|
||||||
|
JOIN hunts h ON p.hunt_id = h.id
|
||||||
|
WHERE s.user_id = ? AND s.points_awarded > 0
|
||||||
|
GROUP BY h.id
|
||||||
|
ORDER BY points DESC
|
||||||
|
`).all(userId);
|
||||||
|
},
|
||||||
|
|
||||||
|
getRank(userId) {
|
||||||
|
const rows = db.prepare(`
|
||||||
|
SELECT user_id, SUM(points_awarded) as total
|
||||||
|
FROM scans WHERE points_awarded > 0
|
||||||
|
GROUP BY user_id
|
||||||
|
ORDER BY total DESC
|
||||||
|
`).all();
|
||||||
|
const idx = rows.findIndex(r => r.user_id === userId);
|
||||||
|
return idx >= 0 ? idx + 1 : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
getTotalPlayerCount() {
|
||||||
|
return db.prepare('SELECT COUNT(DISTINCT user_id) as count FROM scans WHERE points_awarded > 0').get().count;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+24
-1
@@ -1,6 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { Hunts, Packages, Scans } = require('../models');
|
const { Hunts, Packages, Scans, Users } = require('../models');
|
||||||
|
|
||||||
// ─── Hunt profile ─────────────────────────────────────────
|
// ─── Hunt profile ─────────────────────────────────────────
|
||||||
router.get('/hunt/:shortName', (req, res) => {
|
router.get('/hunt/:shortName', (req, res) => {
|
||||||
@@ -59,6 +59,29 @@ router.get('/hunt/:shortName/:cardNumber', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── User profile ─────────────────────────────────────────
|
||||||
|
router.get('/player/:username', (req, res) => {
|
||||||
|
const user = Users.findByUsername(req.params.username);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).render('error', { title: 'Not Found', message: 'Player not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = Users.getProfile(user.id);
|
||||||
|
const recentScans = Users.getRecentScans(user.id);
|
||||||
|
const huntBreakdown = Users.getHuntBreakdown(user.id);
|
||||||
|
const rank = Users.getRank(user.id);
|
||||||
|
const totalPlayers = Users.getTotalPlayerCount();
|
||||||
|
|
||||||
|
res.render('player/profile', {
|
||||||
|
title: `${user.username}'s Profile`,
|
||||||
|
profile,
|
||||||
|
recentScans,
|
||||||
|
huntBreakdown,
|
||||||
|
rank,
|
||||||
|
totalPlayers
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ─── Browse all hunts ─────────────────────────────────────
|
// ─── Browse all hunts ─────────────────────────────────────
|
||||||
router.get('/hunts', (req, res) => {
|
router.get('/hunts', (req, res) => {
|
||||||
const hunts = Hunts.getAll();
|
const hunts = Hunts.getAll();
|
||||||
|
|||||||
+11
-13
@@ -68,13 +68,13 @@ async function generateHuntPDF(hunt, packages, baseUrl, outputStream) {
|
|||||||
const textBlockH = 64;
|
const textBlockH = 64;
|
||||||
const textY = y + (cardH - textBlockH) / 2;
|
const textY = y + (cardH - textBlockH) / 2;
|
||||||
|
|
||||||
doc.fontSize(18).font('Courier-Bold').fillColor('#1a1a1a');
|
doc.fontSize(18).font('Helvetica-Bold').fillColor('#1a1a1a');
|
||||||
doc.text(`${pkg.card_number} of ${totalPackages}`, textX, textY, { width: textW, align: 'center' });
|
doc.text(`${pkg.card_number} of ${totalPackages}`, textX, textY, { width: textW, align: 'center' });
|
||||||
|
|
||||||
doc.fontSize(12).font('Courier').fillColor('#666666');
|
doc.fontSize(12).font('Helvetica').fillColor('#666666');
|
||||||
doc.text(hunt.short_name, textX, textY + 24, { width: textW, align: 'center' });
|
doc.text(hunt.short_name, textX, textY + 24, { width: textW, align: 'center' });
|
||||||
|
|
||||||
doc.fontSize(14).font('Courier-Bold').fillColor('#333333');
|
doc.fontSize(14).font('Helvetica-Bold').fillColor('#333333');
|
||||||
doc.text(pkg.unique_code, textX, textY + 44, { width: textW, align: 'center' });
|
doc.text(pkg.unique_code, textX, textY + 44, { width: textW, align: 'center' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,15 +84,13 @@ async function generateHuntPDF(hunt, packages, baseUrl, outputStream) {
|
|||||||
const backLines = [
|
const backLines = [
|
||||||
'THIS CARD IS UNIQUE',
|
'THIS CARD IS UNIQUE',
|
||||||
'',
|
'',
|
||||||
'IF YOU DO NOT USE IT',
|
'IF YOU DO NOT USE IT PLEASE',
|
||||||
'PLEASE PUT IT SOMEWHERE',
|
'PUT IT SOMEWHERE INTERESTING',
|
||||||
'INTERESTING FOR THE',
|
'FOR THE NEXT PERSON TO FIND',
|
||||||
'NEXT PERSON TO FIND',
|
|
||||||
'',
|
'',
|
||||||
'PLEASE DO NOT',
|
'PLEASE DO NOT THROW ME AWAY'
|
||||||
'THROW ME AWAY'
|
|
||||||
];
|
];
|
||||||
const lineHeight = 11;
|
const lineHeight = 16;
|
||||||
const blockH = backLines.length * lineHeight;
|
const blockH = backLines.length * lineHeight;
|
||||||
|
|
||||||
for (let c = 0; c < pagePackages.length; c++) {
|
for (let c = 0; c < pagePackages.length; c++) {
|
||||||
@@ -105,10 +103,10 @@ async function generateHuntPDF(hunt, packages, baseUrl, outputStream) {
|
|||||||
|
|
||||||
const startY = y + (cardH - blockH) / 2;
|
const startY = y + (cardH - blockH) / 2;
|
||||||
|
|
||||||
doc.font('Courier-Bold').fontSize(9).fillColor('#333333');
|
doc.font('Courier-Bold').fontSize(12).fillColor('#333333');
|
||||||
backLines.forEach((line, li) => {
|
backLines.forEach((line, li) => {
|
||||||
doc.text(line, x, startY + li * lineHeight, {
|
doc.text(line, x + 10, startY + li * lineHeight, {
|
||||||
width: cardW,
|
width: cardW - 20,
|
||||||
align: 'center'
|
align: 'center'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,8 +58,8 @@
|
|||||||
<td><strong><%= pkg.card_number %></strong></td>
|
<td><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><%= pkg.scan_count %></td>
|
<td><%= pkg.scan_count %></td>
|
||||||
<td><%= pkg.first_scanner_name || '—' %></td>
|
<td><% if (pkg.first_scanner_name) { %><a href="/player/<%= pkg.first_scanner_name %>"><%= pkg.first_scanner_name %></a><% } else { %>---<% } %></td>
|
||||||
<td><%= pkg.last_scanner_name || '—' %></td>
|
<td><% if (pkg.last_scanner_name) { %><a href="/player/<%= pkg.last_scanner_name %>"><%= pkg.last_scanner_name %></a><% } else { %>---<% } %></td>
|
||||||
<td>
|
<td>
|
||||||
<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>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<% leaderboard.forEach((entry, i) => { %>
|
<% leaderboard.forEach((entry, i) => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="rank-cell rank-<%= i + 1 %>"><%= i + 1 %></td>
|
<td class="rank-cell rank-<%= i + 1 %>"><%= i + 1 %></td>
|
||||||
<td><strong><%= entry.username %></strong></td>
|
<td><strong><a href="/player/<%= entry.username %>"><%= entry.username %></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>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<td class="rank-cell rank-<%= i + 1 %>">
|
<td class="rank-cell rank-<%= i + 1 %>">
|
||||||
<% if (i === 0) { %>🥇<% } else if (i === 1) { %>🥈<% } else if (i === 2) { %>🥉<% } else { %><%= i + 1 %><% } %>
|
<% if (i === 0) { %>🥇<% } else if (i === 1) { %>🥈<% } else if (i === 2) { %>🥉<% } else { %><%= i + 1 %><% } %>
|
||||||
</td>
|
</td>
|
||||||
<td><strong><%= entry.username %></strong></td>
|
<td><strong><a href="/player/<%= entry.username %>"><%= entry.username %></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>
|
||||||
|
|||||||
@@ -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"><%= pkg.first_scanner_name || '---' %></div>
|
<div class="value"><% if (pkg.first_scanner_name) { %><a href="/player/<%= pkg.first_scanner_name %>"><%= 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"><%= pkg.last_scanner_name || '---' %></div>
|
<div class="value"><% if (pkg.last_scanner_name) { %><a href="/player/<%= pkg.last_scanner_name %>"><%= pkg.last_scanner_name %></a><% } else { %>---<% } %></div>
|
||||||
<div class="label">Most Recent</div>
|
<div class="label">Most Recent</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
<div class="card-header">💬 Package Hint</div>
|
<div class="card-header">💬 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 <%= pkg.last_scanner_name %></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>
|
||||||
<% } 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>
|
||||||
<% } %>
|
<% } %>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<% scanHistory.forEach((scan, i) => { %>
|
<% scanHistory.forEach((scan, i) => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= i + 1 %></td>
|
<td><%= i + 1 %></td>
|
||||||
<td><%= scan.username %></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><% 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);"><%= new Date(scan.scanned_at).toLocaleString() %></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -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"><%= pkg.first_scanner_name || '---' %></div>
|
<div class="value"><% if (pkg.first_scanner_name) { %><a href="/player/<%= pkg.first_scanner_name %>"><%= 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"><%= pkg.last_scanner_name || '---' %></div>
|
<div class="value"><% if (pkg.last_scanner_name) { %><a href="/player/<%= pkg.last_scanner_name %>"><%= 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">💬 Package Hint</div>
|
<div class="card-header">💬 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 <%= pkg.last_scanner_name %></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>
|
||||||
<% } 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>
|
||||||
<% } %>
|
<% } %>
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
<% scanHistory.forEach((scan, i) => { %>
|
<% scanHistory.forEach((scan, i) => { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= i + 1 %></td>
|
<td><%= i + 1 %></td>
|
||||||
<td><%= scan.username %></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><% 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);"><%= new Date(scan.scanned_at).toLocaleString() %></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -9,10 +9,14 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<a href="/" class="navbar-brand">🎯 Loot Hunt</a>
|
<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>
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li><a href="/hunts">Hunts</a></li>
|
<li><a href="/hunts">Hunts</a></li>
|
||||||
<li><a href="/leaderboard">Leaderboard</a></li>
|
<li><a href="/leaderboard">Leaderboard</a></li>
|
||||||
<% if (currentUser) { %>
|
<% if (currentUser) { %>
|
||||||
|
<li><a href="/player/<%= currentUser.username %>">My Profile</a></li>
|
||||||
<% if (currentUser.isAdmin) { %>
|
<% if (currentUser.isAdmin) { %>
|
||||||
<li><a href="/admin">Admin</a></li>
|
<li><a href="/admin">Admin</a></li>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<%- include('../partials/header') %>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div style="text-align: center; margin-bottom: 1.5rem;">
|
||||||
|
<h1 style="margin-bottom: 0.25rem;">👤 <%= profile.username %></h1>
|
||||||
|
<p style="color: var(--muted); margin: 0;">Joined <%= new Date(profile.created_at).toLocaleDateString() %></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-row">
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="value"><%= profile.totalPoints %></div>
|
||||||
|
<div class="label">Total Points</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="value"><%= profile.scanCount %></div>
|
||||||
|
<div class="label">Finds</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="value"><%= rank ? '#' + rank : '---' %></div>
|
||||||
|
<div class="label">Rank<% if (totalPlayers) { %> / <%= totalPlayers %><% } %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (huntBreakdown.length > 0) { %>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Hunts Participated</div>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Hunt</th>
|
||||||
|
<th>Finds</th>
|
||||||
|
<th>Points</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% huntBreakdown.forEach(h => { %>
|
||||||
|
<tr>
|
||||||
|
<td><a href="/hunt/<%= h.hunt_short_name %>"><%= h.hunt_name %></a></td>
|
||||||
|
<td><%= h.scans %></td>
|
||||||
|
<td><span class="points-badge">+<%= h.points %></span></td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (recentScans.length > 0) { %>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Recent Activity</div>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Package</th>
|
||||||
|
<th>Hunt</th>
|
||||||
|
<th>Points</th>
|
||||||
|
<th>When</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% recentScans.forEach(scan => { %>
|
||||||
|
<tr>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="card" style="text-align: center; color: var(--muted); padding: 2rem;">
|
||||||
|
No scans yet. Get out there and find some loot!
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 1rem;">
|
||||||
|
<a href="/leaderboard" class="btn btn-outline">Global Leaderboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
Reference in New Issue
Block a user