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 timeAgo(date) {
|
||||
const now = new Date();
|
||||
const seconds = Math.floor((now - date) / 1000);
|
||||
if (seconds < 5) return 'just now';
|
||||
if (seconds < 60) return seconds + 's ago';
|
||||
const diff = now - date;
|
||||
const seconds = Math.floor(Math.abs(diff) / 1000);
|
||||
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);
|
||||
if (minutes < 60) return minutes + 'm ago';
|
||||
if (minutes < 60) return prefix + minutes + 'm' + suffix;
|
||||
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);
|
||||
if (days < 7) return days + 'd ago';
|
||||
if (days < 30) return Math.floor(days / 7) + 'w ago';
|
||||
if (days < 365) return Math.floor(days / 30) + 'mo ago';
|
||||
return Math.floor(days / 365) + 'y ago';
|
||||
if (days < 7) return prefix + days + 'd' + suffix;
|
||||
if (days < 30) return prefix + Math.floor(days / 7) + 'w' + suffix;
|
||||
if (days < 365) return prefix + Math.floor(days / 30) + 'mo' + suffix;
|
||||
return prefix + Math.floor(days / 365) + 'y' + suffix;
|
||||
}
|
||||
|
||||
function updateTimes() {
|
||||
|
||||
@@ -129,7 +129,8 @@ async function start() {
|
||||
// Home page
|
||||
app.get('/', (req, res) => {
|
||||
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);
|
||||
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
|
||||
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) {
|
||||
try { _db.run(m); } catch (e) { /* column already exists */ }
|
||||
|
||||
@@ -141,11 +141,11 @@ const Users = {
|
||||
|
||||
// ─── Hunts ────────────────────────────────────────────────
|
||||
const Hunts = {
|
||||
create(name, shortName, description, packageCount, expiryDate, createdBy) {
|
||||
create(name, shortName, description, packageCount, expiryDate, createdBy, startDate, hiddenUntilStart) {
|
||||
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;
|
||||
|
||||
// Generate packages
|
||||
@@ -217,9 +217,19 @@ const Hunts = {
|
||||
return !!row;
|
||||
},
|
||||
|
||||
update(id, name, description, expiryDate) {
|
||||
db.prepare('UPDATE hunts SET name = ?, description = ?, expiry_date = ? WHERE id = ?')
|
||||
.run(name, description, expiryDate || null, id);
|
||||
update(id, name, description, expiryDate, startDate, hiddenUntilStart) {
|
||||
db.prepare('UPDATE hunts SET name = ?, description = ?, expiry_date = ?, start_date = ?, hidden_until_start = ? WHERE 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) {
|
||||
|
||||
@@ -36,7 +36,7 @@ router.get('/hunts/new', (req, res) => {
|
||||
|
||||
// Create hunt
|
||||
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
|
||||
if (!name || !short_name || !package_count) {
|
||||
@@ -70,7 +70,7 @@ router.post('/hunts', (req, res) => {
|
||||
}
|
||||
|
||||
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.` };
|
||||
res.redirect(`/admin/hunts/${huntId}`);
|
||||
} catch (err) {
|
||||
@@ -102,12 +102,12 @@ router.get('/hunts/:id/edit', requireHuntAccess, (req, res) => {
|
||||
router.post('/hunts/:id/edit', requireHuntAccess, (req, res) => {
|
||||
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()) {
|
||||
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.' };
|
||||
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.' });
|
||||
}
|
||||
|
||||
// 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 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 ─────────────────────────────────────
|
||||
@@ -23,6 +29,10 @@ router.get('/hunt/:shortName/leaderboard', (req, res) => {
|
||||
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 page = Math.max(1, parseInt(req.query.page) || 1);
|
||||
const totalCount = Hunts.getLeaderboardCount(hunt.id);
|
||||
@@ -128,7 +138,8 @@ router.post('/player/:username/password', requireAuth, (req, res) => {
|
||||
|
||||
// ─── Browse all hunts ─────────────────────────────────────
|
||||
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 });
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,18 @@
|
||||
<div class="form-hint">Leave blank for no expiry.</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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -18,10 +18,18 @@
|
||||
<h3><%= hunt.name %></h3>
|
||||
<span class="meta"><%= hunt.short_name %> · <%= hunt.package_count %> packages
|
||||
<% 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>
|
||||
</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>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
|
||||
@@ -36,6 +36,19 @@
|
||||
<div class="form-hint">Leave blank for no expiry.</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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -42,9 +42,15 @@
|
||||
<div class="label">Discovery Rate</div>
|
||||
</div>
|
||||
<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>
|
||||
<% 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>
|
||||
|
||||
<% if (typeof stats !== 'undefined' && stats.topFinders.length > 0) { %>
|
||||
|
||||
@@ -52,10 +52,14 @@
|
||||
<a href="/hunt/<%= hunt.short_name %>" class="hunt-card">
|
||||
<div class="hunt-info">
|
||||
<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>
|
||||
<% 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>
|
||||
<% } else if (hunt.start_date && new Date(hunt.start_date + 'Z') > new Date()) { %>
|
||||
<span class="badge">Upcoming</span>
|
||||
<% } else { %>
|
||||
<span class="badge"><%= hunt.package_count %> packages</span>
|
||||
<% } %>
|
||||
|
||||
@@ -14,12 +14,17 @@
|
||||
<h3><%= hunt.name %></h3>
|
||||
<span class="meta"><%= hunt.short_name %> · <%= hunt.package_count %> packages · by <%= hunt.creator_name %>
|
||||
<% 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>
|
||||
</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>
|
||||
<% } else if (hunt.start_date && new Date(hunt.start_date + 'Z') > new Date()) { %>
|
||||
<span class="badge">Upcoming</span>
|
||||
<% } else { %>
|
||||
<span class="badge"><%= hunt.package_count %> packages</span>
|
||||
<% } %>
|
||||
|
||||
Reference in New Issue
Block a user