source: Klonkt/src/routes/admin-shows.js@ d188736

main
Last change on this file since d188736 was 8d32dcf, checked in by roboburr <roboburr@…>, 3 months ago

Show agenda + notify-me (premium feature #8) — last of the 8

  • DB: shows (site_id, date, time, city, venue, country, ticket_url, notes).
  • routes/shows.js (public, premium-gated): GET /shows (upcoming shows + notify form), POST /shows/notify (subscribers source 'notify', double opt-in if SMTP; confirm/unsub via the generic /nieuwsbrief links).
  • routes/admin-shows.js (site admin + premium): GET /admin/shows (list + add form + subscriber count), POST / (add; optional notify email to 'notify' subscribers if SMTP is available), POST /:id/delete.
  • SubscriberService.confirmedFor(siteId, source?) — optional source filter ('notify').
  • views: pages/shows.ejs + pages/admin-shows.ejs. Dashboard 📅 Agenda link.

Notify email requires SMTP (like #1/#2 sending); without SMTP the show is saved
and sign-ups still work. node --check passed.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 4.0 KB
RevLine 
[8d32dcf]1/**
2 * Show-agenda (premium feature #8) — beheerkant.
3 *
4 * GET /admin/shows -> lijst + toevoeg-formulier
5 * POST /admin/shows -> show toevoegen (optioneel notify-mail naar abonnees)
6 * POST /admin/shows/:id/delete
7 *
8 * Premium + site-beheerder. Notify-mail vereist SMTP; zonder SMTP wordt de show
9 * gewoon opgeslagen (geen mail).
10 */
11
12import express from 'express';
13import db from '../config/database.js';
14import { v4 as uuid } from 'uuid';
15import { renderPage } from '../middleware/render.js';
16import { requireSiteManager } from '../middleware/auth.js';
17import { premiumUnlocked } from '../services/PatreonService.js';
18import { mailerConfigured, sendMail } from '../config/mailer.js';
19import { confirmedFor, counts } from '../services/SubscriberService.js';
20
21const router = express.Router();
22
23function esc(s) { return String(s || '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
24function fullUrl(req, p) {
25 const base = (process.env.PUBLIC_BASE_URL || ('https://' + (req.get('host') || ''))).replace(/\/$/, '');
26 return base + (req.res.locals.siteUrlBase || '') + p;
27}
28function premiumGate(req, res, next) {
29 if (!premiumUnlocked()) return res.status(403).send('Agenda is premium — koppel Patreon in Beheer → Instellingen.');
30 next();
31}
32function render(req, res, extra = {}) {
33 const site = res.locals.site;
34 const shows = db.prepare('SELECT * FROM shows WHERE site_id = ? ORDER BY date DESC, time DESC').all(site.id);
35 renderPage(req, res, 'pages/admin-shows', {
36 pageTitle: 'Agenda', bodyClass: 'on-admin',
37 shows, smtp: mailerConfigured(), notifyCount: confirmedFor(site.id, 'notify').length,
38 ...extra,
39 });
40}
41
42router.get('/', requireSiteManager, premiumGate, (req, res) => {
43 if (!res.locals.site) return res.status(404).send('Geen site.');
44 render(req, res);
45});
46
47router.post('/', requireSiteManager, premiumGate, async (req, res) => {
48 const site = res.locals.site;
49 if (!site) return res.status(404).send('Geen site.');
50 const b = req.body || {};
51 const date = (b.date || '').trim();
52 const city = (b.city || '').trim();
53 if (!date || !city) return render(req, res, { msg: 'Datum en plaats zijn verplicht.', msgKind: 'bad' });
54 let ticket = (b.ticket_url || '').trim();
55 if (ticket && !/^https?:\/\//i.test(ticket)) ticket = '';
56
57 db.prepare(`INSERT INTO shows (id, site_id, date, time, city, venue, country, ticket_url, notes)
58 VALUES (?,?,?,?,?,?,?,?,?)`).run(
59 uuid(), site.id, date, (b.time || '').trim() || null, city, (b.venue || '').trim() || null,
60 (b.country || '').trim() || null, ticket || null, (b.notes || '').trim() || null,
61 );
62
63 let sent = 0;
64 if (b.notify && mailerConfigured()) {
65 const subs = confirmedFor(site.id, 'notify');
66 const where = city + (b.venue ? ' — ' + b.venue : '');
67 for (const s of subs) {
68 const unsub = fullUrl(req, '/nieuwsbrief/uitschrijven/' + s.token);
69 try {
70 await sendMail({
71 to: s.email,
72 subject: 'Nieuwe show: ' + where + ' (' + date + ')',
73 text: (site.title || '') + ' speelt op ' + date + ' in ' + where + '.' + (ticket ? ('\nTickets: ' + ticket) : '') + '\n\nUitschrijven: ' + unsub,
74 html: '<p><strong>' + esc(site.title) + '</strong> speelt op <strong>' + esc(date) + '</strong> in ' + esc(where) + '.</p>' +
75 (ticket ? ('<p><a href="' + ticket + '">Tickets</a></p>') : '') +
76 '<p style="color:#888;font-size:12px"><a href="' + unsub + '">Uitschrijven</a></p>',
77 });
78 sent++;
79 } catch { /* sla over */ }
80 }
81 }
82 render(req, res, { msg: 'Show toegevoegd.' + (sent ? (' Notify gestuurd naar ' + sent + ' abonnee(s).') : ''), msgKind: 'ok' });
83});
84
85router.post('/:id/delete', requireSiteManager, premiumGate, (req, res) => {
86 const site = res.locals.site;
87 if (site) db.prepare('DELETE FROM shows WHERE id = ? AND site_id = ?').run(req.params.id, site.id);
88 res.redirect((res.locals.siteUrlBase || '') + '/admin/shows');
89});
90
91export default router;
Note: See TracBrowser for help on using the repository browser.