source: Klonkt/src/views/pages/admin-newsletter.ejs@ 45d22b1

main
Last change on this file since 45d22b1 was 2e247e4, checked in by roboburr <roboburr@…>, 3 months ago

Newsletter / mailing list (premium feature #1) + SMTP wiring

Email infrastructure reuses src/config/mailer.js (env SMTP_*, graceful: without
SMTP no sending, sign-ups still work via single opt-in).

  • DB: subscribers table (per-site, status pending/confirmed/unsub, token) + newsletters table (send history). Idempotent in initializeDatabase.
  • SubscriberService: addSubscriber (double opt-in if SMTP, otherwise single), confirm, unsubscribe, confirmedFor, counts. Email validation + dedupe per (site, email).
  • routes/newsletter.js (public, premium-gated; unsubscribe always works): GET/POST /nieuwsbrief, /nieuwsbrief/bevestigen/:token, /nieuwsbrief/uitschrijven/:token. Confirm/unsub links absolute via PUBLIC_BASE_URL + siteUrlBase.
  • routes/admin-newsletter.js (site admin + premium): GET /admin/newsletter (compose + counts + history + shareable signup link + SMTP warning), POST /admin/newsletter/send (to confirmed subscribers, unsubscribe link per email, history written).
  • views: pages/newsletter.ejs (multi-state) + pages/admin-newsletter.ejs.
  • admin dashboard: premium links Newsletter + Press kit added.

Feature #2 (download-for-email) and #8 (notify) reuse subscribers. node --check
passed; local test suite missing better-sqlite3 (runs on the VPS).

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

  • Property mode set to 100644
File size: 4.1 KB
Line 
1<%
2 var c = (typeof nlCounts !== 'undefined') ? nlCounts : { confirmed:0, pending:0, unsub:0 };
3 var msg = (typeof nlMsg !== 'undefined') ? nlMsg : '';
4 var msgKind = (typeof nlMsgKind !== 'undefined') ? nlMsgKind : '';
5 var hist = (typeof nlHistory !== 'undefined') ? nlHistory : [];
6 var smtp = (typeof nlSmtp !== 'undefined') ? nlSmtp : false;
7%>
8<section class="anl">
9 <h1>Nieuwsbrief</h1>
10
11 <% if (msg) { %><div class="anl-msg anl-<%= msgKind %>"><%= msg %></div><% } %>
12
13 <div class="anl-stats">
14 <div class="anl-stat"><span class="anl-n"><%= c.confirmed %></span><span class="anl-l">bevestigd</span></div>
15 <div class="anl-stat"><span class="anl-n"><%= c.pending %></span><span class="anl-l">in afwachting</span></div>
16 <div class="anl-stat"><span class="anl-n"><%= c.unsub %></span><span class="anl-l">uitgeschreven</span></div>
17 </div>
18
19 <% if (!smtp) { %>
20 <div class="anl-msg anl-warn">⚠ SMTP is nog niet ingesteld. Aanmeldingen worden wél verzameld, maar versturen kan pas als je SMTP-gegevens invult (<code>SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM</code> in <code>.env</code>).</div>
21 <% } %>
22
23 <p class="anl-share">Aanmeldlink om te delen: <a href="<%= nlSubscribeUrl %>"><%= nlSubscribeUrl %></a></p>
24
25 <form method="POST" action="<%= siteUrlBase %>/admin/newsletter/send" class="anl-form">
26 <label>Onderwerp
27 <input type="text" name="subject" required maxlength="200" value="<%= (typeof nlSubject!=='undefined')?nlSubject:'' %>" placeholder="Nieuwe single uit!">
28 </label>
29 <label>Bericht
30 <textarea name="body" rows="10" required placeholder="Schrijf je update…"><%= (typeof nlBody!=='undefined')?nlBody:'' %></textarea>
31 </label>
32 <button type="submit" class="anl-send" <%= smtp ? '' : 'disabled' %> onclick="return confirm('Nieuwsbrief versturen naar <%= c.confirmed %> bevestigde abonnee(s)?')">Versturen naar <%= c.confirmed %> abonnee(s)</button>
33 </form>
34
35 <% if (hist.length) { %>
36 <h2 class="anl-h2">Verstuurd</h2>
37 <ul class="anl-hist">
38 <% hist.forEach(function(h){ %>
39 <li><span class="anl-hist-subj"><%= h.subject %></span><span class="anl-hist-meta"><%= h.recipient_count %> ontvanger(s) · <%= formatDateTime ? formatDateTime(h.sent_at) : h.sent_at %></span></li>
40 <% }); %>
41 </ul>
42 <% } %>
43</section>
44
45<style>
46 .anl { max-width: 720px; margin: 0 auto; padding: 24px 18px 64px; }
47 .anl h1 { margin: 0 0 18px; }
48 .anl-msg { padding: 11px 14px; border-radius: 10px; margin-bottom: 16px; font-size: 14px; }
49 .anl-ok { background: rgba(60,160,90,.15); }
50 .anl-bad { background: rgba(200,60,60,.15); }
51 .anl-warn { background: rgba(210,160,40,.15); }
52 .anl-stats { display: flex; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
53 .anl-stat { flex: 1 1 100px; border: 1px solid rgba(128,128,128,.2); border-radius: 12px; padding: 14px; text-align: center; }
54 .anl-n { display: block; font-size: 28px; font-weight: 700; }
55 .anl-l { font-size: 12px; opacity: .65; text-transform: uppercase; letter-spacing: .06em; }
56 .anl-share { font-size: 13px; opacity: .8; margin: 0 0 20px; }
57 .anl-form { display: flex; flex-direction: column; gap: 14px; }
58 .anl-form label { display: flex; flex-direction: column; gap: 6px; font-weight: 600; font-size: 13.5px; }
59 .anl-form input, .anl-form textarea { padding: 11px 13px; border-radius: 10px; border: 1px solid rgba(128,128,128,.4); background: transparent; color: inherit; font: inherit; font-weight: 400; }
60 .anl-send { align-self: flex-start; padding: 11px 20px; border-radius: 10px; border: none; background: var(--accent, #6b8f71); color: #fff; font-weight: 600; cursor: pointer; }
61 .anl-send:disabled { opacity: .5; cursor: not-allowed; }
62 .anl-h2 { margin: 30px 0 12px; font-size: 13px; text-transform: uppercase; letter-spacing: .08em; opacity: .6; }
63 .anl-hist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
64 .anl-hist li { display: flex; justify-content: space-between; gap: 12px; padding: 10px 12px; border: 1px solid rgba(128,128,128,.18); border-radius: 10px; flex-wrap: wrap; }
65 .anl-hist-meta { opacity: .6; font-size: 12.5px; }
66</style>
Note: See TracBrowser for help on using the repository browser.