email templates
This commit is contained in:
@@ -1094,6 +1094,9 @@ function buildContactWelcomeEmailTemplate({
|
||||
welcomeWebsiteUrl,
|
||||
welcomeEpisodeUrl,
|
||||
welcomeImageUrl,
|
||||
welcomeSpotifyBtnLabel = 'Listen on Spotify',
|
||||
welcomeAppleBtnLabel = 'Apple Podcasts',
|
||||
welcomeStartHereLinkLabel = 'Open Start Here page',
|
||||
}) {
|
||||
const welcomeSignoffHtml = escapeHtml(welcomeSignoff).replace(/\n/g, '<br/>')
|
||||
|
||||
@@ -1142,10 +1145,10 @@ function buildContactWelcomeEmailTemplate({
|
||||
`<p style="margin:0 0 6px;font-family:Georgia,serif;font-size:22px;font-weight:600;color:#f0ead8;">${escapeHtml(welcomeStartHereTitle)}</p>` +
|
||||
`<p style="margin:0 0 20px;font-family:Georgia,serif;font-size:16px;font-weight:300;color:#7a7060;line-height:1.6;">${escapeHtml(welcomeStartHereSummary)}</p>` +
|
||||
`<table cellpadding="0" cellspacing="0" border="0" role="presentation"><tr>` +
|
||||
`<td style="padding-right:12px;"><a href="${escapeHtml(welcomeSpotifyUrl)}" target="_blank" style="display:inline-block;padding:11px 22px;background-color:#c9a84c;color:#0d0d0a;font-family:Georgia,serif;font-size:14px;font-weight:600;letter-spacing:0.06em;text-decoration:none;border-radius:3px;">Listen on Spotify</a></td>` +
|
||||
`<td><a href="${escapeHtml(welcomeAppleUrl)}" target="_blank" style="display:inline-block;padding:11px 22px;background-color:transparent;color:#c9a84c;font-family:Georgia,serif;font-size:14px;font-weight:600;letter-spacing:0.06em;text-decoration:none;border-radius:3px;border:1px solid #c9a84c;">Apple Podcasts</a></td>` +
|
||||
`<td style="padding-right:12px;"><a href="${escapeHtml(welcomeSpotifyUrl)}" target="_blank" style="display:inline-block;padding:11px 22px;background-color:#c9a84c;color:#0d0d0a;font-family:Georgia,serif;font-size:14px;font-weight:600;letter-spacing:0.06em;text-decoration:none;border-radius:3px;">${escapeHtml(welcomeSpotifyBtnLabel)}</a></td>` +
|
||||
`<td><a href="${escapeHtml(welcomeAppleUrl)}" target="_blank" style="display:inline-block;padding:11px 22px;background-color:transparent;color:#c9a84c;font-family:Georgia,serif;font-size:14px;font-weight:600;letter-spacing:0.06em;text-decoration:none;border-radius:3px;border:1px solid #c9a84c;">${escapeHtml(welcomeAppleBtnLabel)}</a></td>` +
|
||||
`</tr></table>` +
|
||||
`<p style="margin:18px 0 0;"><a href="${escapeHtml(welcomeEpisodeUrl)}" target="_blank" style="color:#c9a84c;text-decoration:underline;font-family:Georgia,serif;font-size:14px;">Open Start Here page</a></p>` +
|
||||
`<p style="margin:18px 0 0;"><a href="${escapeHtml(welcomeEpisodeUrl)}" target="_blank" style="color:#c9a84c;text-decoration:underline;font-family:Georgia,serif;font-size:14px;">${escapeHtml(welcomeStartHereLinkLabel)}</a></p>` +
|
||||
`</td></tr>` +
|
||||
`<tr><td style="padding:0 40px;"><div style="height:1px;background-color:#2a2518;margin-bottom:32px;"></div></td></tr>` +
|
||||
`<tr><td style="padding:0 40px 32px;">` +
|
||||
@@ -2433,31 +2436,30 @@ async function sendStudyWelcomeEmail(email, displayName) {
|
||||
if (!process.env.RESEND_API_KEY) return
|
||||
try {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
const cfg = cachedSiteContent ?? {}
|
||||
const namePart = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : 'friend'
|
||||
const baseUrl = getCanonicalBaseUrl()
|
||||
const studiesUrl = buildAbsoluteUrl(baseUrl, '/study')
|
||||
const accountUrl = buildAbsoluteUrl(baseUrl, '/study/account')
|
||||
const subject = cfg.studyWelcomeEmailSubject?.trim() || 'Welcome to the Study Community'
|
||||
const bodyText = cfg.studyWelcomeEmailBody?.trim() || 'Your student account is ready. Open your studies and continue learning, or manage your account details anytime.'
|
||||
const ctaLabel = cfg.studyWelcomeEmailCtaLabel?.trim() || 'Open Studies'
|
||||
const studiesUrl = buildAbsoluteUrl(baseUrl, cfg.studyWelcomeEmailCtaPath?.trim() || '/study')
|
||||
const signoff = cfg.studyWelcomeEmailSignoff?.trim() || 'Grace and peace,\nVerse by Verse with Nate'
|
||||
const bodyHtml = (
|
||||
`<p style="margin:0 0 16px;">Welcome, <strong style="color:#f0ead8;">${escapeHtml(namePart)}</strong>.</p>` +
|
||||
`<p style="margin:0 0 16px;">Your student account is ready.</p>` +
|
||||
`<p style="margin:0 0 16px;">Open studies and continue learning, or manage your account details anytime.</p>`
|
||||
`<p style="margin:0 0 16px;">${escapeHtml(bodyText)}</p>`
|
||||
)
|
||||
const footerHtml = `<p style="margin:0;font-family:Georgia,serif;font-size:12px;font-weight:300;color:#7a7060;line-height:1.6;">Grace and peace,<br/>Verse by Verse with Nate</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 ?? 'Verse by Verse with Nate <hello@versebyversewithnate.us>',
|
||||
to: [email],
|
||||
subject: process.env.RESEND_WELCOME_SUBJECT ?? 'Welcome to Verse by Verse with Nate',
|
||||
text:
|
||||
`Welcome, ${namePart}.\n\n` +
|
||||
`Your student account is ready.\n\n` +
|
||||
`Open studies: ${studiesUrl}\n` +
|
||||
`Manage account: ${accountUrl}\n\n` +
|
||||
`Grace and peace,\nVerse by Verse with Nate`,
|
||||
subject,
|
||||
text: `Welcome, ${namePart}.\n\n${bodyText}\n\nOpen studies: ${studiesUrl}\nManage account: ${accountUrl}\n\n${signoff}`,
|
||||
html: buildBrandedEmailHtml({
|
||||
title: 'Welcome to the Study Community',
|
||||
eyebrow: 'Study Account',
|
||||
bodyHtml,
|
||||
ctaLabel: 'Open Studies',
|
||||
ctaLabel,
|
||||
ctaUrl: studiesUrl,
|
||||
footerHtml: footerHtml + `<p style="margin:14px 0 0;font-family:Georgia,serif;font-size:12px;font-weight:300;color:#7a7060;line-height:1.6;"><a href="${escapeHtml(accountUrl)}" target="_blank" style="color:#c9a84c;text-decoration:none;">Manage your account</a></p>`,
|
||||
}),
|
||||
@@ -2472,30 +2474,29 @@ async function sendStudyAccountDeletedEmail(email, displayName) {
|
||||
if (!process.env.RESEND_API_KEY) return
|
||||
try {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
const cfg = cachedSiteContent ?? {}
|
||||
const namePart = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : 'friend'
|
||||
const baseUrl = getCanonicalBaseUrl()
|
||||
const signupUrl = buildAbsoluteUrl(baseUrl, '/study/signup')
|
||||
const subject = cfg.studyDeletedEmailSubject?.trim() || 'Your study account was deleted'
|
||||
const bodyText = cfg.studyDeletedEmailBody?.trim() || 'This confirms your study account and saved notes were deleted. If this was not you, please contact us immediately.'
|
||||
const ctaLabel = cfg.studyDeletedEmailCtaLabel?.trim() || 'Create a New Account'
|
||||
const signupUrl = buildAbsoluteUrl(baseUrl, cfg.studyDeletedEmailCtaPath?.trim() || '/study/signup')
|
||||
const signoff = cfg.studyDeletedEmailSignoff?.trim() || 'Verse by Verse with Nate'
|
||||
const bodyHtml = (
|
||||
`<p style="margin:0 0 16px;">Hi <strong style="color:#f0ead8;">${escapeHtml(namePart)}</strong>,</p>` +
|
||||
`<p style="margin:0 0 16px;">This confirms your study account and saved notes were deleted.</p>` +
|
||||
`<p style="margin:0 0 16px;">If this was not you, please contact us immediately.</p>`
|
||||
`<p style="margin:0 0 16px;">${escapeHtml(bodyText)}</p>`
|
||||
)
|
||||
const footerHtml = `<p style="margin:0;font-family:Georgia,serif;font-size:12px;font-weight:300;color:#7a7060;line-height:1.6;">Verse by Verse with Nate</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 ?? 'Verse by Verse with Nate <hello@versebyversewithnate.us>',
|
||||
to: [email],
|
||||
subject: 'Your study account was deleted',
|
||||
text:
|
||||
`Hi ${namePart},\n\n` +
|
||||
`This confirms your study account and saved notes were deleted.\n\n` +
|
||||
`If this was not you, please contact us immediately.\n\n` +
|
||||
`Create a new account anytime: ${signupUrl}\n\n` +
|
||||
`Verse by Verse with Nate`,
|
||||
subject,
|
||||
text: `Hi ${namePart},\n\n${bodyText}\n\nCreate a new account anytime: ${signupUrl}\n\n${signoff}`,
|
||||
html: buildBrandedEmailHtml({
|
||||
title: 'Study Account Deleted',
|
||||
eyebrow: 'Account Update',
|
||||
bodyHtml,
|
||||
ctaLabel: 'Create a New Account',
|
||||
ctaLabel,
|
||||
ctaUrl: signupUrl,
|
||||
footerHtml,
|
||||
}),
|
||||
@@ -2782,28 +2783,29 @@ async function sendStudyReminderEmail(email, displayName, studyTitle, sectionTit
|
||||
if (!process.env.RESEND_API_KEY) return
|
||||
try {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
const cfg = cachedSiteContent ?? {}
|
||||
const namePart = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : 'friend'
|
||||
const subjectPrefix = cfg.studyReminderEmailSubjectPrefix?.trim() || 'New lesson available:'
|
||||
const bodyText = cfg.studyReminderEmailBody?.trim() || 'A new lesson has been unlocked in your study track. Open it below to continue where you left off.'
|
||||
const ctaLabel = cfg.studyReminderEmailCtaLabel?.trim() || 'Open the Lesson'
|
||||
const signoff = cfg.studyReminderEmailSignoff?.trim() || 'Grace and peace,\nVerse by Verse with Nate'
|
||||
const subject = process.env.RESEND_REMINDER_SUBJECT ?? `${subjectPrefix} ${sectionTitle}`
|
||||
const bodyHtml = (
|
||||
`<p style="margin:0 0 16px;">Hi <strong style="color:#f0ead8;">${escapeHtml(namePart)}</strong>,</p>` +
|
||||
`<p style="margin:0 0 16px;">A new study lesson is available in ${escapeHtml(studyTitle)}.</p>` +
|
||||
`<p style="margin:0 0 16px;">${escapeHtml(sectionTitle)} (${escapeHtml(sectionReference)}) is now unlocked.</p>`
|
||||
`<p style="margin:0 0 16px;">${escapeHtml(bodyText)}</p>` +
|
||||
`<p style="margin:0 0 16px;"><strong style="color:#f0ead8;">${escapeHtml(sectionTitle)}</strong> (${escapeHtml(sectionReference)}) — ${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;">Grace and peace,<br/>Verse by Verse with Nate</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 ?? 'Verse by Verse with Nate <hello@versebyversewithnate.us>',
|
||||
to: [email],
|
||||
subject: process.env.RESEND_REMINDER_SUBJECT ?? `New lesson available: ${sectionTitle}`,
|
||||
text:
|
||||
`Hi ${namePart},\n\n` +
|
||||
`A new lesson is available in ${studyTitle}.\n` +
|
||||
`${sectionTitle} (${sectionReference}) is now unlocked.\n\n` +
|
||||
`Open it here: ${sectionUrl}\n\n` +
|
||||
`Grace and peace,\nVerse by Verse with Nate`,
|
||||
subject,
|
||||
text: `Hi ${namePart},\n\n${bodyText}\n\n${sectionTitle} (${sectionReference}) — ${studyTitle}\n\nOpen it here: ${sectionUrl}\n\n${signoff}`,
|
||||
html: buildBrandedEmailHtml({
|
||||
title: 'New Lesson Available',
|
||||
eyebrow: 'Study Reminder',
|
||||
bodyHtml,
|
||||
ctaLabel: 'Open the Lesson',
|
||||
ctaLabel,
|
||||
ctaUrl: sectionUrl,
|
||||
footerHtml,
|
||||
}),
|
||||
@@ -3790,19 +3792,23 @@ app.post('/api/study-account/request-email-change', studyAuthRateLimiter, requir
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
const baseUrl = getCanonicalBaseUrl()
|
||||
const verifyUrl = buildAbsoluteUrl(baseUrl, `/study/account?verifyEmailToken=${encodeURIComponent(rawToken)}`)
|
||||
const cfg = cachedSiteContent ?? {}
|
||||
const emailChangeSubject = cfg.emailChangeSubject?.trim() || 'Confirm your new email address'
|
||||
const emailChangeBody = cfg.emailChangeBody?.trim() || 'Click the link below to confirm your new account email. If you did not request this change, ignore this message.'
|
||||
const emailChangeCtaLabel = cfg.emailChangeCtaLabel?.trim() || 'Confirm Email Change'
|
||||
const { error } = await resend.emails.send({
|
||||
from: process.env.RESEND_FROM ?? 'Verse by Verse with Nate <hello@versebyversewithnate.us>',
|
||||
to: [newEmail],
|
||||
subject: 'Confirm your new email address',
|
||||
text:
|
||||
`Use this link to confirm your new account email:\n${verifyUrl}\n\n` +
|
||||
`If you did not request this change, ignore this message.`,
|
||||
html:
|
||||
`<div style="font-family:Arial,sans-serif;line-height:1.6;color:#1f1a12;">` +
|
||||
`<p>Click the link below to confirm your new account email:</p>` +
|
||||
`<p><a href="${escapeHtml(verifyUrl)}">Confirm email change</a></p>` +
|
||||
`<p>If you did not request this change, ignore this message.</p>` +
|
||||
`</div>`,
|
||||
subject: emailChangeSubject,
|
||||
text: `${emailChangeBody}\n\n${verifyUrl}`,
|
||||
html: buildBrandedEmailHtml({
|
||||
title: emailChangeSubject,
|
||||
eyebrow: 'Account Security',
|
||||
bodyHtml: `<p style="margin:0 0 16px;">${escapeHtml(emailChangeBody)}</p>`,
|
||||
ctaLabel: emailChangeCtaLabel,
|
||||
ctaUrl: verifyUrl,
|
||||
footerHtml: `<p style="margin:0;font-family:Georgia,serif;font-size:12px;color:#7a7060;">Verse by Verse with Nate</p>`,
|
||||
}),
|
||||
})
|
||||
if (error) {
|
||||
console.error('[study-account] email change send error:', error)
|
||||
@@ -5013,6 +5019,10 @@ app.post('/api/contact', contactRateLimit, async (req, res) => {
|
||||
process.env.RESEND_WELCOME_IMAGE_URL ?? emailConfig.welcomeEmailImageUrl ?? '/images/podcast-art.jpeg',
|
||||
)
|
||||
|
||||
const welcomeSpotifyBtnLabel = emailConfig.welcomeEmailSpotifyBtnLabel?.trim() || 'Listen on Spotify'
|
||||
const welcomeAppleBtnLabel = emailConfig.welcomeEmailAppleBtnLabel?.trim() || 'Apple Podcasts'
|
||||
const welcomeStartHereLinkLabel = emailConfig.welcomeEmailStartHereLinkLabel?.trim() || 'Open Start Here page'
|
||||
|
||||
const welcomeTemplate = buildContactWelcomeEmailTemplate({
|
||||
greetingName,
|
||||
welcomeIntro,
|
||||
@@ -5032,6 +5042,9 @@ app.post('/api/contact', contactRateLimit, async (req, res) => {
|
||||
welcomeWebsiteUrl,
|
||||
welcomeEpisodeUrl,
|
||||
welcomeImageUrl,
|
||||
welcomeSpotifyBtnLabel,
|
||||
welcomeAppleBtnLabel,
|
||||
welcomeStartHereLinkLabel,
|
||||
})
|
||||
|
||||
try {
|
||||
|
||||
@@ -339,6 +339,26 @@ export function sanitizeSiteContent(siteContent) {
|
||||
redirects: sanitizeRedirectRules(siteContent.redirects),
|
||||
podcastFeaturedLinks: sanitizeFeaturedLinks(siteContent.podcastFeaturedLinks ?? DEFAULT_PODCAST_FEATURED_LINKS),
|
||||
episodesSeoIntro: typeof siteContent.episodesSeoIntro === 'string' && siteContent.episodesSeoIntro.trim() ? siteContent.episodesSeoIntro.trim().slice(0, 600) : '',
|
||||
welcomeEmailSpotifyBtnLabel: typeof siteContent.welcomeEmailSpotifyBtnLabel === 'string' ? siteContent.welcomeEmailSpotifyBtnLabel.trim().slice(0, 80) : '',
|
||||
welcomeEmailAppleBtnLabel: typeof siteContent.welcomeEmailAppleBtnLabel === 'string' ? siteContent.welcomeEmailAppleBtnLabel.trim().slice(0, 80) : '',
|
||||
welcomeEmailStartHereLinkLabel: typeof siteContent.welcomeEmailStartHereLinkLabel === 'string' ? siteContent.welcomeEmailStartHereLinkLabel.trim().slice(0, 80) : '',
|
||||
studyWelcomeEmailSubject: typeof siteContent.studyWelcomeEmailSubject === 'string' ? siteContent.studyWelcomeEmailSubject.trim().slice(0, 200) : '',
|
||||
studyWelcomeEmailBody: typeof siteContent.studyWelcomeEmailBody === 'string' ? siteContent.studyWelcomeEmailBody.trim().slice(0, 1000) : '',
|
||||
studyWelcomeEmailCtaLabel: typeof siteContent.studyWelcomeEmailCtaLabel === 'string' ? siteContent.studyWelcomeEmailCtaLabel.trim().slice(0, 80) : '',
|
||||
studyWelcomeEmailCtaPath: typeof siteContent.studyWelcomeEmailCtaPath === 'string' ? siteContent.studyWelcomeEmailCtaPath.trim().slice(0, 200) : '',
|
||||
studyWelcomeEmailSignoff: typeof siteContent.studyWelcomeEmailSignoff === 'string' ? siteContent.studyWelcomeEmailSignoff.trim().slice(0, 200) : '',
|
||||
studyDeletedEmailSubject: typeof siteContent.studyDeletedEmailSubject === 'string' ? siteContent.studyDeletedEmailSubject.trim().slice(0, 200) : '',
|
||||
studyDeletedEmailBody: typeof siteContent.studyDeletedEmailBody === 'string' ? siteContent.studyDeletedEmailBody.trim().slice(0, 1000) : '',
|
||||
studyDeletedEmailCtaLabel: typeof siteContent.studyDeletedEmailCtaLabel === 'string' ? siteContent.studyDeletedEmailCtaLabel.trim().slice(0, 80) : '',
|
||||
studyDeletedEmailCtaPath: typeof siteContent.studyDeletedEmailCtaPath === 'string' ? siteContent.studyDeletedEmailCtaPath.trim().slice(0, 200) : '',
|
||||
studyDeletedEmailSignoff: typeof siteContent.studyDeletedEmailSignoff === 'string' ? siteContent.studyDeletedEmailSignoff.trim().slice(0, 200) : '',
|
||||
studyReminderEmailSubjectPrefix: typeof siteContent.studyReminderEmailSubjectPrefix === 'string' ? siteContent.studyReminderEmailSubjectPrefix.trim().slice(0, 200) : '',
|
||||
studyReminderEmailBody: typeof siteContent.studyReminderEmailBody === 'string' ? siteContent.studyReminderEmailBody.trim().slice(0, 1000) : '',
|
||||
studyReminderEmailCtaLabel: typeof siteContent.studyReminderEmailCtaLabel === 'string' ? siteContent.studyReminderEmailCtaLabel.trim().slice(0, 80) : '',
|
||||
studyReminderEmailSignoff: typeof siteContent.studyReminderEmailSignoff === 'string' ? siteContent.studyReminderEmailSignoff.trim().slice(0, 200) : '',
|
||||
emailChangeSubject: typeof siteContent.emailChangeSubject === 'string' ? siteContent.emailChangeSubject.trim().slice(0, 200) : '',
|
||||
emailChangeBody: typeof siteContent.emailChangeBody === 'string' ? siteContent.emailChangeBody.trim().slice(0, 500) : '',
|
||||
emailChangeCtaLabel: typeof siteContent.emailChangeCtaLabel === 'string' ? siteContent.emailChangeCtaLabel.trim().slice(0, 80) : '',
|
||||
seo: {
|
||||
title: typeof seo.title === 'string' && seo.title.trim() ? seo.title.trim().slice(0, 120) : DEFAULT_SEO.title,
|
||||
description: typeof seo.description === 'string' && seo.description.trim() ? seo.description.trim().slice(0, 240) : DEFAULT_SEO.description,
|
||||
|
||||
+154
-3
@@ -358,6 +358,114 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
function EmailTemplatesPanel({
|
||||
form,
|
||||
handleChange,
|
||||
renderSaveStatus,
|
||||
}: {
|
||||
form: SiteContent
|
||||
handleChange: (key: StringField, value: string) => void
|
||||
renderSaveStatus: () => React.ReactNode
|
||||
}) {
|
||||
const [expandedEmail, setExpandedEmail] = useState<string | null>(null)
|
||||
|
||||
function EmailTemplateCard({
|
||||
type, icon, title, trigger, autoVars, fieldKeys, openKey,
|
||||
}: {
|
||||
type: 'newsletter' | 'study' | 'transactional'
|
||||
icon: string
|
||||
title: string
|
||||
trigger: string
|
||||
autoVars?: string
|
||||
fieldKeys: string[]
|
||||
openKey: string
|
||||
}) {
|
||||
const isOpen = expandedEmail === openKey
|
||||
return (
|
||||
<div className={`admin-et-card admin-et-card--${type}${isOpen ? ' admin-et-card--open' : ''}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="admin-et-card-header"
|
||||
onClick={() => setExpandedEmail(isOpen ? null : openKey)}
|
||||
>
|
||||
<span className="admin-et-card-icon" aria-hidden="true">{icon}</span>
|
||||
<div className="admin-et-card-meta">
|
||||
<div className="admin-et-card-title-row">
|
||||
<span className={`admin-et-badge admin-et-badge--${type}`}>{type}</span>
|
||||
<strong className="admin-et-card-title">{title}</strong>
|
||||
</div>
|
||||
<p className="admin-et-card-trigger">⚡ {trigger}</p>
|
||||
</div>
|
||||
<span className="admin-et-card-chevron">{isOpen ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="admin-et-card-body">
|
||||
{autoVars && (
|
||||
<p className="admin-et-card-vars">
|
||||
<span className="admin-et-card-vars-label">Auto-inserted:</span> {autoVars}
|
||||
</p>
|
||||
)}
|
||||
{fieldKeys.map(key => {
|
||||
const field = FIELDS.find(f => f.key === key && f.section === 'email-templates')
|
||||
if (!field) return null
|
||||
return (
|
||||
<div className="admin-field" key={key}>
|
||||
<label htmlFor={`field-et-${key}`}>{field.label}</label>
|
||||
{field.multiline
|
||||
? <textarea id={`field-et-${key}`} rows={3} value={form[key as StringField] as string} onChange={e => handleChange(key as StringField, e.target.value)} />
|
||||
: <input id={`field-et-${key}`} type="text" value={form[key as StringField] as string} onChange={e => handleChange(key as StringField, e.target.value)} />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="admin-panel-section" aria-label="Email Templates">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Email Templates</h2>
|
||||
<p>Every automated email the site sends — edit subject lines, body copy, and sign-offs. Publish to apply changes.</p>
|
||||
</div>
|
||||
|
||||
<EmailTemplateCard type="newsletter" icon="📬" title="Newsletter Welcome"
|
||||
trigger="Someone subscribes via the homepage form or contact page"
|
||||
autoVars="Subscriber's first name, Spotify / Apple / Amazon links"
|
||||
fieldKeys={['welcomeEmailSubject','welcomeEmailGreetingPrefix','welcomeEmailIntro','welcomeEmailCurrentSeries','welcomeEmailStartHereTitle','welcomeEmailStartHereSummary','welcomeEmailStartHereUrl','welcomeEmailStartHereLinkLabel','welcomeEmailWhatToExpect1','welcomeEmailWhatToExpect2','welcomeEmailWhatToExpect3','welcomeEmailScripture','welcomeEmailScriptureRef','welcomeEmailSignoff','welcomeEmailSpotifyUrl','welcomeEmailSpotifyBtnLabel','welcomeEmailAppleUrl','welcomeEmailAppleBtnLabel','welcomeEmailAmazonUrl']}
|
||||
openKey="newsletter-welcome" />
|
||||
|
||||
<EmailTemplateCard type="study" icon="📖" title="Study Account — New Account"
|
||||
trigger="A student creates a study account"
|
||||
autoVars="Student's display name, link to study hub, link to account page"
|
||||
fieldKeys={['studyWelcomeEmailSubject','studyWelcomeEmailBody','studyWelcomeEmailCtaLabel','studyWelcomeEmailCtaPath','studyWelcomeEmailSignoff']}
|
||||
openKey="study-welcome" />
|
||||
|
||||
<EmailTemplateCard type="study" icon="🗑️" title="Study Account — Account Deleted"
|
||||
trigger="A student (or admin) deletes a study account"
|
||||
autoVars="Student's display name, link to create a new account"
|
||||
fieldKeys={['studyDeletedEmailSubject','studyDeletedEmailBody','studyDeletedEmailCtaLabel','studyDeletedEmailCtaPath','studyDeletedEmailSignoff']}
|
||||
openKey="study-deleted" />
|
||||
|
||||
<EmailTemplateCard type="study" icon="🔔" title="Study Account — New Lesson Unlocked"
|
||||
trigger="A lesson releases for a student who has reminders turned on"
|
||||
autoVars="Student's name, lesson title, scripture reference, study track name — button links directly to the lesson (auto-generated, not editable)"
|
||||
fieldKeys={['studyReminderEmailSubjectPrefix','studyReminderEmailBody','studyReminderEmailCtaLabel','studyReminderEmailSignoff']}
|
||||
openKey="study-reminder" />
|
||||
|
||||
<EmailTemplateCard type="transactional" icon="🔐" title="Account Security — Email Change Confirmation"
|
||||
trigger="A student requests to change their account email address"
|
||||
autoVars="Button links to a one-time verification URL (auto-generated per request, not editable)"
|
||||
fieldKeys={['emailChangeSubject','emailChangeBody','emailChangeCtaLabel']}
|
||||
openKey="email-change" />
|
||||
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
interface Props {
|
||||
content: SiteContent
|
||||
onSave: (c: SiteContent) => void
|
||||
@@ -557,7 +665,7 @@ type AdminView =
|
||||
| 'podcast' | 'current-series' | 'episode-highlights' | 'podcast-checklist' | 'archived-series'
|
||||
| 'downloads' | 'custom-links' | 'content-blocks'
|
||||
| 'questions' | 'analytics' | 'assets' | 'colossians-study'
|
||||
| 'emails' | 'subscribers' | 'contacts' | 'study-users'
|
||||
| 'emails' | 'subscribers' | 'contacts' | 'study-users' | 'email-templates'
|
||||
| 'seo' | 'legal' | 'security' | 'brand' | 'global'
|
||||
|
||||
interface AdminSectionLink {
|
||||
@@ -597,6 +705,7 @@ const ADMIN_VIEW_OPTIONS: Array<{ group: string; options: Array<{ value: AdminVi
|
||||
{ value: 'contacts', label: 'Contacts' },
|
||||
{ value: 'subscribers', label: 'Subscribers' },
|
||||
{ value: 'study-users', label: 'Study Users' },
|
||||
{ value: 'email-templates', label: 'Email Templates' },
|
||||
{ value: 'analytics', label: 'Analytics' },
|
||||
{ value: 'assets', label: 'Asset Manager' },
|
||||
],
|
||||
@@ -661,7 +770,7 @@ const ADMIN_SECTION_LINKS: Partial<Record<AdminView, AdminSectionLink[]>> = {
|
||||
|
||||
type PodcastTab = 'current-series' | 'episode-highlights' | 'podcast-checklist' | 'archived-series'
|
||||
|
||||
type MainContentSection = 'hero' | 'start-here' | 'about' | 'contact' | 'series' | 'share' | 'prism' | 'global'
|
||||
type MainContentSection = 'hero' | 'start-here' | 'about' | 'contact' | 'series' | 'share' | 'prism' | 'global' | 'email-templates'
|
||||
|
||||
const BRAND_KIT_SWATCHES = [
|
||||
{ name: 'Rich Black', hex: '#0a0a08', role: 'Primary background' },
|
||||
@@ -792,7 +901,44 @@ const FIELDS: Array<{ key: StringField; label: string; multiline?: boolean; sect
|
||||
{ key: 'welcomeEmailWhatToExpect3', label: 'Welcome Email — What to Expect 3', multiline: true, section: 'global' },
|
||||
{ key: 'welcomeEmailScripture', label: 'Welcome Email — Scripture Text', multiline: true, section: 'global' },
|
||||
{ key: 'welcomeEmailScriptureRef', label: 'Welcome Email — Scripture Reference', section: 'global' },
|
||||
{ key: 'welcomeEmailSignoff', label: 'Welcome Email — Signoff HTML', multiline: true, section: 'global' },
|
||||
{ key: 'welcomeEmailSignoff', label: 'Signoff', multiline: true, section: 'global' },
|
||||
// ── Email Templates section ──
|
||||
{ key: 'welcomeEmailSubject', label: 'Subject Line', section: 'email-templates' },
|
||||
{ key: 'welcomeEmailGreetingPrefix', label: 'Greeting Prefix (before subscriber name)', section: 'email-templates' },
|
||||
{ key: 'welcomeEmailIntro', label: 'Intro Paragraph', multiline: true, section: 'email-templates' },
|
||||
{ key: 'welcomeEmailCurrentSeries', label: 'Current Series Blurb', multiline: true, section: 'email-templates' },
|
||||
{ key: 'welcomeEmailStartHereTitle', label: 'Start Here Episode Title', section: 'email-templates' },
|
||||
{ key: 'welcomeEmailStartHereSummary', label: 'Start Here Episode Summary', multiline: true, section: 'email-templates' },
|
||||
{ key: 'welcomeEmailStartHereUrl', label: 'Start Here Episode URL', section: 'email-templates' },
|
||||
{ key: 'welcomeEmailWhatToExpect1', label: 'What to Expect — Line 1', multiline: true, section: 'email-templates' },
|
||||
{ key: 'welcomeEmailWhatToExpect2', label: 'What to Expect — Line 2', multiline: true, section: 'email-templates' },
|
||||
{ key: 'welcomeEmailWhatToExpect3', label: 'What to Expect — Line 3', multiline: true, section: 'email-templates' },
|
||||
{ key: 'welcomeEmailScripture', label: 'Closing Scripture Text', multiline: true, section: 'email-templates' },
|
||||
{ key: 'welcomeEmailScriptureRef', label: 'Closing Scripture Reference', section: 'email-templates' },
|
||||
{ key: 'welcomeEmailSignoff', label: 'Signoff', multiline: true, section: 'email-templates' },
|
||||
{ key: 'welcomeEmailSpotifyUrl', label: 'Spotify URL', section: 'email-templates' },
|
||||
{ key: 'welcomeEmailSpotifyBtnLabel', label: '"Listen on Spotify" button label', section: 'email-templates' },
|
||||
{ key: 'welcomeEmailAppleUrl', label: 'Apple Podcasts URL', section: 'email-templates' },
|
||||
{ key: 'welcomeEmailAppleBtnLabel', label: '"Apple Podcasts" button label', section: 'email-templates' },
|
||||
{ key: 'welcomeEmailAmazonUrl', label: 'Amazon Music URL', section: 'email-templates' },
|
||||
{ key: 'welcomeEmailStartHereLinkLabel', label: '"Start Here" link label', section: 'email-templates' },
|
||||
{ key: 'studyWelcomeEmailSubject', label: 'Subject Line', section: 'email-templates' },
|
||||
{ key: 'studyWelcomeEmailBody', label: 'Body Paragraph', multiline: true, section: 'email-templates' },
|
||||
{ key: 'studyWelcomeEmailCtaLabel', label: 'Button Label', section: 'email-templates' },
|
||||
{ key: 'studyWelcomeEmailCtaPath', label: 'Button Link (path or full URL)', section: 'email-templates' },
|
||||
{ key: 'studyWelcomeEmailSignoff', label: 'Signoff', multiline: true, section: 'email-templates' },
|
||||
{ key: 'studyDeletedEmailSubject', label: 'Subject Line', section: 'email-templates' },
|
||||
{ key: 'studyDeletedEmailBody', label: 'Body Paragraph', multiline: true, section: 'email-templates' },
|
||||
{ key: 'studyDeletedEmailCtaLabel', label: 'Button Label', section: 'email-templates' },
|
||||
{ key: 'studyDeletedEmailCtaPath', label: 'Button Link (path or full URL)', section: 'email-templates' },
|
||||
{ key: 'studyDeletedEmailSignoff', label: 'Signoff', multiline: true, section: 'email-templates' },
|
||||
{ key: 'studyReminderEmailSubjectPrefix', label: 'Subject Prefix (lesson title auto-appended)', section: 'email-templates' },
|
||||
{ key: 'studyReminderEmailBody', label: 'Body Paragraph', multiline: true, section: 'email-templates' },
|
||||
{ key: 'studyReminderEmailCtaLabel', label: 'Button Label', section: 'email-templates' },
|
||||
{ key: 'studyReminderEmailSignoff', label: 'Signoff', multiline: true, section: 'email-templates' },
|
||||
{ key: 'emailChangeSubject', label: 'Subject Line', section: 'email-templates' },
|
||||
{ key: 'emailChangeBody', label: 'Body Text', multiline: true, section: 'email-templates' },
|
||||
{ key: 'emailChangeCtaLabel', label: 'Button Label', section: 'email-templates' },
|
||||
]
|
||||
|
||||
function normalizeStudies(siteContent: SiteContent): StudyProgram[] {
|
||||
@@ -4480,6 +4626,11 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* EMAIL TEMPLATES */}
|
||||
{adminView === 'email-templates' && (
|
||||
<EmailTemplatesPanel form={form} handleChange={handleChange} renderSaveStatus={renderSaveStatus} />
|
||||
)}
|
||||
|
||||
{/* QUESTIONS */}
|
||||
{adminView === 'questions' && (
|
||||
<section className="admin-panel-section" aria-label="Q&A Management">
|
||||
|
||||
+109
@@ -6453,3 +6453,112 @@
|
||||
background: rgba(201, 168, 76, 0.08);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* ── Admin: Email Templates ── */
|
||||
.admin-et-card {
|
||||
border-radius: 10px;
|
||||
margin-bottom: 0.75rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(201, 168, 76, 0.12);
|
||||
border-left-width: 4px;
|
||||
background: #0b0b09;
|
||||
}
|
||||
|
||||
.admin-et-card--newsletter { border-left-color: #c9a84c; }
|
||||
.admin-et-card--study { border-left-color: #7abf7a; }
|
||||
.admin-et-card--transactional { border-left-color: #7aaacf; }
|
||||
|
||||
.admin-et-card--open {
|
||||
border-color: rgba(201, 168, 76, 0.3);
|
||||
border-left-width: 4px;
|
||||
}
|
||||
.admin-et-card--open.admin-et-card--newsletter { border-left-color: #c9a84c; }
|
||||
.admin-et-card--open.admin-et-card--study { border-left-color: #7abf7a; }
|
||||
.admin-et-card--open.admin-et-card--transactional { border-left-color: #7aaacf; }
|
||||
|
||||
.admin-et-card-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.9rem 1.1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.admin-et-card-header:hover {
|
||||
background: rgba(201, 168, 76, 0.04);
|
||||
}
|
||||
|
||||
.admin-et-card-icon {
|
||||
font-size: 1.4rem;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.admin-et-card-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-et-card-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-et-card-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: #f0e9cc;
|
||||
}
|
||||
|
||||
.admin-et-card-trigger {
|
||||
font-size: 0.78rem;
|
||||
color: #8a7f5a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-et-card-chevron {
|
||||
font-size: 0.75rem;
|
||||
color: #5a5440;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-et-badge {
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-et-badge--newsletter { background: rgba(201,168,76,0.15); color: #c9a84c; }
|
||||
.admin-et-badge--study { background: rgba(100,160,100,0.15); color: #7abf7a; }
|
||||
.admin-et-badge--transactional { background: rgba(100,140,200,0.15); color: #7aaacf; }
|
||||
|
||||
.admin-et-card-body {
|
||||
padding: 0 1.25rem 1.25rem;
|
||||
border-top: 1px solid rgba(201, 168, 76, 0.08);
|
||||
}
|
||||
|
||||
.admin-et-card-vars {
|
||||
font-size: 0.78rem;
|
||||
color: #6a6040;
|
||||
background: rgba(201, 168, 76, 0.05);
|
||||
border-radius: 5px;
|
||||
padding: 0.45rem 0.75rem;
|
||||
margin: 0.75rem 0 1rem;
|
||||
}
|
||||
|
||||
.admin-et-card-vars-label {
|
||||
color: #8a7f5a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -223,6 +223,31 @@ export interface SiteContent {
|
||||
welcomeEmailScripture: string
|
||||
welcomeEmailScriptureRef: string
|
||||
welcomeEmailSignoff: string
|
||||
// ── Newsletter Welcome Email CTA labels ──
|
||||
welcomeEmailSpotifyBtnLabel: string
|
||||
welcomeEmailAppleBtnLabel: string
|
||||
welcomeEmailStartHereLinkLabel: string
|
||||
// ── Study Account Welcome Email ──
|
||||
studyWelcomeEmailSubject: string
|
||||
studyWelcomeEmailBody: string
|
||||
studyWelcomeEmailCtaLabel: string
|
||||
studyWelcomeEmailCtaPath: string
|
||||
studyWelcomeEmailSignoff: string
|
||||
// ── Study Account Deleted Email ──
|
||||
studyDeletedEmailSubject: string
|
||||
studyDeletedEmailBody: string
|
||||
studyDeletedEmailCtaLabel: string
|
||||
studyDeletedEmailCtaPath: string
|
||||
studyDeletedEmailSignoff: string
|
||||
// ── Study Lesson Reminder Email ──
|
||||
studyReminderEmailSubjectPrefix: string
|
||||
studyReminderEmailBody: string
|
||||
studyReminderEmailCtaLabel: string
|
||||
studyReminderEmailSignoff: string
|
||||
// ── Email Change Confirmation ──
|
||||
emailChangeSubject: string
|
||||
emailChangeBody: string
|
||||
emailChangeCtaLabel: string
|
||||
// ── Header ──
|
||||
headerFollowLabel: string
|
||||
// ── Cookie banner ──
|
||||
@@ -387,6 +412,26 @@ export const DEFAULTS: SiteContent = {
|
||||
welcomeEmailScripture: 'For the grace of God has appeared, bringing salvation to all people.',
|
||||
welcomeEmailScriptureRef: 'Titus 2:11 - BSB',
|
||||
welcomeEmailSignoff: 'Grace and peace,\nNate',
|
||||
welcomeEmailSpotifyBtnLabel: 'Listen on Spotify',
|
||||
welcomeEmailAppleBtnLabel: 'Apple Podcasts',
|
||||
welcomeEmailStartHereLinkLabel: 'Open Start Here page',
|
||||
studyWelcomeEmailSubject: 'Welcome to the Study Community',
|
||||
studyWelcomeEmailBody: 'Your student account is ready. Open your studies and continue learning, or manage your account details anytime.',
|
||||
studyWelcomeEmailCtaLabel: 'Open Studies',
|
||||
studyWelcomeEmailCtaPath: '/study',
|
||||
studyWelcomeEmailSignoff: 'Grace and peace,\nVerse by Verse with Nate',
|
||||
studyDeletedEmailSubject: 'Your study account was deleted',
|
||||
studyDeletedEmailBody: 'This confirms your study account and saved notes were deleted. If this was not you, please contact us immediately.',
|
||||
studyDeletedEmailCtaLabel: 'Create a New Account',
|
||||
studyDeletedEmailCtaPath: '/study/signup',
|
||||
studyDeletedEmailSignoff: 'Verse by Verse with Nate',
|
||||
studyReminderEmailSubjectPrefix: 'New lesson available:',
|
||||
studyReminderEmailBody: 'A new lesson has been unlocked in your study track. Open it below to continue where you left off.',
|
||||
studyReminderEmailCtaLabel: 'Open the Lesson',
|
||||
studyReminderEmailSignoff: 'Grace and peace,\nVerse by Verse with Nate',
|
||||
emailChangeSubject: 'Confirm your new email address',
|
||||
emailChangeBody: 'Click the link below to confirm your new account email. If you did not request this change, ignore this message.',
|
||||
emailChangeCtaLabel: 'Confirm Email Change',
|
||||
headerFollowLabel: 'Follow on Spotify',
|
||||
cookieBannerText: 'We use optional analytics cookies to measure visits and location trends for site improvement.',
|
||||
customLinks: [],
|
||||
|
||||
Reference in New Issue
Block a user