Implement email tab redesign, archive support, manual Q&A creation, URL linking, and question admin tools

- Add dedicated Emails tab with inbox/archive views and two-pane layout
- Implement archive/unarchive for contact submissions with persisted state
- Add manual question creation endpoint and admin form (non-contact origin)
- Implement URL auto-linking in Q&A answers with safe rendering
- Add question admin tools: search, filter (All/Pending/Approved/Answered/Unanswered), pagination
- Expand admin panel widths to reduce cramping
- Restore and enhance Asset Manager table layout
- Update email reply template with fixed from-address and HTML support
This commit is contained in:
nmemmert
2026-05-07 09:43:16 -04:00
parent bc3d906e64
commit 583a9b7b66
8 changed files with 1420 additions and 36 deletions
+13
View File
@@ -174,6 +174,9 @@ function sanitizeCustomLinks(value) {
description: typeof item.description === 'string' ? item.description.trim().slice(0, 4000) : '',
amazonUrl: sanitizeUrl(item.amazonUrl),
amazonLabel: typeof item.amazonLabel === 'string' ? item.amazonLabel.trim().slice(0, 120) : '',
tags: Array.isArray(item.tags)
? item.tags.filter(tag => typeof tag === 'string').map(tag => tag.trim()).filter(Boolean).slice(0, 20)
: [],
placement,
}
})
@@ -188,6 +191,9 @@ function sanitizeCustomBlocks(value) {
id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(),
heading: typeof item.heading === 'string' ? item.heading.trim().slice(0, 140) : '',
body: typeof item.body === 'string' ? item.body.trim().slice(0, 4000) : '',
page: item?.page === 'homepage' || item?.page === 'start-here' || item?.page === 'episodes' || item?.page === 'downloads' || item?.page === 'about' || item?.page === 'contact' || item?.page === 'questions'
? item.page
: 'downloads',
}))
.filter(item => item.heading || item.body)
}
@@ -235,6 +241,13 @@ function sanitizeArchivedSeries(value) {
studyGuideUrl: sanitizeUrl(item.studyGuideUrl),
resourceLinks: sanitizeArchivedSeriesResourceLinks(item.resourceLinks),
notes: sanitizeArchivedSeriesNotes(item.notes),
episodeRange:
Number.isInteger(item?.episodeRange?.from)
&& Number.isInteger(item?.episodeRange?.to)
&& item.episodeRange.from > 0
&& item.episodeRange.to >= item.episodeRange.from
? { from: item.episodeRange.from, to: item.episodeRange.to }
: undefined,
}))
.filter(
item =>