Fix contact form crash, analytics range filter, Scripture Index parsing, and unsafe-delete UI bugs
Publish Container / docker (push) Successful in 13m12s

- server/routes/contact.js: import fireWebhooks (was undefined, throwing on every submission)
- server/routes/analytics.js: filter admin-stats by row.at, the field recordVisitor actually sets, instead of the never-populated row.visitedAt
- src/App.tsx: parseSIRef now supports chapter-spanning verse ranges (e.g. "5:15-6:2"); SI_BOOK_MAP indexes both spaced and unspaced book names so multi-word Bible Book values from the admin field resolve correctly
- src/App.css / src/App.tsx: fix Episodes/Studies nav dropdown menus being unclickable — a positioning gap between the trigger and menu created a hover dead zone; menu is now flush with spacing moved into padding so hover carries through
- src/components/StudySectionComments.tsx: check response status before removing a comment locally, so a rejected delete no longer appears to succeed
- src/components/QASection.tsx: don't bail out of search scoring for 1-2 character queries, so short substrings like "OT" or "AI" can still match
- src/AdminPage.tsx: check response status in study-user, QR code, and episode-script delete handlers before updating local state

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Yvx4rPVy2HWe7ubuk3X3J
This commit is contained in:
nmemmertandClaude Sonnet 5 committed 2026-09-07 09:39:37 -04:00
1 parent 6ed29ea527
commit c158fb6317
7 files changed
+76 -27

No files matched your search

+36 -11
View File
@@ -423,8 +423,10 @@ function SiteHeader({ content }: { content: SiteContent }) {
</button>
</div>
<div className="nav-dropdown-menu">
<NavLink to="/episodes" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setEpisodesOpen(false) }}>Current Series</NavLink>
<NavLink to="/finished" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setEpisodesOpen(false) }}>Finished Books</NavLink>
<div className="nav-dropdown-menu-inner">
<NavLink to="/episodes" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setEpisodesOpen(false) }}>Current Series</NavLink>
<NavLink to="/finished" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setEpisodesOpen(false) }}>Finished Books</NavLink>
</div>
</div>
</div>
<NavLink to="/questions" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Q&amp;A</NavLink>
@@ -437,8 +439,10 @@ function SiteHeader({ content }: { content: SiteContent }) {
</button>
</div>
<div className="nav-dropdown-menu">
<NavLink to="/study" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setStudiesOpen(false) }}>All Studies</NavLink>
<NavLink to="/scripture" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setStudiesOpen(false) }}>Scripture Index</NavLink>
<div className="nav-dropdown-menu-inner">
<NavLink to="/study" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setStudiesOpen(false) }}>All Studies</NavLink>
<NavLink to="/scripture" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setStudiesOpen(false) }}>Scripture Index</NavLink>
</div>
</div>
</div>
<NavLink to="/study/account" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>My Account</NavLink>
@@ -2052,18 +2056,31 @@ const SI_BOOK_MAP: Record<string, string> = {}
const SI_BOOK_NAME: Record<string, string> = {}
const SI_BOOK_ORDER: string[] = []
for (const b of BIBLE_BOOKS) {
SI_BOOK_MAP[b.name.toLowerCase()] = b.id
for (const alias of b.aliases ?? []) SI_BOOK_MAP[alias] = b.id
// Admin's "Bible Book" field strips all whitespace (e.g. "1corinthians"), so
// index both the spaced and unspaced forms of every name/alias.
for (const key of [b.name.toLowerCase(), ...(b.aliases ?? [])]) {
SI_BOOK_MAP[key] = b.id
SI_BOOK_MAP[key.replace(/\s+/g, '')] = b.id
}
SI_BOOK_NAME[b.id] = b.name
SI_BOOK_ORDER.push(b.id)
}
function parseSIRef(ref: string) {
const m = ref.match(/^(.*?)\s+(\d+):(\d+)(?:-(\d+))?$/)
// Supports same-chapter ranges ("5:15-20") and chapter-spanning ranges ("5:15-6:2").
const m = ref.match(/^(.*?)\s+(\d+):(\d+)(?:-(?:(\d+):)?(\d+))?$/)
if (!m) return null
const bookId = SI_BOOK_MAP[m[1].toLowerCase().replace(/\.\s*/g, ' ').trim()]
if (!bookId) return null
return { bookId, chapter: parseInt(m[2], 10), verseStart: parseInt(m[3], 10), verseEnd: m[4] ? parseInt(m[4], 10) : parseInt(m[3], 10) }
const chapter = parseInt(m[2], 10)
const endChapter = m[4] ? parseInt(m[4], 10) : chapter
return {
bookId,
chapter,
endChapter,
verseStart: parseInt(m[3], 10),
verseEnd: m[5] ? parseInt(m[5], 10) : parseInt(m[3], 10),
}
}
type HellaoContent = Array<{ type: string; number?: number; content?: Array<{ text?: string } | string> }>
@@ -2094,6 +2111,7 @@ interface ScriptureRow {
displayRef: string
bookId: string | null
chapter: number
endChapter: number
verseStart: number
verseEnd: number
sources: ScriptureSource[]
@@ -2134,17 +2152,21 @@ function ScriptureIndexPage({ content }: { content: SiteContent }) {
: entry.sourceTitle,
}
const key = parsed
? `${parsed.bookId}-${parsed.chapter}-${parsed.verseStart}-${parsed.verseEnd}`
? `${parsed.bookId}-${parsed.chapter}-${parsed.verseStart}-${parsed.endChapter}-${parsed.verseEnd}`
: `other-${entry.reference.trim().toLowerCase()}`
let row = rowMap.get(key)
if (!row) {
const spansChapters = parsed && parsed.endChapter !== parsed.chapter
row = {
key,
displayRef: parsed
? `${SI_BOOK_NAME[parsed.bookId]} ${parsed.chapter}:${parsed.verseStart}${parsed.verseEnd !== parsed.verseStart ? `-${parsed.verseEnd}` : ''}`
? spansChapters
? `${SI_BOOK_NAME[parsed.bookId]} ${parsed.chapter}:${parsed.verseStart}-${parsed.endChapter}:${parsed.verseEnd}`
: `${SI_BOOK_NAME[parsed.bookId]} ${parsed.chapter}:${parsed.verseStart}${parsed.verseEnd !== parsed.verseStart ? `-${parsed.verseEnd}` : ''}`
: entry.reference.trim(),
bookId: parsed?.bookId ?? null,
chapter: parsed?.chapter ?? 0,
endChapter: parsed?.endChapter ?? parsed?.chapter ?? 0,
verseStart: parsed?.verseStart ?? 0,
verseEnd: parsed?.verseEnd ?? 0,
sources: [],
@@ -2182,7 +2204,10 @@ function ScriptureIndexPage({ content }: { content: SiteContent }) {
for (const row of group.rows) {
const ck = `${group.bookId}/${row.chapter}`
if (!chapterMap.has(ck)) chapterMap.set(ck, [])
chapterMap.get(ck)!.push({ key: row.key, verseStart: row.verseStart, verseEnd: row.verseEnd })
// Chapter-spanning refs (e.g. "5:15-6:2") preview only the starting chapter's
// verses, since extractVerseRange operates on a single fetched chapter.
const verseEnd = row.endChapter !== row.chapter ? Number.MAX_SAFE_INTEGER : row.verseEnd
chapterMap.get(ck)!.push({ key: row.key, verseStart: row.verseStart, verseEnd })
}
}
for (const [ck, rows] of chapterMap) {