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:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.37",
|
"version": "1.1.38",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+121
-1
@@ -699,7 +699,7 @@ type StringField = Exclude<keyof SiteContent, 'customLinks' | 'customBlocks' | '
|
|||||||
type AdminView =
|
type AdminView =
|
||||||
| 'dashboard' | 'homepage' | 'start-here' | 'about' | 'contact'
|
| 'dashboard' | 'homepage' | 'start-here' | 'about' | 'contact'
|
||||||
| 'podcast' | 'current-series' | 'episode-highlights'
|
| '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'
|
| 'questions' | 'study-comments' | 'analytics' | 'assets' | 'colossians-study' | 'qr-codes'
|
||||||
| 'subscribers' | 'study-users' | 'email-templates'
|
| 'subscribers' | 'study-users' | 'email-templates'
|
||||||
| 'seo' | 'legal' | 'security' | 'brand' | 'global'
|
| '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: 'downloads', label: 'Downloads' },
|
||||||
{ value: 'custom-links', label: 'Custom Links' },
|
{ value: 'custom-links', label: 'Custom Links' },
|
||||||
{ value: 'content-blocks', label: 'Content Blocks' },
|
{ 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 === 'footer') return 'Footer navigation links'
|
||||||
if (placement === 'otherSites') return 'Other site links in footer dropdown'
|
if (placement === 'otherSites') return 'Other site links in footer dropdown'
|
||||||
if (placement === 'externalSites') return 'Homepage external site links'
|
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)'
|
return 'Downloads page (More Resources)'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCustomLinkPreviewPath(placement: CustomLink['placement']) {
|
function getCustomLinkPreviewPath(placement: CustomLink['placement']) {
|
||||||
if (placement === 'resources') return '/resources'
|
if (placement === 'resources') return '/resources'
|
||||||
|
if (placement === 'books') return '/resources'
|
||||||
|
if (placement === 'recommended') return '/resources'
|
||||||
return '/'
|
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[]) {
|
function updateLink(id: string, field: keyof CustomLink, value: string | string[]) {
|
||||||
setForm(f => ({
|
setForm(f => ({
|
||||||
...f,
|
...f,
|
||||||
@@ -3828,6 +3854,14 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
|||||||
<label htmlFor={`block-body-${block.id}`}>Body Text</label>
|
<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)} />
|
<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>
|
||||||
|
<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>
|
</div>
|
||||||
<button type="button" className="btn-admin-remove" onClick={() => removeBlock(block.id)}>Remove</button>
|
<button type="button" className="btn-admin-remove" onClick={() => removeBlock(block.id)}>Remove</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -3837,6 +3871,92 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
|||||||
</section>
|
</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 */}
|
{/* STUDIES */}
|
||||||
{adminView === 'colossians-study' && (
|
{adminView === 'colossians-study' && (
|
||||||
<section className="admin-panel-section">
|
<section className="admin-panel-section">
|
||||||
|
|||||||
+142
@@ -4407,6 +4407,140 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-block-inline-link {
|
||||||
|
color: var(--brand-gold);
|
||||||
|
text-decoration: underline;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-block-link-btn {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Books section ── */
|
||||||
|
.section-books {
|
||||||
|
background: #0d0d0d;
|
||||||
|
border-top: 1px solid rgba(201, 168, 76, 0.12);
|
||||||
|
padding: 4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.books-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.25rem;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border: 1px solid rgba(201, 168, 76, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-card-cover {
|
||||||
|
width: 80px;
|
||||||
|
height: 110px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-card-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-card-title {
|
||||||
|
font-family: var(--brand-font-heading);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--brand-warm-white);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-card-description {
|
||||||
|
font-family: var(--brand-font-body);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--brand-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-card-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Recommended Resources section ── */
|
||||||
|
.recommended-resources-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
max-width: 600px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommended-resource-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.25rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
text-decoration: none;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border: 1px solid rgba(201, 168, 76, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommended-resource-card:hover {
|
||||||
|
border-color: rgba(201, 168, 76, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommended-resource-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommended-resource-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommended-resource-title {
|
||||||
|
font-family: var(--brand-font-heading);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--brand-warm-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommended-resource-description {
|
||||||
|
font-family: var(--brand-font-body);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--brand-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommended-resource-action {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--brand-gold);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Study Guide section ── */
|
/* ── Study Guide section ── */
|
||||||
.section-guide {
|
.section-guide {
|
||||||
background: #0d0d0d;
|
background: #0d0d0d;
|
||||||
@@ -5414,6 +5548,14 @@
|
|||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-field-optional {
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-field input,
|
.admin-field input,
|
||||||
.admin-field textarea {
|
.admin-field textarea {
|
||||||
background: #111;
|
background: #111;
|
||||||
|
|||||||
+81
-2
@@ -885,6 +885,16 @@ function ShareShowButton() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function linkifyText(text: string) {
|
||||||
|
const urlRegex = /(https?:\/\/[^\s]+)/g
|
||||||
|
const parts = text.split(urlRegex)
|
||||||
|
return parts.map((part, i) =>
|
||||||
|
urlRegex.test(part)
|
||||||
|
? <a key={i} href={part} target="_blank" rel="noopener noreferrer" className="custom-block-inline-link">{part}</a>
|
||||||
|
: part
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function CustomBlocksSection({ content, page }: { content: SiteContent; page: string }) {
|
function CustomBlocksSection({ content, page }: { content: SiteContent; page: string }) {
|
||||||
const blocks = (content.customBlocks ?? []).filter(block => {
|
const blocks = (content.customBlocks ?? []).filter(block => {
|
||||||
const blockPage = block.page ?? 'downloads'
|
const blockPage = block.page ?? 'downloads'
|
||||||
@@ -897,7 +907,12 @@ function CustomBlocksSection({ content, page }: { content: SiteContent; page: st
|
|||||||
<section key={block.id} className="section-custom-block" aria-label={block.heading}>
|
<section key={block.id} className="section-custom-block" aria-label={block.heading}>
|
||||||
<div className="section-inner custom-block-inner">
|
<div className="section-inner custom-block-inner">
|
||||||
<h2>{block.heading}</h2>
|
<h2>{block.heading}</h2>
|
||||||
<p>{block.body}</p>
|
<p>{linkifyText(block.body)}</p>
|
||||||
|
{block.linkUrl && (
|
||||||
|
<a href={block.linkUrl} target="_blank" rel="noopener noreferrer" className="btn-primary custom-block-link-btn">
|
||||||
|
{block.linkLabel || 'Learn More'}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
@@ -1723,6 +1738,69 @@ function EpisodesPage({ content }: { content: SiteContent }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RecommendedResourcesSection({ content }: { content: SiteContent }) {
|
||||||
|
const recommended = (content.customLinks ?? []).filter(link => link.placement === 'recommended')
|
||||||
|
if (recommended.length === 0) return <TruthForLifeWidget type="resourcead" />
|
||||||
|
return (
|
||||||
|
<section className="section-tfl" aria-label="Recommended resources">
|
||||||
|
<div className="section-inner">
|
||||||
|
<div className="tfl-label-row">
|
||||||
|
<h2 className="section-heading" style={{ marginBottom: '1rem' }}>
|
||||||
|
<span className="ornament">✦</span> Recommended Resources <span className="ornament">✦</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="recommended-resources-list">
|
||||||
|
{recommended.map(item => (
|
||||||
|
<a key={item.id} href={item.url} target="_blank" rel="noopener noreferrer" className="recommended-resource-card">
|
||||||
|
{item.imageUrl && (
|
||||||
|
<img src={item.imageUrl} alt={item.label} className="recommended-resource-image" loading="lazy" />
|
||||||
|
)}
|
||||||
|
<div className="recommended-resource-meta">
|
||||||
|
<span className="recommended-resource-title">{item.label}</span>
|
||||||
|
{item.description && <p className="recommended-resource-description">{item.description}</p>}
|
||||||
|
<span className="recommended-resource-action">Learn more →</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BooksSectionSection({ content }: { content: SiteContent }) {
|
||||||
|
const books = (content.customLinks ?? []).filter(link => link.placement === 'books')
|
||||||
|
if (books.length === 0) return null
|
||||||
|
return (
|
||||||
|
<section className="section-resources section-books" aria-label="Recommended books">
|
||||||
|
<div className="section-inner">
|
||||||
|
<div className="download-library-head">
|
||||||
|
<p className="eyebrow">Reading List</p>
|
||||||
|
<h2 className="section-heading">Books worth your time.</h2>
|
||||||
|
</div>
|
||||||
|
<div className="books-list">
|
||||||
|
{books.map(book => (
|
||||||
|
<div key={book.id} className="book-card">
|
||||||
|
{book.imageUrl && (
|
||||||
|
<img src={book.imageUrl} alt={book.label} className="book-card-cover" loading="lazy" />
|
||||||
|
)}
|
||||||
|
<div className="book-card-meta">
|
||||||
|
<span className="book-card-title">{book.label}</span>
|
||||||
|
{book.description && <p className="book-card-description">{book.description}</p>}
|
||||||
|
{book.amazonUrl && (
|
||||||
|
<a href={book.amazonUrl} target="_blank" rel="noopener noreferrer" className="btn-primary book-card-btn">
|
||||||
|
{book.amazonLabel || 'View on Amazon'}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function ResourcesPage({ content }: { content: SiteContent }) {
|
function ResourcesPage({ content }: { content: SiteContent }) {
|
||||||
const siteTitle = content.seo?.title || DEFAULTS.seo.title
|
const siteTitle = content.seo?.title || DEFAULTS.seo.title
|
||||||
usePageMeta(
|
usePageMeta(
|
||||||
@@ -1735,8 +1813,9 @@ function ResourcesPage({ content }: { content: SiteContent }) {
|
|||||||
<SiteSearchBar content={content} />
|
<SiteSearchBar content={content} />
|
||||||
<StudyGuideSection content={content} />
|
<StudyGuideSection content={content} />
|
||||||
<DownloadLibrarySection content={content} />
|
<DownloadLibrarySection content={content} />
|
||||||
|
<BooksSectionSection content={content} />
|
||||||
<CustomBlocksSection content={content} page="downloads" />
|
<CustomBlocksSection content={content} page="downloads" />
|
||||||
<TruthForLifeWidget type="resourcead" />
|
<RecommendedResourcesSection content={content} />
|
||||||
<SiteFooter content={content} />
|
<SiteFooter content={content} />
|
||||||
<AnalyticsConsentBanner content={content} />
|
<AnalyticsConsentBanner content={content} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-1
@@ -26,7 +26,7 @@ export interface CustomLink {
|
|||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
url: string
|
url: string
|
||||||
placement: 'platforms' | 'footer' | 'resources' | 'otherSites' | 'externalSites'
|
placement: 'platforms' | 'footer' | 'resources' | 'otherSites' | 'externalSites' | 'books' | 'recommended'
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
description?: string
|
description?: string
|
||||||
amazonUrl?: string
|
amazonUrl?: string
|
||||||
@@ -39,6 +39,8 @@ export interface CustomBlock {
|
|||||||
heading: string
|
heading: string
|
||||||
body: string
|
body: string
|
||||||
page?: 'homepage' | 'start-here' | 'episodes' | 'downloads' | 'about' | 'contact' | 'questions'
|
page?: 'homepage' | 'start-here' | 'episodes' | 'downloads' | 'about' | 'contact' | 'questions'
|
||||||
|
linkUrl?: string
|
||||||
|
linkLabel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WhereToNextCard {
|
export interface WhereToNextCard {
|
||||||
|
|||||||
Reference in New Issue
Block a user