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

+11 -7
View File
@@ -174,11 +174,12 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) {
async function handleDelete(id: string) {
try {
await fetch(`/api/admin/study-users/${id}`, { method: 'DELETE' })
const res = await fetch(`/api/admin/study-users/${id}`, { method: 'DELETE' })
if (!res.ok) { flashMsg(id, 'Failed to delete user.'); return }
setUsers(prev => prev.filter(u => u.id !== id))
setConfirmDeleteId(null)
setExpandedId(null)
} catch { /* ignore */ }
} catch { flashMsg(id, 'Failed to delete user.') }
}
const filtered = users.filter(u =>
@@ -2479,19 +2480,21 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
async function handleDeleteQrCode(id: string) {
if (!confirm('Delete this QR code and all its scan history?')) return
try {
await fetch(`/api/admin/qr-codes/${id}`, { method: 'DELETE' })
const res = await fetch(`/api/admin/qr-codes/${id}`, { method: 'DELETE' })
if (!res.ok) { alert('Failed to delete QR code.'); return }
setQrCodes(prev => prev.filter(c => c.id !== id))
setQrScans(prev => prev.filter(s => s.qrId !== id))
} catch { /* ignore */ }
} catch { alert('Failed to delete QR code.') }
}
async function handleClearQrScans(id: string) {
if (!confirm('Clear all scan history for this QR code?')) return
try {
await fetch(`/api/admin/qr-codes/${id}/scans`, { method: 'DELETE' })
const res = await fetch(`/api/admin/qr-codes/${id}/scans`, { method: 'DELETE' })
if (!res.ok) { alert('Failed to clear scan history.'); return }
setQrScans(prev => prev.filter(s => s.qrId !== id))
setQrCodes(prev => prev.map(c => c.id === id ? { ...c, scanCount: 0 } : c))
} catch { /* ignore */ }
} catch { alert('Failed to clear scan history.') }
}
async function loadStudyComments() {
@@ -2568,7 +2571,8 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
async function handleDeleteScript(episodeNumber: string) {
if (!confirm(`Remove script for Episode ${episodeNumber}?`)) return
try {
await fetch(`/api/admin-episode-scripts/${encodeURIComponent(episodeNumber)}`, { method: 'DELETE' })
const res = await fetch(`/api/admin-episode-scripts/${encodeURIComponent(episodeNumber)}`, { method: 'DELETE' })
if (!res.ok) { alert('Failed to delete script.'); return }
setScriptsList(prev => prev.filter(s => s.episodeNumber !== episodeNumber))
} catch { alert('Failed to delete script.') }
}