allow for a starting date and hidden hunt if it hasnt started
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 31s
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 31s
This commit is contained in:
@@ -2,18 +2,23 @@
|
|||||||
(function () {
|
(function () {
|
||||||
function timeAgo(date) {
|
function timeAgo(date) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const seconds = Math.floor((now - date) / 1000);
|
const diff = now - date;
|
||||||
if (seconds < 5) return 'just now';
|
const seconds = Math.floor(Math.abs(diff) / 1000);
|
||||||
if (seconds < 60) return seconds + 's ago';
|
const isFuture = diff < 0;
|
||||||
|
const prefix = isFuture ? 'in ' : '';
|
||||||
|
const suffix = isFuture ? '' : ' ago';
|
||||||
|
|
||||||
|
if (seconds < 5) return isFuture ? 'in a moment' : 'just now';
|
||||||
|
if (seconds < 60) return prefix + seconds + 's' + suffix;
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
if (minutes < 60) return minutes + 'm ago';
|
if (minutes < 60) return prefix + minutes + 'm' + suffix;
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
if (hours < 24) return hours + 'h ago';
|
if (hours < 24) return prefix + hours + 'h' + suffix;
|
||||||
const days = Math.floor(hours / 24);
|
const days = Math.floor(hours / 24);
|
||||||
if (days < 7) return days + 'd ago';
|
if (days < 7) return prefix + days + 'd' + suffix;
|
||||||
if (days < 30) return Math.floor(days / 7) + 'w ago';
|
if (days < 30) return prefix + Math.floor(days / 7) + 'w' + suffix;
|
||||||
if (days < 365) return Math.floor(days / 30) + 'mo ago';
|
if (days < 365) return prefix + Math.floor(days / 30) + 'mo' + suffix;
|
||||||
return Math.floor(days / 365) + 'y ago';
|
return prefix + Math.floor(days / 365) + 'y' + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTimes() {
|
function updateTimes() {
|
||||||
|
|||||||
@@ -129,7 +129,8 @@ async function start() {
|
|||||||
// Home page
|
// Home page
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
const { Hunts, Scans } = require('./models');
|
const { Hunts, Scans } = require('./models');
|
||||||
const hunts = Hunts.getAll();
|
const allHunts = Hunts.getAll();
|
||||||
|
const hunts = allHunts.filter(h => !Hunts.isHidden(h));
|
||||||
const recentActivity = Scans.getRecentActivity(5);
|
const recentActivity = Scans.getRecentActivity(5);
|
||||||
res.render('home', { title: 'Loot Hunt', hunts, recentActivity });
|
res.render('home', { title: 'Loot Hunt', hunts, recentActivity });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -224,7 +224,9 @@ const ready = new Promise(resolve => { _readyResolve = resolve; });
|
|||||||
|
|
||||||
// Migrations — add columns to existing databases
|
// Migrations — add columns to existing databases
|
||||||
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 hidden_until_start INTEGER DEFAULT 0'
|
||||||
];
|
];
|
||||||
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 */ }
|
||||||
|
|||||||
@@ -141,11 +141,11 @@ const Users = {
|
|||||||
|
|
||||||
// ─── Hunts ────────────────────────────────────────────────
|
// ─── Hunts ────────────────────────────────────────────────
|
||||||
const Hunts = {
|
const Hunts = {
|
||||||
create(name, shortName, description, packageCount, expiryDate, createdBy) {
|
create(name, shortName, description, packageCount, expiryDate, createdBy, startDate, hiddenUntilStart) {
|
||||||
const stmt = db.prepare(
|
const stmt = db.prepare(
|
||||||
'INSERT INTO hunts (name, short_name, description, package_count, expiry_date, created_by) VALUES (?, ?, ?, ?, ?, ?)'
|
'INSERT INTO hunts (name, short_name, description, package_count, expiry_date, created_by, start_date, hidden_until_start) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
||||||
);
|
);
|
||||||
const result = stmt.run(name, shortName.toUpperCase(), description, packageCount, expiryDate || null, createdBy);
|
const result = stmt.run(name, shortName.toUpperCase(), description, packageCount, expiryDate || null, createdBy, startDate || null, hiddenUntilStart ? 1 : 0);
|
||||||
const huntId = result.lastInsertRowid;
|
const huntId = result.lastInsertRowid;
|
||||||
|
|
||||||
// Generate packages
|
// Generate packages
|
||||||
@@ -217,9 +217,19 @@ const Hunts = {
|
|||||||
return !!row;
|
return !!row;
|
||||||
},
|
},
|
||||||
|
|
||||||
update(id, name, description, expiryDate) {
|
update(id, name, description, expiryDate, startDate, hiddenUntilStart) {
|
||||||
db.prepare('UPDATE hunts SET name = ?, description = ?, expiry_date = ? WHERE id = ?')
|
db.prepare('UPDATE hunts SET name = ?, description = ?, expiry_date = ?, start_date = ?, hidden_until_start = ? WHERE id = ?')
|
||||||
.run(name, description, expiryDate || null, id);
|
.run(name, description, expiryDate || null, startDate || null, hiddenUntilStart ? 1 : 0, id);
|
||||||
|
},
|
||||||
|
|
||||||
|
isHidden(hunt) {
|
||||||
|
if (!hunt.hidden_until_start || !hunt.start_date) return false;
|
||||||
|
return new Date(hunt.start_date + 'Z') > new Date();
|
||||||
|
},
|
||||||
|
|
||||||
|
hasStarted(hunt) {
|
||||||
|
if (!hunt.start_date) return true;
|
||||||
|
return new Date(hunt.start_date + 'Z') <= new Date();
|
||||||
},
|
},
|
||||||
|
|
||||||
resetScans(id) {
|
resetScans(id) {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ router.get('/hunts/new', (req, res) => {
|
|||||||
|
|
||||||
// Create hunt
|
// Create hunt
|
||||||
router.post('/hunts', (req, res) => {
|
router.post('/hunts', (req, res) => {
|
||||||
const { name, short_name, description, package_count, expiry_date } = req.body;
|
const { name, short_name, description, package_count, expiry_date, start_date, hidden_until_start } = req.body;
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!name || !short_name || !package_count) {
|
if (!name || !short_name || !package_count) {
|
||||||
@@ -70,7 +70,7 @@ router.post('/hunts', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const huntId = Hunts.create(name, shortName, description, count, expiry_date, req.session.userId);
|
const huntId = Hunts.create(name, shortName, description, count, expiry_date, req.session.userId, start_date, hidden_until_start);
|
||||||
req.session.flash = { type: 'success', message: `Hunt "${name}" created with ${count} packages.` };
|
req.session.flash = { type: 'success', message: `Hunt "${name}" created with ${count} packages.` };
|
||||||
res.redirect(`/admin/hunts/${huntId}`);
|
res.redirect(`/admin/hunts/${huntId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -102,12 +102,12 @@ router.get('/hunts/:id/edit', requireHuntAccess, (req, res) => {
|
|||||||
router.post('/hunts/:id/edit', requireHuntAccess, (req, res) => {
|
router.post('/hunts/:id/edit', requireHuntAccess, (req, res) => {
|
||||||
const hunt = req.hunt;
|
const hunt = req.hunt;
|
||||||
|
|
||||||
const { name, description, expiry_date } = req.body;
|
const { name, description, expiry_date, start_date, hidden_until_start } = req.body;
|
||||||
if (!name || !name.trim()) {
|
if (!name || !name.trim()) {
|
||||||
return res.render('admin/edit-hunt', { title: `Edit: ${hunt.name}`, hunt: { ...hunt, ...req.body }, error: 'Hunt name is required.' });
|
return res.render('admin/edit-hunt', { title: `Edit: ${hunt.name}`, hunt: { ...hunt, ...req.body }, error: 'Hunt name is required.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
Hunts.update(hunt.id, name.trim(), (description || '').trim(), expiry_date);
|
Hunts.update(hunt.id, name.trim(), (description || '').trim(), expiry_date, start_date, hidden_until_start);
|
||||||
req.session.flash = { type: 'success', message: 'Hunt updated successfully.' };
|
req.session.flash = { type: 'success', message: 'Hunt updated successfully.' };
|
||||||
res.redirect(`/admin/hunts/${hunt.id}`);
|
res.redirect(`/admin/hunts/${hunt.id}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,10 +10,16 @@ router.get('/hunt/:shortName', (req, res) => {
|
|||||||
return res.status(404).render('error', { title: 'Not Found', message: 'Hunt not found.' });
|
return res.status(404).render('error', { title: 'Not Found', message: 'Hunt not found.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block access if hidden and not started (unless admin/organizer)
|
||||||
|
if (Hunts.isHidden(hunt) && !(req.session && (req.session.isAdmin || req.session.isOrganizer))) {
|
||||||
|
return res.status(404).render('error', { title: 'Not Found', message: 'Hunt not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
const packages = Packages.getByHunt(hunt.id);
|
const packages = Packages.getByHunt(hunt.id);
|
||||||
const isExpired = Hunts.isExpired(hunt);
|
const isExpired = Hunts.isExpired(hunt);
|
||||||
|
const hasStarted = Hunts.hasStarted(hunt);
|
||||||
|
|
||||||
res.render('hunt/profile', { title: hunt.name, hunt, packages, isExpired });
|
res.render('hunt/profile', { title: hunt.name, hunt, packages, isExpired, hasStarted });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Hunt leaderboard ─────────────────────────────────────
|
// ─── Hunt leaderboard ─────────────────────────────────────
|
||||||
@@ -23,6 +29,10 @@ router.get('/hunt/:shortName/leaderboard', (req, res) => {
|
|||||||
return res.status(404).render('error', { title: 'Not Found', message: 'Hunt not found.' });
|
return res.status(404).render('error', { title: 'Not Found', message: 'Hunt not found.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Hunts.isHidden(hunt) && !(req.session && (req.session.isAdmin || req.session.isOrganizer))) {
|
||||||
|
return res.status(404).render('error', { title: 'Not Found', message: 'Hunt not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
const perPage = 25;
|
const perPage = 25;
|
||||||
const page = Math.max(1, parseInt(req.query.page) || 1);
|
const page = Math.max(1, parseInt(req.query.page) || 1);
|
||||||
const totalCount = Hunts.getLeaderboardCount(hunt.id);
|
const totalCount = Hunts.getLeaderboardCount(hunt.id);
|
||||||
@@ -128,7 +138,8 @@ router.post('/player/:username/password', requireAuth, (req, res) => {
|
|||||||
|
|
||||||
// ─── Browse all hunts ─────────────────────────────────────
|
// ─── Browse all hunts ─────────────────────────────────────
|
||||||
router.get('/hunts', (req, res) => {
|
router.get('/hunts', (req, res) => {
|
||||||
const hunts = Hunts.getAll();
|
const allHunts = Hunts.getAll();
|
||||||
|
const hunts = allHunts.filter(h => !Hunts.isHidden(h));
|
||||||
res.render('hunt/list', { title: 'All Hunts', hunts });
|
res.render('hunt/list', { title: 'All Hunts', hunts });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,18 @@
|
|||||||
<div class="form-hint">Leave blank for no expiry.</div>
|
<div class="form-hint">Leave blank for no expiry.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="start_date">Start Date (optional)</label>
|
||||||
|
<input type="datetime-local" id="start_date" name="start_date" class="form-control">
|
||||||
|
<div class="form-hint">When the hunt becomes active. Leave blank to start immediately.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
|
<input type="checkbox" id="hidden_until_start" name="hidden_until_start" value="1">
|
||||||
|
<label for="hidden_until_start" style="margin: 0; cursor: pointer;">Hidden until start date</label>
|
||||||
|
<div class="form-hint" style="margin-left: auto;">Keep this hunt secret until the start time.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center;">Create Hunt</button>
|
<button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center;">Create Hunt</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,10 +18,18 @@
|
|||||||
<h3><%= hunt.name %></h3>
|
<h3><%= hunt.name %></h3>
|
||||||
<span class="meta"><%= hunt.short_name %> · <%= hunt.package_count %> packages
|
<span class="meta"><%= hunt.short_name %> · <%= hunt.package_count %> packages
|
||||||
<% if (isAdmin && hunt.creator_name) { %> · by <%= hunt.creator_name %><% } %>
|
<% if (isAdmin && hunt.creator_name) { %> · by <%= hunt.creator_name %><% } %>
|
||||||
<% if (hunt.expiry_date) { %> · Expires: <%= new Date(hunt.expiry_date).toLocaleDateString() %><% } %>
|
<% if (hunt.expiry_date) { %> · Expires <time datetime="<%= hunt.expiry_date %>"><%= new Date(hunt.expiry_date).toLocaleDateString() %></time><% } %>
|
||||||
|
<% if (hunt.start_date) { %> · Starts <time datetime="<%= hunt.start_date %>"><%= new Date(hunt.start_date).toLocaleDateString() %></time><% } %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge">Manage</span>
|
<div style="display: flex; gap: 0.4rem; align-items: center;">
|
||||||
|
<% if (hunt.hidden_until_start && hunt.start_date && new Date(hunt.start_date + 'Z') > new Date()) { %>
|
||||||
|
<span class="badge expired" style="font-size: 0.7rem;">Hidden</span>
|
||||||
|
<% } else if (hunt.start_date && new Date(hunt.start_date + 'Z') > new Date()) { %>
|
||||||
|
<span class="badge" style="font-size: 0.7rem;">Upcoming</span>
|
||||||
|
<% } %>
|
||||||
|
<span class="badge">Manage</span>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
@@ -36,6 +36,19 @@
|
|||||||
<div class="form-hint">Leave blank for no expiry.</div>
|
<div class="form-hint">Leave blank for no expiry.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="start_date">Start Date (optional)</label>
|
||||||
|
<input type="datetime-local" id="start_date" name="start_date" class="form-control"
|
||||||
|
value="<%= hunt.start_date ? new Date(hunt.start_date).toISOString().slice(0, 16) : '' %>">
|
||||||
|
<div class="form-hint">When the hunt becomes active. Leave blank to start immediately.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
|
<input type="checkbox" id="hidden_until_start" name="hidden_until_start" value="1" <%= hunt.hidden_until_start ? 'checked' : '' %>>
|
||||||
|
<label for="hidden_until_start" style="margin: 0; cursor: pointer;">Hidden until start date</label>
|
||||||
|
<div class="form-hint" style="margin-left: auto;">Keep this hunt secret until the start time.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center;">Save Changes</button>
|
<button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center;">Save Changes</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,9 +42,15 @@
|
|||||||
<div class="label">Discovery Rate</div>
|
<div class="label">Discovery Rate</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-box">
|
<div class="stat-box">
|
||||||
<div class="value"><%= hunt.expiry_date ? new Date(hunt.expiry_date).toLocaleDateString() : 'Never' %></div>
|
<div class="value"><% if (hunt.expiry_date) { %><time datetime="<%= hunt.expiry_date %>"><%= new Date(hunt.expiry_date).toLocaleDateString() %></time><% } else { %>Never<% } %></div>
|
||||||
<div class="label">Expires</div>
|
<div class="label">Expires</div>
|
||||||
</div>
|
</div>
|
||||||
|
<% if (hunt.start_date) { %>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="value"><time datetime="<%= hunt.start_date %>"><%= new Date(hunt.start_date).toLocaleDateString() %></time></div>
|
||||||
|
<div class="label">Starts</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if (typeof stats !== 'undefined' && stats.topFinders.length > 0) { %>
|
<% if (typeof stats !== 'undefined' && stats.topFinders.length > 0) { %>
|
||||||
|
|||||||
@@ -52,10 +52,14 @@
|
|||||||
<a href="/hunt/<%= hunt.short_name %>" class="hunt-card">
|
<a href="/hunt/<%= hunt.short_name %>" class="hunt-card">
|
||||||
<div class="hunt-info">
|
<div class="hunt-info">
|
||||||
<h3><%= hunt.name %></h3>
|
<h3><%= hunt.name %></h3>
|
||||||
<span class="meta"><%= hunt.short_name %> · <%= hunt.package_count %> packages · by <%= hunt.creator_name %></span>
|
<span class="meta"><%= hunt.short_name %> · <%= hunt.package_count %> packages · by <%= hunt.creator_name %>
|
||||||
|
<% if (hunt.expiry_date) { %> · Expires <time datetime="<%= hunt.expiry_date %>"><%= new Date(hunt.expiry_date).toLocaleDateString() %></time><% } %>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% if (hunt.expiry_date && new Date(hunt.expiry_date) < new Date()) { %>
|
<% if (hunt.expiry_date && new Date(hunt.expiry_date + 'Z') < new Date()) { %>
|
||||||
<span class="badge expired">Expired</span>
|
<span class="badge expired">Expired</span>
|
||||||
|
<% } else if (hunt.start_date && new Date(hunt.start_date + 'Z') > new Date()) { %>
|
||||||
|
<span class="badge">Upcoming</span>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<span class="badge"><%= hunt.package_count %> packages</span>
|
<span class="badge"><%= hunt.package_count %> packages</span>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
@@ -14,12 +14,17 @@
|
|||||||
<h3><%= hunt.name %></h3>
|
<h3><%= hunt.name %></h3>
|
||||||
<span class="meta"><%= hunt.short_name %> · <%= hunt.package_count %> packages · by <%= hunt.creator_name %>
|
<span class="meta"><%= hunt.short_name %> · <%= hunt.package_count %> packages · by <%= hunt.creator_name %>
|
||||||
<% if (hunt.expiry_date) { %>
|
<% if (hunt.expiry_date) { %>
|
||||||
· Expires: <%= new Date(hunt.expiry_date).toLocaleDateString() %>
|
· Expires <time datetime="<%= hunt.expiry_date %>"><%= new Date(hunt.expiry_date).toLocaleDateString() %></time>
|
||||||
|
<% } %>
|
||||||
|
<% if (hunt.start_date && new Date(hunt.start_date + 'Z') > new Date()) { %>
|
||||||
|
· Starts <time datetime="<%= hunt.start_date %>"><%= new Date(hunt.start_date).toLocaleDateString() %></time>
|
||||||
<% } %>
|
<% } %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% if (hunt.expiry_date && new Date(hunt.expiry_date) < new Date()) { %>
|
<% if (hunt.expiry_date && new Date(hunt.expiry_date + 'Z') < new Date()) { %>
|
||||||
<span class="badge expired">Expired</span>
|
<span class="badge expired">Expired</span>
|
||||||
|
<% } else if (hunt.start_date && new Date(hunt.start_date + 'Z') > new Date()) { %>
|
||||||
|
<span class="badge">Upcoming</span>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<span class="badge"><%= hunt.package_count %> packages</span>
|
<span class="badge"><%= hunt.package_count %> packages</span>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
Reference in New Issue
Block a user