Add /contacts and /calendar pages; email signature settings; v1.1.12
- /contacts: standalone page with hybrid contact list (submissions + manual entry), inline edit, search, archive - /calendar: monthly release scheduling calendar reading/writing podcast checklist episode dates - /email settings: editable signature panel; signature persisted server-side and injected into outgoing emails - Move contacts out of /admin panel (now links to /contacts route) - Partial PATCH for contact submissions (name, notes, archived independently) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
queueDraftQuestionsWrite,
|
||||
queueReplyTemplatesWrite,
|
||||
queueReplyHistoryWrite,
|
||||
queueEmailSettingsWrite,
|
||||
normalizeContactEmailStatus,
|
||||
normalizeMessageType,
|
||||
sanitizeReplyTemplates,
|
||||
@@ -398,18 +399,54 @@ export function register(app) {
|
||||
res.json({ submissions: state.contactSubmissions.slice(0, 300) })
|
||||
})
|
||||
|
||||
app.post('/api/admin-contact-submissions/add', requireAdminAuth, (req, res) => {
|
||||
const name = typeof req.body?.name === 'string' ? req.body.name.trim() : ''
|
||||
const email = typeof req.body?.email === 'string' ? req.body.email.trim() : ''
|
||||
const notes = typeof req.body?.notes === 'string' ? req.body.notes.trim().slice(0, 2000) : ''
|
||||
|
||||
if (!name && !email) {
|
||||
res.status(400).json({ message: 'Name or email is required.' }); return
|
||||
}
|
||||
if (email && !/^[a-zA-Z0-9._%+\-]{1,64}@[a-zA-Z0-9.\-]{1,253}\.[a-zA-Z]{2,}$/.test(email)) {
|
||||
res.status(400).json({ message: 'Invalid email address.' }); return
|
||||
}
|
||||
|
||||
const submission = {
|
||||
id: randomUUID(),
|
||||
submittedAt: new Date().toISOString(),
|
||||
name: name.slice(0, 200),
|
||||
email,
|
||||
message: '',
|
||||
messageType: 'general',
|
||||
subscribe: false,
|
||||
archived: false,
|
||||
source: 'manual',
|
||||
notes,
|
||||
emailStatus: normalizeContactEmailStatus(null, false),
|
||||
}
|
||||
|
||||
state.contactSubmissions.unshift(submission)
|
||||
state.contactSubmissions = state.contactSubmissions.slice(0, MAX_CONTACT_SUBMISSIONS)
|
||||
queueContactSubmissionsWrite()
|
||||
res.json({ ok: true, submission })
|
||||
})
|
||||
|
||||
app.patch('/api/admin-contact-submissions/:id', requireAdminAuth, (req, res) => {
|
||||
const { id } = req.params
|
||||
if (typeof id !== 'string' || !id.trim()) {
|
||||
res.status(400).json({ message: 'Invalid submission id.' }); return
|
||||
}
|
||||
|
||||
const archived = req.body?.archived === true
|
||||
const patch = {}
|
||||
if (typeof req.body?.archived === 'boolean') patch.archived = req.body.archived
|
||||
if (typeof req.body?.name === 'string') patch.name = req.body.name.trim().slice(0, 200)
|
||||
if (typeof req.body?.notes === 'string') patch.notes = req.body.notes.trim().slice(0, 2000)
|
||||
|
||||
let found = false
|
||||
state.contactSubmissions = state.contactSubmissions.map(item => {
|
||||
if (item.id !== id) return item
|
||||
found = true
|
||||
return { ...item, archived }
|
||||
return { ...item, ...patch }
|
||||
})
|
||||
|
||||
if (!found) {
|
||||
@@ -417,7 +454,7 @@ export function register(app) {
|
||||
}
|
||||
|
||||
queueContactSubmissionsWrite()
|
||||
res.json({ ok: true, archived })
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
app.delete('/api/admin-contact-submissions/:id', requireAdminAuth, (req, res) => {
|
||||
@@ -493,11 +530,12 @@ export function register(app) {
|
||||
}
|
||||
|
||||
const recipientName = splitName(submission.name).firstName || submission.name || 'friend'
|
||||
const html = buildAdminReplyTemplate({ recipientName, message })
|
||||
const signature = state.emailSettings?.signature ?? 'Grace and peace,\nVerse by Verse with Nate'
|
||||
const html = buildAdminReplyTemplate({ recipientName, message, signature })
|
||||
const replyToAddress = getResendReplyToAddress()
|
||||
const defaultFrom = getResendFromAddress() || ADMIN_REPLY_FROM
|
||||
const fromAddress = ADMIN_REPLY_FROM_OPTIONS.includes(requestedFrom) ? requestedFrom : defaultFrom
|
||||
const text = `Hi ${recipientName},\n\n${message}\n\nGrace and peace,\nVerse by Verse with Nate\n${replyToAddress}`
|
||||
const text = `Hi ${recipientName},\n\n${message}\n\n${signature}\n${replyToAddress}`
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
|
||||
const sendResult = await sendResendEmailWithRetry({
|
||||
@@ -576,11 +614,12 @@ export function register(app) {
|
||||
}
|
||||
|
||||
const recipientName = toName ? splitName(toName).firstName || toName : 'friend'
|
||||
const html = buildAdminReplyTemplate({ recipientName, message })
|
||||
const signature = state.emailSettings?.signature ?? 'Grace and peace,\nVerse by Verse with Nate'
|
||||
const html = buildAdminReplyTemplate({ recipientName, message, signature })
|
||||
const defaultFrom = getResendFromAddress() || ADMIN_REPLY_FROM
|
||||
const fromAddress = ADMIN_REPLY_FROM_OPTIONS.includes(requestedFrom) ? requestedFrom : defaultFrom
|
||||
const replyToAddress = getResendReplyToAddress()
|
||||
const text = `Hi ${recipientName},\n\n${message}\n\nGrace and peace,\nVerse by Verse with Nate`
|
||||
const text = `Hi ${recipientName},\n\n${message}\n\n${signature}`
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
|
||||
await sendResendEmailWithRetry({
|
||||
@@ -617,6 +656,19 @@ export function register(app) {
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/admin-email-settings', requireAdminAuth, (_req, res) => {
|
||||
res.json(state.emailSettings ?? { signature: 'Grace and peace,\nVerse by Verse with Nate' })
|
||||
})
|
||||
|
||||
app.put('/api/admin-email-settings', requireAdminAuth, (req, res) => {
|
||||
const signature = typeof req.body?.signature === 'string'
|
||||
? req.body.signature.slice(0, 1000)
|
||||
: (state.emailSettings?.signature ?? 'Grace and peace,\nVerse by Verse with Nate')
|
||||
state.emailSettings = { ...state.emailSettings, signature }
|
||||
queueEmailSettingsWrite()
|
||||
res.json({ ok: true, settings: state.emailSettings })
|
||||
})
|
||||
|
||||
app.get('/api/admin-subscribers', requireAdminAuth, (_req, res) => {
|
||||
const seen = new Set()
|
||||
const subscribers = state.contactSubmissions
|
||||
|
||||
Reference in New Issue
Block a user