From 0512d40fc6c230e752867db19932768a61bd8f8e Mon Sep 17 00:00:00 2001 From: nmemmert Date: Wed, 26 Aug 2026 11:03:43 -0400 Subject: [PATCH] Redesign resources page with PageBanner, featured download, and consistent page headers; v1.1.40 - Replace hero study guide form with PageBanner + featured download card in library - Add PageBanner component used consistently across all interior pages (Downloads, Episodes, About, Contact, Q&A, Finished Books) - Remove duplicate headings/eyebrows that conflicted with banner titles on About, Contact, Q&A, Finished Books pages - Compact download card sizing (padding, border-radius, image size) - Add Books & Resources tabbed section with fallback to TruthForLife widget - Fix server sanitizer to pass through 'resources' placement items without URL - Add featured-badge and featured card styling for primary guide Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- server/helpers.js | 2 +- src/App.css | 114 +++++++++-- src/App.tsx | 366 +++++++++++++++++++---------------- src/components/QASection.tsx | 4 - 5 files changed, 307 insertions(+), 181 deletions(-) diff --git a/package.json b/package.json index d76481c..1f2c3be 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "1.1.39", + "version": "1.1.40", "type": "module", "scripts": { "dev": "vite", diff --git a/server/helpers.js b/server/helpers.js index cc47921..b950c99 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -186,7 +186,7 @@ function sanitizeCustomLinks(value) { placement, } }) - .filter(item => item.label && (item.url || item.amazonUrl || item.placement === 'books' || item.placement === 'recommended')) + .filter(item => item.label && (item.url || item.amazonUrl || item.placement === 'books' || item.placement === 'recommended' || item.placement === 'resources')) } function sanitizeCustomBlocks(value) { diff --git a/src/App.css b/src/App.css index 9cdc2b8..cdaa26c 100644 --- a/src/App.css +++ b/src/App.css @@ -4119,11 +4119,11 @@ .resource-download-card { background: #111; border: 1px solid rgba(201, 168, 76, 0.18); - border-radius: 18px; - padding: 1.5rem; + border-radius: 14px; + padding: 1rem 1.25rem; display: flex; flex-direction: column; - gap: 1rem; + gap: 0.75rem; text-decoration: none; color: inherit; transition: transform 160ms ease, background 160ms ease, border-color 160ms ease; @@ -4172,9 +4172,9 @@ } .resource-link-image { - width: 80px; - height: 80px; - border-radius: 14px; + width: 64px; + height: 64px; + border-radius: 10px; object-fit: cover; border: 1px solid rgba(201, 168, 76, 0.2); flex-shrink: 0; @@ -4480,13 +4480,10 @@ /* ── Recommended Resources section ── */ .recommended-resources-list { - display: flex; - flex-direction: column; - gap: 1.5rem; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.25rem; margin-top: 1rem; - max-width: 600px; - margin-left: auto; - margin-right: auto; } .recommended-resource-card { @@ -4541,6 +4538,99 @@ font-weight: 600; } +/* ── Interior page banner ── */ +.section-page-banner { + padding: 3.5rem 0 2rem; + border-bottom: 1px solid rgba(201, 168, 76, 0.12); +} + +.page-banner-inner { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.page-banner-title { + font-family: var(--brand-font-heading); + font-size: clamp(2rem, 4vw, 3rem); + font-weight: 700; + color: var(--brand-warm-white); + margin: 0.15rem 0 0; + line-height: 1.15; +} + +.page-banner-desc { + font-family: var(--brand-font-body); + font-size: 1.05rem; + font-weight: 300; + color: var(--brand-muted); + max-width: 52ch; + margin: 0.25rem 0 0; + line-height: 1.6; +} + +/* ── Featured download card ── */ +.resource-download-card--featured { + margin-bottom: 1.5rem; + border-color: rgba(201, 168, 76, 0.35); + background: rgba(201, 168, 76, 0.04); +} + +.resource-link-image--featured { + width: 80px !important; + height: 110px !important; + border-radius: 6px !important; + object-fit: cover; +} + +.featured-badge { + display: inline-block; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--brand-gold); + background: rgba(201, 168, 76, 0.12); + border: 1px solid rgba(201, 168, 76, 0.3); + border-radius: 4px; + padding: 0.15rem 0.5rem; + margin-bottom: 0.25rem; + align-self: flex-start; +} + +/* ── Books + Recommended Resources tabs ── */ +.books-resources-tabs { + display: flex; + gap: 0.25rem; + margin-bottom: 2rem; + border-bottom: 1px solid rgba(201, 168, 76, 0.15); + padding-bottom: 0; +} + +.books-resources-tab { + background: none; + border: none; + border-bottom: 2px solid transparent; + padding: 0.6rem 1.1rem; + margin-bottom: -1px; + font-family: var(--brand-font-body); + font-size: 0.92rem; + font-weight: 600; + color: rgba(255, 255, 255, 0.5); + cursor: pointer; + transition: color 160ms ease, border-color 160ms ease; + letter-spacing: 0.02em; +} + +.books-resources-tab:hover { + color: rgba(255, 255, 255, 0.85); +} + +.books-resources-tab--active { + color: var(--brand-gold); + border-bottom-color: var(--brand-gold); +} + /* ── Study Guide section ── */ .section-guide { background: #0d0d0d; diff --git a/src/App.tsx b/src/App.tsx index 014b7e4..f92a584 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -267,11 +267,27 @@ interface DownloadPageResource { resourceId: string amazonUrl?: string amazonLabel?: string + isPrimaryGuide?: boolean } function resolveDownloadPageResource(content: SiteContent, pageId: string | undefined): DownloadPageResource | null { if (!pageId) return null + if (pageId === 'primary-guide') { + return { + id: 'primary-guide', + label: content.studyGuideTitle || 'Companion Study Guide', + imageUrl: content.seriesImageUrl || '/images/titus-cover.png', + summary: content.studyGuideDescription || 'Complete the short form below and your download will start right away.', + tags: [], + buttonText: `Download ${content.studyGuideTitle || 'Guide'}`, + resourceId: 'primary-guide', + amazonUrl: content.studyGuideUrl, + amazonLabel: content.studyGuideAmazonButtonLabel, + isPrimaryGuide: true, + } + } + if (pageId.startsWith('custom--')) { const customId = pageId.slice('custom--'.length) const resource = (content.customLinks ?? []).find(link => link.id === customId && link.placement === 'resources') @@ -543,7 +559,6 @@ function AboutSection({ content }: { content: SiteContent }) {
-

{content.aboutShowEyebrow || 'About the Show'}

{content.aboutShowHeading}

{content.aboutShowP1}

{content.aboutShowP2}

@@ -559,42 +574,6 @@ function AboutSection({ content }: { content: SiteContent }) { ) } -function StudyGuideSection({ content }: { content: SiteContent }) { - return ( -
-
-
- {content.seriesTitle} -
-
-

Downloads

-

Study Guides and Downloads

-

Start with the primary guide below, then explore the rest of the download library further down the page.

-

Free Download

-

{content.studyGuideTitle}

-

{content.studyGuideDescription}

- - {content.studyGuideUrl && ( - - )} -
-
-
- ) -} function ContactSection({ content }: { content: SiteContent }) { return ( @@ -602,9 +581,6 @@ function ContactSection({ content }: { content: SiteContent }) {
-

{content.contactEyebrow || 'Get in Touch'}

-

{content.contactHeading || 'Contact Nate'}

-
+
+

{eyebrow}

+

{title}

+ {desc &&

{desc}

} +
+ + ) +} + function DownloadLibrarySection({ content }: { content: SiteContent }) { const resources = (content.customLinks ?? []).filter(link => link.placement === 'resources') const [activeTag, setActiveTag] = useState(null) - if (resources.length === 0) return null - + const hasPrimaryGuide = !!(content.studyGuideTitle) const allTags = Array.from(new Set(resources.flatMap(r => r.tags ?? []))).filter(Boolean) const filteredResources = resources.filter(r => { return !activeTag || (r.tags ?? []).includes(activeTag) }) + if (!hasPrimaryGuide && resources.length === 0) return null + return (
-
-

Download Library

-

Guides, worksheets, and study downloads.

-
- - {allTags.length > 0 && ( -
-
- - {allTags.map(tag => ( - - ))} - {activeTag !== null && ( - + {hasPrimaryGuide && ( + +
+ {(content.seriesImageUrl || '/images/titus-cover.png') && ( + {content.seriesTitle} )} +
+
Featured
+ {content.studyGuideTitle} + {content.studyGuideDescription && ( +

{content.studyGuideDescription}

+ )} + Download free guide → +
-
+ )} - {filteredResources.length > 0 ? ( -
-
- {filteredResources.map(resource => ( - -
- {resource.imageUrl && ( - {resource.label} - )} -
- {resource.label} - {resource.description &&

{resource.description}

} - {(resource.tags ?? []).length > 0 && ( -
{(resource.tags ?? []).join(', ')}
+ {resources.length > 0 && ( + <> + {allTags.length > 0 && ( +
+
+ + {allTags.map(tag => ( + + ))} + {activeTag !== null && ( + + )} +
+
+ )} + + {filteredResources.length > 0 ? ( +
+ {filteredResources.map(resource => ( + +
+ {resource.imageUrl && ( + {resource.label} )} - Open download page → +
+ {resource.label} + {resource.description &&

{resource.description}

} + {(resource.tags ?? []).length > 0 && ( +
{(resource.tags ?? []).join(', ')}
+ )} + Open download page → +
-
- - ))} -
-
- ) : ( -

No downloads tagged "{activeTag}".

+ + ))} +
+ ) : ( +

No downloads tagged "{activeTag}".

+ )} + )}
@@ -834,7 +843,9 @@ function DownloadDetailPage({ content }: { content: SiteContent }) { )}
- + {resource.isPrimaryGuide + ? + : }
@@ -1466,16 +1477,9 @@ function FinishedBooksPage({ content }: { content: SiteContent }) {
+
-
-

- Finished Books -

-

- Every series we've completed — with a full ordered episode playlist for each book of the Bible. -

-
{books.length === 0 ? (
📖
@@ -1715,6 +1719,7 @@ function EpisodesPage({ content }: { content: SiteContent }) {
+
@@ -1738,64 +1743,97 @@ function EpisodesPage({ content }: { content: SiteContent }) { ) } -function RecommendedResourcesSection({ content }: { content: SiteContent }) { - const recommended = (content.customLinks ?? []).filter(link => link.placement === 'recommended') - if (recommended.length === 0) return - return ( -
-
-
-

- Recommended Resources -

-
- -
-
- ) -} - -function BooksSectionSection({ content }: { content: SiteContent }) { +function BooksAndResourcesSection({ content }: { content: SiteContent }) { const books = (content.customLinks ?? []).filter(link => link.placement === 'books') - if (books.length === 0) return null + const recommended = (content.customLinks ?? []).filter(link => link.placement === 'recommended') + const [activeTab, setActiveTab] = useState<'books' | 'recommended'>('books') + + const hasBooks = books.length > 0 + const hasRecommended = recommended.length > 0 + const showTabs = hasBooks && hasRecommended + + if (!hasBooks && !hasRecommended) return + + const tab = showTabs ? activeTab : (hasBooks ? 'books' : 'recommended') + return ( -
+
-
-

Reading List

-

Books worth your time.

-
-
- {books.map(book => ( -
- {book.imageUrl && ( - {book.label} - )} -
- {book.label} - {book.description &&

{book.description}

} - {(book.amazonUrl || book.url) && ( - - {book.amazonUrl ? (book.amazonLabel || 'View on Amazon') : (book.amazonLabel || 'Learn More')} - - )} + {showTabs ? ( +
+ + +
+ ) : ( +
+

{tab === 'books' ? 'Reading List' : 'Recommended'}

+

{tab === 'books' ? 'Books worth your time.' : 'Resources worth your time.'}

+
+ )} + + {tab === 'books' && ( + <> + {showTabs && ( +
+

Books worth your time.

+ )} +
+ {books.map(book => ( +
+ {book.imageUrl && ( + {book.label} + )} +
+ {book.label} + {book.description &&

{book.description}

} + {(book.amazonUrl || book.url) && ( + + {book.amazonUrl ? (book.amazonLabel || 'View on Amazon') : (book.amazonLabel || 'Learn More')} + + )} +
+
+ ))}
- ))} -
+ + )} + + {tab === 'recommended' && ( + <> + {showTabs && ( +
+

Recommended Resources

+
+ )} + + + )}
) @@ -1811,11 +1849,10 @@ function ResourcesPage({ content }: { content: SiteContent }) {
- + - - +
@@ -1832,6 +1869,7 @@ function AboutPage({ content }: { content: SiteContent }) {
+ @@ -1846,6 +1884,7 @@ function ContactPage({ content }: { content: SiteContent }) {
+ @@ -2367,6 +2406,7 @@ function QuestionsPage({ content }: { content: SiteContent }) {
+ diff --git a/src/components/QASection.tsx b/src/components/QASection.tsx index eea0c37..797dd94 100644 --- a/src/components/QASection.tsx +++ b/src/components/QASection.tsx @@ -783,10 +783,6 @@ export default function QASection() { return (
-

- Questions & Answers -

-

Have a question about Scripture or the podcast?

Submit a Question →