From 34b3a4cbd0ea06145ea3954e1fb9a7facf259c9d Mon Sep 17 00:00:00 2001 From: Mike Johnston Date: Thu, 19 Mar 2026 14:39:06 -0400 Subject: [PATCH] allow for a starting date and hidden hunt if it hasnt started --- public/js/timeago.js | 23 ++++++++++++++--------- src/app.js | 3 ++- src/config/database.js | 4 +++- src/models/index.js | 22 ++++++++++++++++------ src/routes/admin.js | 8 ++++---- src/routes/hunts.js | 15 +++++++++++++-- src/views/admin/create-hunt.ejs | 12 ++++++++++++ src/views/admin/dashboard.ejs | 12 ++++++++++-- src/views/admin/edit-hunt.ejs | 13 +++++++++++++ src/views/admin/manage-hunt.ejs | 8 +++++++- src/views/home.ejs | 8 ++++++-- src/views/hunt/list.ejs | 9 +++++++-- 12 files changed, 107 insertions(+), 30 deletions(-) diff --git a/public/js/timeago.js b/public/js/timeago.js index 61d2803..c9772bf 100644 --- a/public/js/timeago.js +++ b/public/js/timeago.js @@ -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() { diff --git a/src/app.js b/src/app.js index a663c75..3760621 100644 --- a/src/app.js +++ b/src/app.js @@ -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 }); }); diff --git a/src/config/database.js b/src/config/database.js index 0210145..521a0fe 100644 --- a/src/config/database.js +++ b/src/config/database.js @@ -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 */ } diff --git a/src/models/index.js b/src/models/index.js index 536012f..f69cbfd 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -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) { diff --git a/src/routes/admin.js b/src/routes/admin.js index 43a7ed6..56be7fc 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -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}`); }); diff --git a/src/routes/hunts.js b/src/routes/hunts.js index e18929f..7b8d779 100644 --- a/src/routes/hunts.js +++ b/src/routes/hunts.js @@ -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 }); }); diff --git a/src/views/admin/create-hunt.ejs b/src/views/admin/create-hunt.ejs index 9c1a5c3..5ff523e 100644 --- a/src/views/admin/create-hunt.ejs +++ b/src/views/admin/create-hunt.ejs @@ -40,6 +40,18 @@
Leave blank for no expiry.
+
+ + +
When the hunt becomes active. Leave blank to start immediately.
+
+ +
+ + +
Keep this hunt secret until the start time.
+
+ diff --git a/src/views/admin/dashboard.ejs b/src/views/admin/dashboard.ejs index d2c7a27..a352678 100644 --- a/src/views/admin/dashboard.ejs +++ b/src/views/admin/dashboard.ejs @@ -18,10 +18,18 @@

<%= hunt.name %>

<%= 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 <% } %> + <% if (hunt.start_date) { %> · Starts <% } %> - Manage +
+ <% if (hunt.hidden_until_start && hunt.start_date && new Date(hunt.start_date + 'Z') > new Date()) { %> + Hidden + <% } else if (hunt.start_date && new Date(hunt.start_date + 'Z') > new Date()) { %> + Upcoming + <% } %> + Manage +
<% }) %> <% } %> diff --git a/src/views/admin/edit-hunt.ejs b/src/views/admin/edit-hunt.ejs index a65fa4c..f644480 100644 --- a/src/views/admin/edit-hunt.ejs +++ b/src/views/admin/edit-hunt.ejs @@ -36,6 +36,19 @@
Leave blank for no expiry.
+
+ + +
When the hunt becomes active. Leave blank to start immediately.
+
+ +
+ > + +
Keep this hunt secret until the start time.
+
+ diff --git a/src/views/admin/manage-hunt.ejs b/src/views/admin/manage-hunt.ejs index 842352c..b9bf54b 100644 --- a/src/views/admin/manage-hunt.ejs +++ b/src/views/admin/manage-hunt.ejs @@ -42,9 +42,15 @@
Discovery Rate
-
<%= hunt.expiry_date ? new Date(hunt.expiry_date).toLocaleDateString() : 'Never' %>
+
<% if (hunt.expiry_date) { %><% } else { %>Never<% } %>
Expires
+ <% if (hunt.start_date) { %> +
+
+
Starts
+
+ <% } %> <% if (typeof stats !== 'undefined' && stats.topFinders.length > 0) { %> diff --git a/src/views/home.ejs b/src/views/home.ejs index 8f3d748..484090f 100644 --- a/src/views/home.ejs +++ b/src/views/home.ejs @@ -52,10 +52,14 @@

<%= hunt.name %>

- <%= hunt.short_name %> · <%= hunt.package_count %> packages · by <%= hunt.creator_name %> + <%= hunt.short_name %> · <%= hunt.package_count %> packages · by <%= hunt.creator_name %> + <% if (hunt.expiry_date) { %> · Expires <% } %> +
- <% if (hunt.expiry_date && new Date(hunt.expiry_date) < new Date()) { %> + <% if (hunt.expiry_date && new Date(hunt.expiry_date + 'Z') < new Date()) { %> Expired + <% } else if (hunt.start_date && new Date(hunt.start_date + 'Z') > new Date()) { %> + Upcoming <% } else { %> <%= hunt.package_count %> packages <% } %> diff --git a/src/views/hunt/list.ejs b/src/views/hunt/list.ejs index b1e231f..9bdda7f 100644 --- a/src/views/hunt/list.ejs +++ b/src/views/hunt/list.ejs @@ -14,12 +14,17 @@

<%= hunt.name %>

<%= hunt.short_name %> · <%= hunt.package_count %> packages · by <%= hunt.creator_name %> <% if (hunt.expiry_date) { %> - · Expires: <%= new Date(hunt.expiry_date).toLocaleDateString() %> + · Expires + <% } %> + <% if (hunt.start_date && new Date(hunt.start_date + 'Z') > new Date()) { %> + · Starts <% } %> - <% if (hunt.expiry_date && new Date(hunt.expiry_date) < new Date()) { %> + <% if (hunt.expiry_date && new Date(hunt.expiry_date + 'Z') < new Date()) { %> Expired + <% } else if (hunt.start_date && new Date(hunt.start_date + 'Z') > new Date()) { %> + Upcoming <% } else { %> <%= hunt.package_count %> packages <% } %>