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
+63
-1
@@ -78,6 +78,9 @@ interface StudyUserRecord {
|
||||
noteCount: number
|
||||
subscribeNewsletter: boolean
|
||||
studyRemindersEnabled: boolean
|
||||
reminderFrequency: 'immediate' | 'weekly'
|
||||
remindersPausedUntil: string | null
|
||||
reengagementSentAt: Record<string, string>
|
||||
}
|
||||
|
||||
function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) {
|
||||
@@ -89,6 +92,7 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) {
|
||||
const [newPassword, setNewPassword] = useState<Record<string, string>>({})
|
||||
const [msg, setMsg] = useState<Record<string, string>>({})
|
||||
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null)
|
||||
const [reengagementTier, setReengagementTier] = useState<Record<string, string>>({})
|
||||
|
||||
function flashMsg(id: string, text: string) {
|
||||
setMsg(prev => ({ ...prev, [id]: text }))
|
||||
@@ -154,6 +158,20 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) {
|
||||
} catch (e) { flashMsg(user.id, e instanceof Error ? e.message : 'Error') }
|
||||
}
|
||||
|
||||
async function handleSendReengagement(user: StudyUserRecord) {
|
||||
const tier = reengagementTier[user.id] ?? 't1'
|
||||
try {
|
||||
const res = await fetch(`/api/admin/study-users/${user.id}/send-reengagement`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tier }),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.message ?? 'Error')
|
||||
flashMsg(user.id, `Tier ${tier.slice(1)} re-engagement email sent.`)
|
||||
} catch (e) { flashMsg(user.id, e instanceof Error ? e.message : 'Error') }
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
try {
|
||||
await fetch(`/api/admin/study-users/${id}`, { method: 'DELETE' })
|
||||
@@ -222,7 +240,13 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) {
|
||||
<span>Last studied: {user.lastStudiedDate ?? '—'}</span>
|
||||
<span>Streak: {user.currentStreak ?? 0} 🔥 (best: {user.longestStreak ?? 0})</span>
|
||||
<span>Newsletter: {user.subscribeNewsletter ? 'Yes' : 'No'}</span>
|
||||
<span>Reminders: {user.studyRemindersEnabled ? 'On' : 'Off'}</span>
|
||||
<span>Reminders: {user.studyRemindersEnabled ? 'On' : 'Off'} · {user.reminderFrequency === 'weekly' ? 'Weekly digest' : 'Per-lesson'}</span>
|
||||
{user.remindersPausedUntil && new Date(user.remindersPausedUntil) > new Date() && (
|
||||
<span style={{ color: '#c9a84c' }}>Paused until {new Date(user.remindersPausedUntil).toLocaleDateString()}</span>
|
||||
)}
|
||||
{user.reengagementSentAt && Object.keys(user.reengagementSentAt).length > 0 && (
|
||||
<span>Re-engagement sent: {Object.keys(user.reengagementSentAt).sort().join(', ')}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Display Name */}
|
||||
@@ -292,6 +316,25 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Re-engagement */}
|
||||
{user.enrolledStudies.length > 0 && (
|
||||
<div className="admin-study-user-section">
|
||||
<h4>Send Re-engagement Email</h4>
|
||||
<div className="admin-study-user-row">
|
||||
<select
|
||||
value={reengagementTier[user.id] ?? 't1'}
|
||||
onChange={e => setReengagementTier(prev => ({ ...prev, [user.id]: e.target.value }))}
|
||||
>
|
||||
<option value="t1">Tier 1 (gentle nudge)</option>
|
||||
<option value="t2">Tier 2 (momentum)</option>
|
||||
<option value="t3">Tier 3 (still here)</option>
|
||||
</select>
|
||||
<button type="button" className="btn-admin-save" onClick={() => handleSendReengagement(user)}>Send</button>
|
||||
</div>
|
||||
<p className="admin-stats-note" style={{ marginTop: '0.25rem' }}>Sends the selected tier email immediately, regardless of inactivity threshold.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{msg[user.id] && <p className="admin-study-user-msg">{msg[user.id]}</p>}
|
||||
|
||||
{/* Delete */}
|
||||
@@ -419,6 +462,18 @@ function EmailTemplatesPanel({
|
||||
fieldKeys={['studyReengagementDays','studyReengagementT1Subject','studyReengagementT1Body','studyReengagementT1Cta','studyReengagementT2Subject','studyReengagementT2Body','studyReengagementT2Cta','studyReengagementT3Subject','studyReengagementT3Body','studyReengagementT3Cta']}
|
||||
openKey="study-reengagement" />
|
||||
|
||||
<EmailTemplateCard type="study" icon="🏅" title="Study Account — Completion Certificate"
|
||||
trigger="A student earns their certificate by completing all lessons in a study"
|
||||
autoVars="Student's name, study title, and a link to the public certificate are auto-inserted"
|
||||
fieldKeys={['studyCertificateEmailSubject','studyCertificateEmailBody','studyCertificateEmailCtaLabel','studyCertificateEmailSignoff']}
|
||||
openKey="study-certificate" />
|
||||
|
||||
<EmailTemplateCard type="study" icon="📅" title="Study Account — Weekly Lesson Digest"
|
||||
trigger="Students who choose 'Weekly Digest' mode receive one summary email per week listing all newly released lessons"
|
||||
autoVars="Student's name, lesson list (title, scripture reference, link), and a one-click unsubscribe link"
|
||||
fieldKeys={['studyWeeklyDigestEmailSubject','studyWeeklyDigestEmailBody','studyWeeklyDigestEmailSignoff']}
|
||||
openKey="study-weekly-digest" />
|
||||
|
||||
<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)"
|
||||
@@ -927,6 +982,13 @@ const FIELDS: Array<{ key: StringField; label: string; multiline?: boolean; sect
|
||||
{ key: 'studyReengagementT3Subject', label: 'Tier 3 — Subject Line', section: 'email-templates' },
|
||||
{ key: 'studyReengagementT3Body', label: 'Tier 3 — Body', multiline: true, section: 'email-templates' },
|
||||
{ key: 'studyReengagementT3Cta', label: 'Tier 3 — Button Label', section: 'email-templates' },
|
||||
{ key: 'studyCertificateEmailSubject', label: 'Subject Line', section: 'email-templates' },
|
||||
{ key: 'studyCertificateEmailBody', label: 'Body Paragraph', multiline: true, section: 'email-templates' },
|
||||
{ key: 'studyCertificateEmailCtaLabel', label: 'Button Label', section: 'email-templates' },
|
||||
{ key: 'studyCertificateEmailSignoff', label: 'Signoff', multiline: true, section: 'email-templates' },
|
||||
{ key: 'studyWeeklyDigestEmailSubject', label: 'Subject Line', section: 'email-templates' },
|
||||
{ key: 'studyWeeklyDigestEmailBody', label: 'Intro Paragraph', multiline: true, section: 'email-templates' },
|
||||
{ key: 'studyWeeklyDigestEmailSignoff', 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' },
|
||||
|
||||
+165
-31
@@ -44,6 +44,8 @@ type StudyAccountOverview = {
|
||||
displayName: string
|
||||
subscribeNewsletter: boolean
|
||||
studyRemindersEnabled: boolean
|
||||
reminderFrequency?: string
|
||||
remindersPausedUntil?: string | null
|
||||
avatarUrl?: string
|
||||
}
|
||||
stats: {
|
||||
@@ -69,6 +71,7 @@ type StudyNotesMap = Record<string, string>
|
||||
|
||||
type StudyProgress = {
|
||||
completedSectionIds: string[]
|
||||
bookmarkedSectionIds: string[]
|
||||
}
|
||||
|
||||
type StudyCommunityReply = {
|
||||
@@ -1256,7 +1259,7 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
||||
const [auth, setAuth] = useState<StudyAuthState>({ checked: false, authenticated: false, username: '', enrolledStudySlugs: [] })
|
||||
const [enrolling, setEnrolling] = useState(false)
|
||||
const [enrollMessage, setEnrollMessage] = useState('')
|
||||
const [studyProgress, setStudyProgress] = useState<StudyProgress>({ completedSectionIds: [] })
|
||||
const [studyProgress, setStudyProgress] = useState<StudyProgress>({ completedSectionIds: [], bookmarkedSectionIds: [] })
|
||||
const [progressLoading, setProgressLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1305,14 +1308,14 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
||||
let cancelled = false
|
||||
setProgressLoading(true)
|
||||
|
||||
readJson<{ studySlug: string; completedSectionIds: string[] }>(`/api/study-progress/${encodeURIComponent(study.slug)}`)
|
||||
readJson<{ studySlug: string; completedSectionIds: string[]; bookmarkedSectionIds: string[] }>(`/api/study-progress/${encodeURIComponent(study.slug)}`)
|
||||
.then(data => {
|
||||
if (cancelled) return
|
||||
setStudyProgress({ completedSectionIds: Array.isArray(data.completedSectionIds) ? data.completedSectionIds : [] })
|
||||
setStudyProgress({ completedSectionIds: Array.isArray(data.completedSectionIds) ? data.completedSectionIds : [], bookmarkedSectionIds: Array.isArray(data.bookmarkedSectionIds) ? data.bookmarkedSectionIds : [] })
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return
|
||||
setStudyProgress({ completedSectionIds: [] })
|
||||
setStudyProgress({ completedSectionIds: [], bookmarkedSectionIds: [] })
|
||||
})
|
||||
.finally(() => {
|
||||
if (cancelled) return
|
||||
@@ -1499,6 +1502,8 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
||||
const noteMapDirtyRef = useRef(false)
|
||||
const [contextNoteMenu, setContextNoteMenu] = useState<{ x: number; y: number; text: string; anchor: NoteAnchorId } | null>(null)
|
||||
const [completedSectionIds, setCompletedSectionIds] = useState<string[]>([])
|
||||
const [bookmarkedSectionIds, setBookmarkedSectionIds] = useState<string[]>([])
|
||||
const [bookmarkSaving, setBookmarkSaving] = useState(false)
|
||||
const [progressSaving, setProgressSaving] = useState(false)
|
||||
const [progressMessage, setProgressMessage] = useState('')
|
||||
const [checkpointAnswers, setCheckpointAnswers] = useState<Record<number, string>>({})
|
||||
@@ -1568,14 +1573,16 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
||||
let cancelled = false
|
||||
setProgressMessage('')
|
||||
|
||||
readJson<{ studySlug: string; completedSectionIds: string[] }>(`/api/study-progress/${encodeURIComponent(currentStudySlug)}`)
|
||||
readJson<{ studySlug: string; completedSectionIds: string[]; bookmarkedSectionIds: string[] }>(`/api/study-progress/${encodeURIComponent(currentStudySlug)}`)
|
||||
.then(data => {
|
||||
if (cancelled) return
|
||||
setCompletedSectionIds(Array.isArray(data.completedSectionIds) ? data.completedSectionIds : [])
|
||||
setBookmarkedSectionIds(Array.isArray(data.bookmarkedSectionIds) ? data.bookmarkedSectionIds : [])
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return
|
||||
setCompletedSectionIds([])
|
||||
setBookmarkedSectionIds([])
|
||||
})
|
||||
.finally(() => {
|
||||
if (cancelled) return
|
||||
@@ -2017,30 +2024,56 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
||||
<h2 style={{ margin: 0 }}>Lesson Progress</h2>
|
||||
<p style={{ margin: '0.25rem 0 0', fontSize: '0.85rem', color: '#5a5440' }}>No checkpoint questions for this lesson.</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={lessonCompleted ? 'btn-secondary' : 'btn-primary'}
|
||||
style={lessonCompleted ? { borderColor: '#2196f3', color: '#2196f3' } : {}}
|
||||
disabled={progressSaving}
|
||||
onClick={async () => {
|
||||
if (!study || !section?.id) return
|
||||
setProgressSaving(true)
|
||||
const wasComplete = lessonCompleted
|
||||
try {
|
||||
const method = wasComplete ? 'DELETE' : 'POST'
|
||||
const data = await readJson<{ ok: boolean; completedSectionIds: string[] }>(
|
||||
`/api/study-progress/${encodeURIComponent(study.slug)}/${encodeURIComponent(section.id)}`,
|
||||
{ method }
|
||||
)
|
||||
const newIds = Array.isArray(data.completedSectionIds) ? data.completedSectionIds : []
|
||||
setCompletedSectionIds(newIds)
|
||||
if (!wasComplete) checkAndShowCelebration(newIds, false)
|
||||
} catch { /* ignore */ }
|
||||
finally { setProgressSaving(false) }
|
||||
}}
|
||||
>
|
||||
{progressSaving ? '…' : lessonCompleted ? '✓ Completed' : 'Mark as Completed'}
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
type="button"
|
||||
className={lessonCompleted ? 'btn-secondary' : 'btn-primary'}
|
||||
style={lessonCompleted ? { borderColor: '#2196f3', color: '#2196f3' } : {}}
|
||||
disabled={progressSaving}
|
||||
onClick={async () => {
|
||||
if (!study || !section?.id) return
|
||||
setProgressSaving(true)
|
||||
const wasComplete = lessonCompleted
|
||||
try {
|
||||
const method = wasComplete ? 'DELETE' : 'POST'
|
||||
const data = await readJson<{ ok: boolean; completedSectionIds: string[] }>(
|
||||
`/api/study-progress/${encodeURIComponent(study.slug)}/${encodeURIComponent(section.id)}`,
|
||||
{ method }
|
||||
)
|
||||
const newIds = Array.isArray(data.completedSectionIds) ? data.completedSectionIds : []
|
||||
setCompletedSectionIds(newIds)
|
||||
if (!wasComplete) checkAndShowCelebration(newIds, false)
|
||||
} catch { /* ignore */ }
|
||||
finally { setProgressSaving(false) }
|
||||
}}
|
||||
>
|
||||
{progressSaving ? '…' : lessonCompleted ? '✓ Completed' : 'Mark as Completed'}
|
||||
</button>
|
||||
{isEnrolled && section?.id && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-secondary"
|
||||
style={bookmarkedSectionIds.includes(section.id) ? { borderColor: '#c9a84c', color: '#c9a84c' } : {}}
|
||||
disabled={bookmarkSaving}
|
||||
title={bookmarkedSectionIds.includes(section.id) ? 'Remove bookmark' : 'Bookmark this lesson'}
|
||||
onClick={async () => {
|
||||
if (!study || !section?.id) return
|
||||
setBookmarkSaving(true)
|
||||
const isBookmarked = bookmarkedSectionIds.includes(section.id)
|
||||
try {
|
||||
const data = await readJson<{ ok: boolean; bookmarkedSectionIds: string[] }>(
|
||||
`/api/study-progress/${encodeURIComponent(study.slug)}/${encodeURIComponent(section.id)}/bookmark`,
|
||||
{ method: isBookmarked ? 'DELETE' : 'POST' }
|
||||
)
|
||||
setBookmarkedSectionIds(Array.isArray(data.bookmarkedSectionIds) ? data.bookmarkedSectionIds : [])
|
||||
} catch { /* ignore */ }
|
||||
finally { setBookmarkSaving(false) }
|
||||
}}
|
||||
>
|
||||
{bookmarkSaving ? '…' : bookmarkedSectionIds.includes(section.id) ? '🔖 Bookmarked' : '🔖 Bookmark'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
)}
|
||||
|
||||
@@ -2439,6 +2472,8 @@ export function StudyAccountPage() {
|
||||
|
||||
const [subscribeNewsletter, setSubscribeNewsletter] = useState(true)
|
||||
const [studyRemindersEnabled, setStudyRemindersEnabled] = useState(true)
|
||||
const [reminderFrequency, setReminderFrequency] = useState<'immediate' | 'weekly'>('immediate')
|
||||
const [remindersPausedUntil, setRemindersPausedUntil] = useState<string | null>(null)
|
||||
const [prefMessage, setPrefMessage] = useState('')
|
||||
const [prefBusy, setPrefBusy] = useState(false)
|
||||
const [accountModal, setAccountModal] = useState<'none' | 'enrollments' | 'changeEmail' | 'changePassword' | 'profile' | 'preferences' | 'export' | 'choose2fa' | 'setup2fa' | 'setup2fa-email' | 'disable2fa' | 'recoveryCodes'>('none')
|
||||
@@ -2509,6 +2544,8 @@ export function StudyAccountPage() {
|
||||
setAvatarUrlInput(data.profile.avatarUrl ?? '')
|
||||
setSubscribeNewsletter(data.profile.subscribeNewsletter !== false)
|
||||
setStudyRemindersEnabled(data.profile.studyRemindersEnabled === true)
|
||||
setReminderFrequency(data.profile.reminderFrequency === 'weekly' ? 'weekly' : 'immediate')
|
||||
setRemindersPausedUntil(data.profile.remindersPausedUntil ?? null)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoadingOverview(false))
|
||||
@@ -2541,6 +2578,8 @@ export function StudyAccountPage() {
|
||||
setAvatarUrlInput(data.profile.avatarUrl ?? '')
|
||||
setSubscribeNewsletter(data.profile.subscribeNewsletter !== false)
|
||||
setStudyRemindersEnabled(data.profile.studyRemindersEnabled === true)
|
||||
setReminderFrequency(data.profile.reminderFrequency === 'weekly' ? 'weekly' : 'immediate')
|
||||
setRemindersPausedUntil(data.profile.remindersPausedUntil ?? null)
|
||||
}
|
||||
|
||||
async function handleAvatarUpload(file: File) {
|
||||
@@ -2600,12 +2639,14 @@ export function StudyAccountPage() {
|
||||
setPrefBusy(true)
|
||||
setPrefMessage('')
|
||||
try {
|
||||
const data = await readJson<{ ok: boolean; subscribeNewsletter: boolean; studyRemindersEnabled: boolean }>('/api/study-account/preferences', {
|
||||
const data = await readJson<{ ok: boolean; subscribeNewsletter: boolean; studyRemindersEnabled: boolean; reminderFrequency: string; remindersPausedUntil: string | null }>('/api/study-account/preferences', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ subscribeNewsletter, studyRemindersEnabled }),
|
||||
body: JSON.stringify({ subscribeNewsletter, studyRemindersEnabled, reminderFrequency, remindersPausedUntil }),
|
||||
})
|
||||
setAuth(prev => ({ ...prev, subscribeNewsletter: data.subscribeNewsletter, studyRemindersEnabled: data.studyRemindersEnabled }))
|
||||
setReminderFrequency(data.reminderFrequency === 'weekly' ? 'weekly' : 'immediate')
|
||||
setRemindersPausedUntil(data.remindersPausedUntil ?? null)
|
||||
setPrefMessage('Preferences updated.')
|
||||
await refreshOverview()
|
||||
} catch (err) {
|
||||
@@ -3008,6 +3049,99 @@ export function StudyAccountPage() {
|
||||
<input type="checkbox" checked={studyRemindersEnabled} onChange={e => setStudyRemindersEnabled(e.target.checked)} />
|
||||
<span>Send email reminders when a new lesson is released</span>
|
||||
</label>
|
||||
{studyRemindersEnabled && (
|
||||
<div style={{ marginLeft: '2rem', display: 'grid', gap: '0.75rem' }}>
|
||||
<div>
|
||||
<p style={{ margin: '0 0 0.4rem', fontSize: '0.875rem', color: '#b9b09b' }}>Reminder frequency</p>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReminderFrequency('immediate')}
|
||||
style={{
|
||||
padding: '0.35rem 0.75rem',
|
||||
borderRadius: '0.375rem',
|
||||
border: reminderFrequency === 'immediate' ? '1px solid #d4af7a' : '1px solid #444',
|
||||
background: reminderFrequency === 'immediate' ? '#2a2218' : 'transparent',
|
||||
color: reminderFrequency === 'immediate' ? '#d4af7a' : '#b9b09b',
|
||||
fontSize: '0.875rem',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Per-lesson
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReminderFrequency('weekly')}
|
||||
style={{
|
||||
padding: '0.35rem 0.75rem',
|
||||
borderRadius: '0.375rem',
|
||||
border: reminderFrequency === 'weekly' ? '1px solid #d4af7a' : '1px solid #444',
|
||||
background: reminderFrequency === 'weekly' ? '#2a2218' : 'transparent',
|
||||
color: reminderFrequency === 'weekly' ? '#d4af7a' : '#b9b09b',
|
||||
fontSize: '0.875rem',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Weekly digest
|
||||
</button>
|
||||
</div>
|
||||
<p style={{ margin: '0.35rem 0 0', fontSize: '0.8rem', color: '#7a7060' }}>
|
||||
{reminderFrequency === 'weekly'
|
||||
? 'Receive one weekly email summarizing all new lessons.'
|
||||
: 'Receive an email each time a new lesson is released.'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style={{ margin: '0 0 0.4rem', fontSize: '0.875rem', color: '#b9b09b' }}>
|
||||
Pause reminders
|
||||
{remindersPausedUntil && new Date(remindersPausedUntil) > new Date()
|
||||
? ` · paused until ${new Date(remindersPausedUntil).toLocaleDateString()}`
|
||||
: ''}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
{[7, 14, 30].map(days => (
|
||||
<button
|
||||
key={days}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const d = new Date()
|
||||
d.setDate(d.getDate() + days)
|
||||
setRemindersPausedUntil(d.toISOString())
|
||||
}}
|
||||
style={{
|
||||
padding: '0.35rem 0.75rem',
|
||||
borderRadius: '0.375rem',
|
||||
border: '1px solid #444',
|
||||
background: 'transparent',
|
||||
color: '#b9b09b',
|
||||
fontSize: '0.875rem',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{days} days
|
||||
</button>
|
||||
))}
|
||||
{remindersPausedUntil && new Date(remindersPausedUntil) > new Date() && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRemindersPausedUntil(null)}
|
||||
style={{
|
||||
padding: '0.35rem 0.75rem',
|
||||
borderRadius: '0.375rem',
|
||||
border: '1px solid #555',
|
||||
background: 'transparent',
|
||||
color: '#7a7060',
|
||||
fontSize: '0.875rem',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Clear pause
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<label className="contact-consent" style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<input type="checkbox" checked={subscribeNewsletter} onChange={e => setSubscribeNewsletter(e.target.checked)} />
|
||||
<span>Subscribe to newsletter updates</span>
|
||||
|
||||
@@ -259,6 +259,15 @@ export interface SiteContent {
|
||||
studyReengagementT3Subject: string
|
||||
studyReengagementT3Body: string
|
||||
studyReengagementT3Cta: string
|
||||
// ── Study Completion Certificate Email ──
|
||||
studyCertificateEmailSubject: string
|
||||
studyCertificateEmailBody: string
|
||||
studyCertificateEmailCtaLabel: string
|
||||
studyCertificateEmailSignoff: string
|
||||
// ── Study Weekly Lesson Digest Email ──
|
||||
studyWeeklyDigestEmailSubject: string
|
||||
studyWeeklyDigestEmailBody: string
|
||||
studyWeeklyDigestEmailSignoff: string
|
||||
// ── Email Change Confirmation ──
|
||||
emailChangeSubject: string
|
||||
emailChangeBody: string
|
||||
@@ -472,6 +481,13 @@ export const DEFAULTS: SiteContent = {
|
||||
studyReengagementT3Subject: 'Your progress is still here',
|
||||
studyReengagementT3Body: "Your study progress is still intact and waiting. There's no deadline — come back whenever you're ready.",
|
||||
studyReengagementT3Cta: 'Open Your Study',
|
||||
studyCertificateEmailSubject: 'Your certificate is ready',
|
||||
studyCertificateEmailBody: 'Congratulations — you have completed every lesson in this study. Your certificate is ready to view and share.',
|
||||
studyCertificateEmailCtaLabel: 'View Certificate',
|
||||
studyCertificateEmailSignoff: 'Grace and peace,\nVerse by Verse with Nate',
|
||||
studyWeeklyDigestEmailSubject: 'Your weekly study digest',
|
||||
studyWeeklyDigestEmailBody: 'Here are the lessons released this week in your study track. Open any to continue where you left off.',
|
||||
studyWeeklyDigestEmailSignoff: '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',
|
||||
|
||||
Reference in new issue
Block a user