Add Books + editable Recommended Resources sections; improve content block links; v1.1.38

- CustomBlock: add linkUrl/linkLabel fields for a styled button below the body
- Body text auto-linkifies raw URLs into gold anchor tags
- New 'books' placement: Reading List grid on Downloads page (cover, desc, Amazon btn)
- New 'recommended' placement: editable Recommended Resources section on Downloads
  page; falls back to Truth For Life widget when empty
- Admin: Books and Recommended Resources panels added to Content nav

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-26 08:16:36 -04:00
1 parent a35e04320a
commit eb1d3ddd9c
5 files changed
+348 -5

No files matched your search

+121 -1
View File
@@ -699,7 +699,7 @@ type StringField = Exclude<keyof SiteContent, 'customLinks' | 'customBlocks' | '
type AdminView =
| 'dashboard' | 'homepage' | 'start-here' | 'about' | 'contact'
| 'podcast' | 'current-series' | 'episode-highlights'
| 'downloads' | 'custom-links' | 'content-blocks'
| 'downloads' | 'custom-links' | 'content-blocks' | 'books' | 'recommended-resources'
| 'questions' | 'study-comments' | 'analytics' | 'assets' | 'colossians-study' | 'qr-codes'
| 'subscribers' | 'study-users' | 'email-templates'
| 'seo' | 'legal' | 'security' | 'brand' | 'global'
@@ -731,6 +731,8 @@ const ADMIN_VIEW_OPTIONS: Array<{ group: string; options: Array<{ value: AdminVi
{ value: 'downloads', label: 'Downloads' },
{ value: 'custom-links', label: 'Custom Links' },
{ value: 'content-blocks', label: 'Content Blocks' },
{ value: 'books', label: 'Books' },
{ value: 'recommended-resources', label: 'Recommended Resources' },
],
},
{
@@ -1996,11 +1998,15 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
if (placement === 'footer') return 'Footer navigation links'
if (placement === 'otherSites') return 'Other site links in footer dropdown'
if (placement === 'externalSites') return 'Homepage external site links'
if (placement === 'books') return 'Downloads page (Reading List)'
if (placement === 'recommended') return 'Downloads page (Recommended Resources)'
return 'Downloads page (More Resources)'
}
function getCustomLinkPreviewPath(placement: CustomLink['placement']) {
if (placement === 'resources') return '/resources'
if (placement === 'books') return '/resources'
if (placement === 'recommended') return '/resources'
return '/'
}
@@ -2162,6 +2168,26 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
}))
}
function addBook() {
setForm(f => ({
...f,
customLinks: [
...(f.customLinks ?? []),
{ id: Date.now().toString(36), label: '', url: '', imageUrl: '', description: '', amazonUrl: '', amazonLabel: 'View on Amazon', placement: 'books' as const },
],
}))
}
function addRecommended() {
setForm(f => ({
...f,
customLinks: [
...(f.customLinks ?? []),
{ id: Date.now().toString(36), label: '', url: '', imageUrl: '', description: '', placement: 'recommended' as const },
],
}))
}
function updateLink(id: string, field: keyof CustomLink, value: string | string[]) {
setForm(f => ({
...f,
@@ -3828,6 +3854,14 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
<label htmlFor={`block-body-${block.id}`}>Body Text</label>
<textarea id={`block-body-${block.id}`} value={block.body} rows={3} placeholder="Write your content here…" onChange={e => updateBlock(block.id, 'body', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`block-link-url-${block.id}`}>Button Link URL <span className="admin-field-optional">(optional)</span></label>
<input id={`block-link-url-${block.id}`} type="url" value={block.linkUrl ?? ''} placeholder="https://…" onChange={e => updateBlock(block.id, 'linkUrl', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`block-link-label-${block.id}`}>Button Label <span className="admin-field-optional">(optional)</span></label>
<input id={`block-link-label-${block.id}`} type="text" value={block.linkLabel ?? ''} placeholder="e.g. Get the book on Amazon" onChange={e => updateBlock(block.id, 'linkLabel', e.target.value)} />
</div>
</div>
<button type="button" className="btn-admin-remove" onClick={() => removeBlock(block.id)}>Remove</button>
</div>
@@ -3837,6 +3871,92 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
</section>
)}
{/* BOOKS */}
{adminView === 'books' && (
<section className="admin-panel-section">
<div className="admin-panel-head">
<h2>Books</h2>
<p>Amazon books shown in the Reading List section on the Downloads page. Add a cover image, description, and Amazon link for each book.</p>
</div>
{(form.customLinks ?? []).filter(l => l.placement === 'books').length === 0 && <p className="admin-stats-note">No books yet.</p>}
{(form.customLinks ?? []).filter(l => l.placement === 'books').map(book => (
<div key={book.id} className="admin-array-row">
<div className="admin-array-fields">
<div className="admin-field">
<label htmlFor={`book-title-${book.id}`}>Title</label>
<input id={`book-title-${book.id}`} type="text" value={book.label} placeholder="Book title" onChange={e => updateLink(book.id, 'label', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`book-description-${book.id}`}>Description</label>
<textarea id={`book-description-${book.id}`} rows={3} value={book.description ?? ''} placeholder="Short description of the book" onChange={e => updateLink(book.id, 'description', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`book-image-${book.id}`}>Cover Image URL</label>
<input id={`book-image-${book.id}`} type="text" value={book.imageUrl ?? ''} placeholder="/uploads/cover.jpg" onChange={e => updateLink(book.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(book.imageUrl, value => updateLink(book.id, 'imageUrl', value), `book-image-${book.id}-asset`)}
{renderImagePreview(book.imageUrl, `${book.label || 'Book'} cover`)}
</div>
<div className="admin-field">
<label htmlFor={`book-amazon-url-${book.id}`}>Amazon URL</label>
<input id={`book-amazon-url-${book.id}`} type="url" value={book.amazonUrl ?? ''} placeholder="https://amazon.com/..." onChange={e => updateLink(book.id, 'amazonUrl', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`book-amazon-label-${book.id}`}>Amazon Button Label</label>
<input id={`book-amazon-label-${book.id}`} type="text" value={book.amazonLabel ?? ''} placeholder="View on Amazon" onChange={e => updateLink(book.id, 'amazonLabel', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`book-url-${book.id}`}>Link URL <span className="admin-field-optional">(optional fallback)</span></label>
<input id={`book-url-${book.id}`} type="url" value={book.url ?? ''} placeholder="https://..." onChange={e => updateLink(book.id, 'url', e.target.value)} />
</div>
</div>
<button type="button" className="btn-admin-remove" onClick={() => removeLink(book.id)}>Remove</button>
</div>
))}
<button type="button" className="btn-admin-add" onClick={addBook}>+ Add Book</button>
{renderSaveStatus()}
</section>
)}
{/* RECOMMENDED RESOURCES */}
{adminView === 'recommended-resources' && (
<section className="admin-panel-section">
<div className="admin-panel-head">
<h2>Recommended Resources</h2>
<p>Curated resources shown at the bottom of the Downloads page. When at least one is added, it replaces the default Truth For Life widget. Add a title, image, link, and description for each resource.</p>
</div>
{(form.customLinks ?? []).filter(l => l.placement === 'recommended').length === 0 && (
<p className="admin-stats-note">No custom recommended resources yet — the Truth For Life widget shows by default.</p>
)}
{(form.customLinks ?? []).filter(l => l.placement === 'recommended').map(item => (
<div key={item.id} className="admin-array-row">
<div className="admin-array-fields">
<div className="admin-field">
<label htmlFor={`rec-title-${item.id}`}>Title</label>
<input id={`rec-title-${item.id}`} type="text" value={item.label} placeholder="Resource title" onChange={e => updateLink(item.id, 'label', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`rec-url-${item.id}`}>Link URL</label>
<input id={`rec-url-${item.id}`} type="url" value={item.url} placeholder="https://..." onChange={e => updateLink(item.id, 'url', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`rec-description-${item.id}`}>Description</label>
<textarea id={`rec-description-${item.id}`} rows={3} value={item.description ?? ''} placeholder="What is this resource?" onChange={e => updateLink(item.id, 'description', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`rec-image-${item.id}`}>Image URL</label>
<input id={`rec-image-${item.id}`} type="text" value={item.imageUrl ?? ''} placeholder="/uploads/example.jpg" onChange={e => updateLink(item.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(item.imageUrl, value => updateLink(item.id, 'imageUrl', value), `rec-image-${item.id}-asset`)}
{renderImagePreview(item.imageUrl, `${item.label || 'Resource'} image`)}
</div>
</div>
<button type="button" className="btn-admin-remove" onClick={() => removeLink(item.id)}>Remove</button>
</div>
))}
<button type="button" className="btn-admin-add" onClick={addRecommended}>+ Add Recommended Resource</button>
{renderSaveStatus()}
</section>
)}
{/* STUDIES */}
{adminView === 'colossians-study' && (
<section className="admin-panel-section">