Add study account engagement features; v1.2.4
- Pause reminders: remindersPausedUntil field + scheduler skip + account UI with 7/14/30-day presets - Email digest mode: reminderFrequency field (immediate/weekly) + weekly digest email + scheduler + account UI toggle - Admin user list improvements: streak, reengagement, frequency, pause, per-study lesson progress - Admin manual re-engagement: POST endpoint + tier selector button in admin panel - Study bookmarks: bookmarkedSectionIds in progress file + POST/DELETE endpoints + toggle button in lesson UI - Study completion certificate email: auto-sent on first certificate issuance - Content fields for certificate and weekly digest email templates in admin Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
5bc8245ed2
commit
038ef2b039
12 files changed
+540
-38
No files matched your search
@@ -635,3 +635,80 @@ export async function sendStudyReengagementEmail(email, displayName, studyTitle,
|
||||
console.error('[study-reengagement] send exception:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendStudyCertificateEmail(email, displayName, studyTitle, certUrl) {
|
||||
if (!process.env.RESEND_API_KEY) return
|
||||
try {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
const cfg = state.cachedSiteContent ?? {}
|
||||
const namePart = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : 'friend'
|
||||
const subject = cfg.studyCertificateEmailSubject?.trim() || 'Your certificate is ready'
|
||||
const bodyText = cfg.studyCertificateEmailBody?.trim() || 'Congratulations — you have completed every lesson in this study. Your certificate is ready to view and share.'
|
||||
const ctaLabel = cfg.studyCertificateEmailCtaLabel?.trim() || 'View Certificate'
|
||||
const signoff = cfg.studyCertificateEmailSignoff?.trim() || 'Grace and peace,\nVerse by Verse with Nate'
|
||||
const bodyHtml = (
|
||||
`<p style="margin:0 0 16px;">Congratulations, <strong style="color:#f0ead8;">${escapeHtml(namePart)}</strong>!</p>` +
|
||||
`<p style="margin:0 0 16px;">${escapeHtml(bodyText)}</p>` +
|
||||
`<p style="margin:0 0 16px;color:#c9a84c;font-weight:700;">${escapeHtml(studyTitle)}</p>`
|
||||
)
|
||||
const footerHtml = `<p style="margin:0;font-family:Georgia,serif;font-size:12px;font-weight:300;color:#7a7060;line-height:1.6;">${escapeHtml(signoff).replace(/\n/g, '<br/>')}</p>`
|
||||
const { error } = await resend.emails.send({
|
||||
from: process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM,
|
||||
to: [email],
|
||||
subject,
|
||||
text: `Congratulations, ${namePart}!\n\n${bodyText}\n\n${studyTitle}\n\nView your certificate: ${certUrl}\n\n${signoff}`,
|
||||
html: buildBrandedEmailHtml({
|
||||
title: 'Study Complete',
|
||||
eyebrow: 'Certificate of Completion',
|
||||
bodyHtml,
|
||||
ctaLabel,
|
||||
ctaUrl: certUrl,
|
||||
footerHtml,
|
||||
}),
|
||||
})
|
||||
if (error) console.error('[study-certificate] email send error:', error)
|
||||
} catch (err) {
|
||||
console.error('[study-certificate] email exception:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendStudyWeeklyDigestEmail(email, displayName, lessons, unsubUrl) {
|
||||
if (!process.env.RESEND_API_KEY) return
|
||||
try {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
const cfg = state.cachedSiteContent ?? {}
|
||||
const namePart = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : 'friend'
|
||||
const subject = cfg.studyWeeklyDigestEmailSubject?.trim() || 'Your weekly study digest'
|
||||
const introText = cfg.studyWeeklyDigestEmailBody?.trim() || 'Here are the lessons released this week in your study track. Open any to continue where you left off.'
|
||||
const signoff = cfg.studyWeeklyDigestEmailSignoff?.trim() || 'Grace and peace,\nVerse by Verse with Nate'
|
||||
const lessonListHtml = lessons.map(l =>
|
||||
`<p style="margin:0 0 12px;"><a href="${escapeHtml(l.sectionUrl)}" style="color:#c9a84c;text-decoration:none;font-weight:700;">${escapeHtml(l.sectionTitle)}</a>` +
|
||||
(l.sectionReference ? ` <span style="color:#7a7060;">(${escapeHtml(l.sectionReference)})</span>` : '') +
|
||||
(l.studyTitle ? ` <span style="color:#7a7060;font-size:0.9em;">— ${escapeHtml(l.studyTitle)}</span>` : '') +
|
||||
`</p>`
|
||||
).join('')
|
||||
const lessonListText = lessons.map(l => `• ${l.sectionTitle}${l.sectionReference ? ` (${l.sectionReference})` : ''}: ${l.sectionUrl}`).join('\n')
|
||||
const bodyHtml = (
|
||||
`<p style="margin:0 0 16px;">Hi <strong style="color:#f0ead8;">${escapeHtml(namePart)}</strong>,</p>` +
|
||||
`<p style="margin:0 0 20px;">${escapeHtml(introText)}</p>` +
|
||||
`<div style="border-left:2px solid #2a2518;padding-left:16px;">${lessonListHtml}</div>`
|
||||
)
|
||||
const unsubLine = unsubUrl ? `<p style="margin:16px 0 0;font-size:11px;color:#7a7060;">Not interested? <a href="${escapeHtml(unsubUrl)}" style="color:#7a7060;">Unsubscribe from these reminders.</a></p>` : ''
|
||||
const footerHtml = `<p style="margin:0;font-family:Georgia,serif;font-size:12px;font-weight:300;color:#7a7060;line-height:1.6;">${escapeHtml(signoff).replace(/\n/g, '<br/>')}</p>${unsubLine}`
|
||||
const { error } = await resend.emails.send({
|
||||
from: process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM,
|
||||
to: [email],
|
||||
subject,
|
||||
text: `Hi ${namePart},\n\n${introText}\n\n${lessonListText}\n\n${signoff}${unsubUrl ? `\n\nUnsubscribe: ${unsubUrl}` : ''}`,
|
||||
html: buildBrandedEmailHtml({
|
||||
title: 'Weekly Study Digest',
|
||||
eyebrow: 'Study Reminder',
|
||||
bodyHtml,
|
||||
footerHtml,
|
||||
}),
|
||||
})
|
||||
if (error) console.error('[study-digest] send error:', error)
|
||||
} catch (err) {
|
||||
console.error('[study-digest] send exception:', err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user