Scripture Index: verse of the week (admin override or weekly rotation through covered passages); v2.45.0
Publish Container / build (push) Successful in 17m33s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-15 11:50:19 -04:00
1 parent e9d9dd0d50
commit a1adfa0ef1
6 files changed
+75 -4

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "siteforge", "name": "siteforge",
"version": "2.44.0", "version": "2.45.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "siteforge", "name": "siteforge",
"version": "2.44.0", "version": "2.45.0",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.1135.0", "@aws-sdk/client-s3": "^3.1135.0",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "siteforge", "name": "siteforge",
"private": true, "private": true,
"version": "2.44.0", "version": "2.45.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+15
View File
@@ -884,6 +884,7 @@ const ADMIN_SECTION_LINKS: Partial<Record<AdminView, AdminSectionLink[]>> = {
{ id: 'global-header', label: 'Header' }, { id: 'global-header', label: 'Header' },
{ id: 'global-platforms', label: 'Platform URLs' }, { id: 'global-platforms', label: 'Platform URLs' },
{ id: 'global-banner', label: 'Analytics Banner' }, { id: 'global-banner', label: 'Analytics Banner' },
{ id: 'global-verse', label: 'Verse of the Week' },
{ id: 'global-welcome-email', label: 'Welcome Email' }, { id: 'global-welcome-email', label: 'Welcome Email' },
], ],
'start-here': [ 'start-here': [
@@ -1027,6 +1028,7 @@ const FIELDS: Array<{ key: StringField; label: string; multiline?: boolean; sect
{ key: 'platformCreatorUrl', label: 'Platform — Creator Profile URL', section: 'global' }, { key: 'platformCreatorUrl', label: 'Platform — Creator Profile URL', section: 'global' },
{ key: 'platformKofiUrl', label: 'Platform — Ko-fi URL', section: 'global' }, { key: 'platformKofiUrl', label: 'Platform — Ko-fi URL', section: 'global' },
{ key: 'platformKofiQrUrl', label: 'Platform — Ko-fi QR Code Image URL', section: 'global' }, { key: 'platformKofiQrUrl', label: 'Platform — Ko-fi QR Code Image URL', section: 'global' },
{ key: 'verseOfTheWeek', label: 'Scripture Index — Verse of the Week (optional)', section: 'global' },
{ key: 'welcomeEmailSubject', label: 'Welcome Email — Subject', section: 'global' }, { key: 'welcomeEmailSubject', label: 'Welcome Email — Subject', section: 'global' },
{ key: 'welcomeEmailGreetingPrefix', label: 'Welcome Email — Greeting Prefix', section: 'global' }, { key: 'welcomeEmailGreetingPrefix', label: 'Welcome Email — Greeting Prefix', section: 'global' },
{ key: 'welcomeEmailIntro', label: 'Welcome Email — Intro Paragraph', multiline: true, section: 'global' }, { key: 'welcomeEmailIntro', label: 'Welcome Email — Intro Paragraph', multiline: true, section: 'global' },
@@ -6436,6 +6438,19 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div> </div>
))} ))}
</>}
{tabOf('global', 'global-footer') === 'global-verse' && <>
<div className="admin-panel-subhead" id="global-verse"><h3>Verse of the Week</h3></div>
<div className="admin-field">
<label htmlFor="field-verseOfTheWeek">Verse of the Week (optional)</label>
<input id="field-verseOfTheWeek" type="text" placeholder="Titus 2:11-12" value={(form.verseOfTheWeek as string) ?? ''} onChange={e => handleChange('verseOfTheWeek', e.target.value)} />
<p className="admin-field-hint">Shown at the top of the Scripture Index. Leave blank and the site picks a different covered passage each week automatically. Use a full reference with the book name.</p>
{(() => {
const vowProblem = checkPassageReference(String(form.verseOfTheWeek ?? ''), '')
return vowProblem ? <p className="admin-field-warning" role="alert" style={{ color: '#e0a030', fontSize: '0.85rem', margin: '0.35rem 0 0' }}>⚠ {vowProblem}</p> : null
})()}
</div>
</>} </>}
{tabOf('global', 'global-footer') === 'global-welcome-email' && <> {tabOf('global', 'global-footer') === 'global-welcome-email' && <>
<div className="admin-panel-subhead" id="global-welcome-email"><h3>Welcome Email</h3></div> <div className="admin-panel-subhead" id="global-welcome-email"><h3>Welcome Email</h3></div>
+5 -1
View File
@@ -13718,6 +13718,10 @@ a.study-track-card:hover {
.si-chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .si-chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.si-verse-btn { margin-left: auto; font: inherit; font-size: 0.8rem; background: none; border: 0; padding: 0; cursor: pointer; color: var(--brand-muted); text-decoration: underline; } .si-verse-btn { margin-left: auto; font: inherit; font-size: 0.8rem; background: none; border: 0; padding: 0; cursor: pointer; color: var(--brand-muted); text-decoration: underline; }
.si-verse-btn:hover { color: var(--brand-gold); } .si-verse-btn:hover { color: var(--brand-gold); }
.si-vow { margin-bottom: 1rem; padding: 1.1rem 1.25rem; border: 1px solid var(--brand-gold-dark); border-radius: 12px; background: radial-gradient(ellipse at top left, rgba(201, 168, 76, 0.12), transparent 70%), var(--brand-black-3); }
.si-vow-eyebrow { margin: 0 0 0.25rem; font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--brand-gold); }
.si-vow-ref { margin: 0 0 0.5rem; font-family: var(--brand-font-heading); font-size: 1.6rem; font-weight: 600; line-height: 1.15; }
.si-vow-text { margin: 0 0 0.75rem; font-style: italic; line-height: 1.65; color: var(--brand-muted); }
.si-top { margin-bottom: 1rem; padding: 0.9rem 1rem; background: var(--brand-black-3); border: 1px solid var(--brand-border); border-radius: 12px; } .si-top { margin-bottom: 1rem; padding: 0.9rem 1rem; background: var(--brand-black-3); border: 1px solid var(--brand-border); border-radius: 12px; }
.si-top-title { margin: 0 0 0.6rem; font-family: var(--brand-font-heading); font-size: 1.15rem; font-weight: 600; } .si-top-title { margin: 0 0 0.6rem; font-family: var(--brand-font-heading); font-size: 1.15rem; font-weight: 600; }
.si-top-list { display: flex; flex-wrap: wrap; gap: 0.5rem; } .si-top-list { display: flex; flex-wrap: wrap; gap: 0.5rem; }
@@ -13731,7 +13735,7 @@ a.study-track-card:hover {
.si-main { padding: 0; } .si-main { padding: 0; }
.si-main, .si-group, .si-top { background: #fff !important; color: #000 !important; } .si-main, .si-group, .si-top { background: #fff !important; color: #000 !important; }
.si-print-title { display: block; font-size: 1.6rem; margin: 0 0 1rem; color: #000; } .si-print-title { display: block; font-size: 1.6rem; margin: 0 0 1rem; color: #000; }
.si-toolbar, .si-books, .si-meta, .si-top, .si-verse-btn, .si-verse, .si-chev { display: none !important; } .si-toolbar, .si-books, .si-meta, .si-top, .si-vow, .si-verse-btn, .si-verse, .si-chev { display: none !important; }
.si-group { border: 0; border-bottom: 1px solid #999; break-inside: avoid-page; border-radius: 0; } .si-group { border: 0; border-bottom: 1px solid #999; break-inside: avoid-page; border-radius: 0; }
.si-group-head { padding: 0.5rem 0; color: #000; } .si-group-head { padding: 0.5rem 0; color: #000; }
.si-ref { color: #000; } .si-ref { color: #000; }
+50
View File
@@ -2838,6 +2838,38 @@ function ScriptureIndexPage({ content }: { content: SiteContent }) {
return promise return promise
} }
// Verse of the week: the admin's pick if set, otherwise rotate through covered passages by calendar week
const [weekIndex] = useState(() => Math.floor(Date.now() / (7 * 24 * 60 * 60 * 1000)))
const verseOfWeek = useMemo(() => {
const override = content.verseOfTheWeek?.trim()
const parsed = override ? parseSIRef(override) : null
if (parsed) {
const key = `${parsed.bookId}-${parsed.chapter}-${parsed.verseStart}-${parsed.endChapter}-${parsed.verseEnd}`
const existing = bookGroups.flatMap(g => g.rows).find(r => r.key === key)
return existing ?? {
key,
displayRef: `${SI_BOOK_NAME[parsed.bookId]} ${parsed.chapter}:${parsed.verseStart}${parsed.verseEnd !== parsed.verseStart ? `-${parsed.verseEnd}` : ''}`,
bookId: parsed.bookId, chapter: parsed.chapter, endChapter: parsed.endChapter,
verseStart: parsed.verseStart, verseEnd: parsed.verseEnd, sources: [],
} as ScriptureRow
}
const pool = bookGroups.flatMap(g => g.rows)
if (pool.length === 0) return null
return pool[weekIndex % pool.length]
}, [bookGroups, content.verseOfTheWeek, weekIndex])
const [votText, setVotText] = useState<string | null>(null)
const votKey = verseOfWeek?.key
useEffect(() => {
if (!verseOfWeek || !verseOfWeek.bookId) return
let cancelled = false
const verseEnd = verseOfWeek.endChapter !== verseOfWeek.chapter ? Number.MAX_SAFE_INTEGER : verseOfWeek.verseEnd
fetchChapter(verseOfWeek.bookId, verseOfWeek.chapter)
.then(ch => { if (!cancelled) setVotText(extractVerseRange(ch, verseOfWeek.verseStart, verseEnd) || null) })
.catch(() => { if (!cancelled) setVotText(null) })
return () => { cancelled = true }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [votKey])
function toggleVerse(row: ScriptureRow) { function toggleVerse(row: ScriptureRow) {
const open = shownVerses.has(row.key) const open = shownVerses.has(row.key)
setShownVerses(prev => { setShownVerses(prev => {
@@ -2921,6 +2953,24 @@ function ScriptureIndexPage({ content }: { content: SiteContent }) {
</div> </div>
</div> </div>
{!filtering && verseOfWeek && (
<div className="si-vow">
<p className="si-vow-eyebrow">Verse of the week</p>
<h2 className="si-vow-ref">{verseOfWeek.displayRef}</h2>
{votText && <p className="si-vow-text">“{votText}”</p>}
{verseOfWeek.sources.length > 0 && (
<div className="si-sources">
{verseOfWeek.sources.slice(0, 3).map((src, i) => (
<Link key={i} to={src.url} className={`si-chip si-chip--${src.type}`}>
<span className="si-chip-kind">{src.type === 'episode' ? 'Episode' : 'Study'}</span>
<span className="si-chip-label">{src.label}</span>
</Link>
))}
</div>
)}
</div>
)}
{!filtering && topRows.length > 0 && ( {!filtering && topRows.length > 0 && (
<div className="si-top" aria-label="Most-studied passages"> <div className="si-top" aria-label="Most-studied passages">
<h2 className="si-top-title">Most-studied passages</h2> <h2 className="si-top-title">Most-studied passages</h2>
+2
View File
@@ -319,6 +319,7 @@ export interface SiteContent {
platformCreatorUrl: string platformCreatorUrl: string
platformKofiUrl?: string platformKofiUrl?: string
platformKofiQrUrl?: string platformKofiQrUrl?: string
verseOfTheWeek?: string
// ── Welcome Email ── // ── Welcome Email ──
welcomeEmailSubject: string welcomeEmailSubject: string
welcomeEmailGreetingPrefix: string welcomeEmailGreetingPrefix: string
@@ -571,6 +572,7 @@ export const DEFAULTS: SiteContent = {
platformCreatorUrl: 'https://creators.spotify.com/pod/profile/nmemmert/', platformCreatorUrl: 'https://creators.spotify.com/pod/profile/nmemmert/',
platformKofiUrl: 'https://ko-fi.com/versebyversewithnate', platformKofiUrl: 'https://ko-fi.com/versebyversewithnate',
platformKofiQrUrl: '/images/kofi-qr.png', platformKofiQrUrl: '/images/kofi-qr.png',
verseOfTheWeek: '',
welcomeEmailSubject: 'Welcome to Verse by Verse with Nate', welcomeEmailSubject: 'Welcome to Verse by Verse with Nate',
welcomeEmailGreetingPrefix: 'Glad you\'re here', welcomeEmailGreetingPrefix: 'Glad you\'re here',
welcomeEmailIntro: 'Thanks for subscribing to Verse by Verse with Nate - a Bible teaching podcast where we slow down, dig into the text, and pull out the nuggets God has for us word by word.', welcomeEmailIntro: 'Thanks for subscribing to Verse by Verse with Nate - a Bible teaching podcast where we slow down, dig into the text, and pull out the nuggets God has for us word by word.',