← back to Designtradelive
server.js
123 lines
'use strict';
const path = require('path');
const fs = require('fs');
const express = require('express');
const compression = require('compression');
const app = express();
const PORT = parseInt(process.env.PORT || '9926', 10);
const DATA_FILE = path.join(__dirname, 'data', 'events.json');
const GA_ID = 'G-E8R8V9RDXN';
const SITE_NAME = 'DesignTradeLive';
const DOMAIN = 'designtradelive.com';
let EVENTS = [];
function loadEvents() {
try {
EVENTS = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
} catch (err) {
console.error('[events] failed to load events.json:', err.message);
EVENTS = [];
}
}
loadEvents();
fs.watchFile(DATA_FILE, { interval: 5000 }, () => loadEvents());
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(compression());
// Snapshot-file 404 guard — refactor/editor leftovers must never serve from
// the static root even if one accidentally lands in public/.
const SNAPSHOT_PATH_RE = /\.(bak|orig|rej|swp)(\..*)?$|\.pre-|~$/i;
app.use((req, res, next) => {
if (SNAPSHOT_PATH_RE.test(req.path)) return res.status(404).end();
next();
});
app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1h' }));
app.use((req, res, next) => {
res.set('Cache-Control', 'no-store, must-revalidate');
next();
});
function categorize(events) {
const counts = {};
for (const e of events) counts[e.category] = (counts[e.category] || 0) + 1;
return Object.entries(counts).sort((a, b) => b[1] - a[1]);
}
function sortEvents(events, sort) {
const arr = events.slice();
switch (sort) {
case 'name':
return arr.sort((a, b) => a.name.localeCompare(b.name));
case 'city':
return arr.sort((a, b) => a.city.localeCompare(b.city));
case 'category':
return arr.sort((a, b) => a.category.localeCompare(b.category) || a.start_date.localeCompare(b.start_date));
case 'tier':
return arr.sort((a, b) => {
const order = { major: 0, regional: 1, local: 2 };
return (order[a.tier] ?? 9) - (order[b.tier] ?? 9) || a.start_date.localeCompare(b.start_date);
});
case 'date':
default:
return arr.sort((a, b) => a.start_date.localeCompare(b.start_date));
}
}
app.get('/', (req, res) => {
const q = (req.query.q || '').toLowerCase().trim();
const cat = req.query.category || '';
const sort = req.query.sort || 'date';
let filtered = EVENTS;
if (q) {
filtered = filtered.filter(e =>
e.name.toLowerCase().includes(q) ||
e.city.toLowerCase().includes(q) ||
e.country.toLowerCase().includes(q) ||
e.category.toLowerCase().includes(q) ||
(e.summary || '').toLowerCase().includes(q)
);
}
if (cat) filtered = filtered.filter(e => e.category === cat);
filtered = sortEvents(filtered, sort);
res.render('index', {
events: filtered,
total: EVENTS.length,
categories: categorize(EVENTS),
q, cat, sort,
GA_ID, SITE_NAME, DOMAIN
});
});
app.get('/events/:slug', (req, res) => {
const ev = EVENTS.find(e => e.slug === req.params.slug);
if (!ev) return res.status(404).render('404', { GA_ID, SITE_NAME, DOMAIN });
res.render('event', { ev, GA_ID, SITE_NAME, DOMAIN });
});
app.get('/api/events', (req, res) => {
res.set('Cache-Control', 'public, max-age=300');
res.json({ count: EVENTS.length, events: EVENTS });
});
app.get('/healthz', (req, res) => res.json({ ok: true, count: EVENTS.length }));
// Clean-URL aliases for nav links that don't have dedicated pages yet.
// The footer carries the about copy as an anchor (#about); the hamburger
// menu links to /about. Redirect /about to the footer anchor so the link
// never 404s.
app.get('/about', (req, res) => res.redirect(301, '/#about'));
app.get('/contact', (req, res) => res.redirect(301, '/#about'));
app.use((req, res) => res.status(404).render('404', { GA_ID, SITE_NAME, DOMAIN }));
app.listen(PORT, '127.0.0.1', () => {
console.log(`[${SITE_NAME}] listening on http://127.0.0.1:${PORT} — ${EVENTS.length} events loaded`);
});