diff --git a/package.json b/package.json index dd5d1b3..25da85f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "1.0.9", + "version": "1.0.10", "type": "module", "scripts": { "dev": "vite", diff --git a/server/email.js b/server/email.js index 72b9300..2c649d6 100644 --- a/server/email.js +++ b/server/email.js @@ -385,6 +385,53 @@ export function buildAdminReplyTemplate({ recipientName, message }) { ` } +export function buildQuestionAnsweredEmailTemplate({ firstName, question, answer, questionUrl }) { + const safeName = escapeHtml(firstName || 'friend') + const safeQuestion = escapeHtml(question) + const safeAnswer = escapeHtml(answer.length > 600 ? answer.slice(0, 597) + '…' : answer).replace(/\n/g, '
') + const safeUrl = escapeHtml(questionUrl) + const text = `Hi ${firstName || 'friend'},\n\nYour question has been answered on Verse by Verse with Nate.\n\nYour question: ${question}\n\nAnswer: ${answer}\n\nRead it at: ${questionUrl}\n\nGrace and peace,\nNate` + const html = ` +
+ + + + +
+ + + + + + + + + + +
+
Verse by Verse with Nate
+

Your Question Was Answered

+
+

Hi ${safeName},

+

Nate has answered your question on Verse by Verse with Nate.

+
+

Your question

+

${safeQuestion}

+
+
+

Answer

+

${safeAnswer}

+
+ Read Full Answer → +
+

You received this because you requested a notification when this question was answered.

+
+
+
+ ` + return { subject: 'Your question has been answered — Verse by Verse with Nate', text, html } +} + // ── Transactional email senders ──────────────────────────────────────────── export async function sendStudyWelcomeEmail(email, displayName) { diff --git a/server/routes/contact.js b/server/routes/contact.js index d7e0e9f..2ac8808 100644 --- a/server/routes/contact.js +++ b/server/routes/contact.js @@ -102,7 +102,7 @@ function contactRateLimit(req, res, next) { export function register(app) { app.post('/api/contact', contactRateLimit, async (req, res) => { try { - const { firstName, lastName, email, message, messageType, subscribe, _honey } = req.body ?? {} + const { firstName, lastName, email, message, messageType, subscribe, notifyOnAnswer, _honey } = req.body ?? {} if (_honey) { res.json({ ok: true }); return } @@ -164,6 +164,7 @@ export function register(app) { answeredAt: null, isApproved: false, approvedAt: null, + notifyOnAnswer: notifyOnAnswer === true, } state.questions.unshift(question) state.questions = state.questions.slice(0, MAX_QUESTIONS) diff --git a/server/routes/questions.js b/server/routes/questions.js index 4fe32d6..4eda9a2 100644 --- a/server/routes/questions.js +++ b/server/routes/questions.js @@ -3,6 +3,9 @@ import { requireAdminAuth } from '../auth.js' import { MAX_QUESTIONS } from '../config.js' import { state } from '../state.js' import { queueQuestionsWrite, queueDraftQuestionsWrite } from '../data.js' +import { buildQuestionAnsweredEmailTemplate, getResendFromAddress, getResendReplyToAddress, sendResendEmailWithRetry, getCanonicalBaseUrl } from '../email.js' +import { buildAbsoluteUrl } from '../helpers.js' +import { Resend } from 'resend' function ensureDraftQuestions() { if (state.draftQuestions !== null) return @@ -103,6 +106,29 @@ export function register(app) { question.approvedAt = approved === true ? new Date().toISOString() : null queueDraftQuestionsWrite() + if (approved === true && question.notifyOnAnswer && question.email && question.answer) { + try { + const resend = new Resend(process.env.RESEND_API_KEY) + const from = getResendFromAddress() + const replyTo = getResendReplyToAddress() + const baseUrl = getCanonicalBaseUrl() + const questionUrl = buildAbsoluteUrl(baseUrl, `/questions#qa-${question.id}`) + const template = buildQuestionAnsweredEmailTemplate({ + firstName: question.firstName, + question: question.question, + answer: question.answer, + questionUrl, + }) + await sendResendEmailWithRetry({ + resend, + payload: { from, replyTo, to: question.email, subject: template.subject, text: template.text, html: template.html }, + context: `notify-on-answer:${question.id}`, + }) + } catch (err) { + console.error('[notify-on-answer] Failed to send notification email:', err?.message ?? err) + } + } + res.json({ ok: true, question }) }) diff --git a/src/components/ContactForm.tsx b/src/components/ContactForm.tsx index f5904e1..741cf9f 100644 --- a/src/components/ContactForm.tsx +++ b/src/components/ContactForm.tsx @@ -36,6 +36,7 @@ export default function ContactForm() { } }, [source, studyName]) const [subscribe, setSubscribe] = useState(true) + const [notifyOnAnswer, setNotifyOnAnswer] = useState(true) const [honey, setHoney] = useState('') const [status, setStatus] = useState<'idle' | 'submitting' | 'error'>('idle') const [errorMsg, setErrorMsg] = useState('') @@ -71,7 +72,7 @@ export default function ContactForm() { const res = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...fields, subscribe, _honey: honey }), + body: JSON.stringify({ ...fields, subscribe, notifyOnAnswer: fields.messageType === 'question' ? notifyOnAnswer : false, _honey: honey }), }) if (!res.ok) { @@ -135,6 +136,16 @@ export default function ContactForm() { Message