Polish hero UX and add admin brand kit panel

This commit is contained in:
nmemmert
2026-04-16 10:31:35 -04:00
parent 5562db992b
commit 0c328057c8
5 changed files with 1128 additions and 446 deletions
+162 -8
View File
@@ -65,6 +65,17 @@ export interface ArchivedSeries {
export interface SiteContent {
eyebrow: string
heroTagline: string
startHereHeading: string
startHereIntro: string
startHereStep1Title: string
startHereStep1Body: string
startHereStep1Cta: string
startHereStep2Title: string
startHereStep2Body: string
startHereStep2Cta: string
startHereStep3Title: string
startHereStep3Body: string
startHereStep3Cta: string
aboutShowHeading: string
aboutShowP1: string
aboutShowP2: string
@@ -87,6 +98,21 @@ export interface SiteContent {
export const DEFAULTS: SiteContent = {
eyebrow: 'A Journey Through Scripture',
heroTagline: "Exploring God's Word one verse at a time",
startHereHeading: 'New Here? Start Here',
startHereIntro:
'If this is your first visit, this path helps you get grounded quickly and make the most of the site.',
startHereStep1Title: 'Step 1: Listen to 3 Starter Episodes',
startHereStep1Body:
'Start with the newest episode, one from the beginning of the current study, and one from the middle.',
startHereStep1Cta: 'Open Episodes',
startHereStep2Title: 'Step 2: Use Q&A to Go Deeper',
startHereStep2Body:
'Browse by topic or search key words to find concise biblical answers and related follow-up questions.',
startHereStep2Cta: 'Go to Q&A',
startHereStep3Title: 'Step 3: Ask Nate Directly',
startHereStep3Body:
'Use the contact form to submit your Bible question for future Q&A or an upcoming episode.',
startHereStep3Cta: 'Submit a Question',
aboutShowHeading: 'Depth. Clarity. Application.',
aboutShowP1:
'Verse by Verse with Nate walks through Scripture passage by passage — unpacking the original context, drawing out the meaning, and connecting each verse to how we live today.',
@@ -193,6 +219,33 @@ function QASection() {
setPage(0)
}
const tokenizeForRelated = (text: string) =>
text
.toLowerCase()
.replace(/[^a-z0-9\s]/g, ' ')
.split(/\s+/)
.filter(token => token.length > 3)
const getRelatedQuestions = (current: PublicQuestion) => {
const currentTokens = new Set(tokenizeForRelated(`${current.question} ${current.answer}`))
const related = questions
.filter(candidate => candidate.id !== current.id)
.map(candidate => {
const candidateTokens = tokenizeForRelated(`${candidate.question} ${candidate.answer}`)
const overlap = candidateTokens.filter(token => currentTokens.has(token)).length
const sameTopic = Boolean(current.topic && candidate.topic && current.topic === candidate.topic)
const score = overlap + (sameTopic ? 5 : 0)
return { candidate, score }
})
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 3)
.map(item => item.candidate)
return related
}
return (
<section id="qa" className="section-qa" aria-label="Questions and answers">
<div className="section-inner">
@@ -265,6 +318,28 @@ function QASection() {
<div className="qa-card-face qa-card-back">
<span className="qa-face-label">A</span>
<p className="qa-answer-text">{question.answer}</p>
{getRelatedQuestions(question).length > 0 && (
<div className="qa-related-wrap">
<p className="qa-related-label">Related questions</p>
<div className="qa-related-list">
{getRelatedQuestions(question).map(related => (
<button
key={related.id}
type="button"
className="qa-related-btn"
onClick={e => {
e.stopPropagation()
handleTopic(null)
handleSearch(related.question)
setExpanded({ [related.id]: true })
}}
>
{related.question}
</button>
))}
</div>
</div>
)}
<p style={{ margin: '0.75rem 0 0', fontSize: '0.8rem', color: '#a89060', fontStyle: 'italic' }}> Answered by Nate</p>
</div>
</div>
@@ -480,6 +555,7 @@ function ShareShowButton() {
function LandingPage({ content }: { content: SiteContent }) {
const archivedSeries = content.archivedSeries ?? []
const [seriesView, setSeriesView] = useState<'current' | 'archive'>('current')
const [activeArchivedIndex, setActiveArchivedIndex] = useState(0)
useEffect(() => {
@@ -496,6 +572,16 @@ function LandingPage({ content }: { content: SiteContent }) {
return () => window.clearInterval(intervalId)
}, [archivedSeries.length])
useEffect(() => {
if (window.location.hash === '#archives' && archivedSeries.length > 0) {
setSeriesView('archive')
return
}
if (window.location.hash === '#series') {
setSeriesView('current')
}
}, [archivedSeries.length])
return (
<div className="site">
{/* ── HEADER ── */}
@@ -503,10 +589,14 @@ function LandingPage({ content }: { content: SiteContent }) {
<div className="header-inner">
<span className="header-ornament"> &nbsp; &nbsp; </span>
<nav className="header-nav">
<Link to="/start-here">New Here</Link>
<a href="#listen">Listen</a>
<a href="#about">About</a>
<a href="#qa">Q&amp;A</a>
<a href="#series">Series</a>
{archivedSeries.length > 0 && (
<a href="#archives" onClick={() => setSeriesView('archive')}>Archived</a>
)}
<a href="#contact">Contact</a>
<a
href={SPOTIFY_SHOW_URL}
@@ -514,7 +604,7 @@ function LandingPage({ content }: { content: SiteContent }) {
rel="noreferrer"
className="header-cta"
>
Subscribe Free
Follow on Spotify
</a>
</nav>
</div>
@@ -532,11 +622,7 @@ function LandingPage({ content }: { content: SiteContent }) {
</div>
<div className="hero-text">
<p className="eyebrow">{content.eyebrow}</p>
<h1 className="hero-title">
<span className="verse-word">VERSE</span>
<span className="by-label">· by ·</span>
<span className="verse-word">VERSE</span>
</h1>
<h1 className="hero-title">Verse by Verse</h1>
<p className="with-nate">
<span className="word-with">with </span>Nate
</p>
@@ -627,6 +713,36 @@ function LandingPage({ content }: { content: SiteContent }) {
</section>
{/* ── SERIES ── */}
{archivedSeries.length > 0 && (
<section className="section-series-toggle" aria-label="Series view toggle">
<div className="section-inner">
<div className="series-toggle-wrap">
<button
type="button"
className={`series-toggle-btn ${seriesView === 'current' ? 'series-toggle-btn--active' : ''}`}
onClick={() => {
setSeriesView('current')
window.location.hash = 'series'
}}
>
Current Study
</button>
<button
type="button"
className={`series-toggle-btn ${seriesView === 'archive' ? 'series-toggle-btn--active' : ''}`}
onClick={() => {
setSeriesView('archive')
window.location.hash = 'archives'
}}
>
Archived Studies
</button>
</div>
</div>
</section>
)}
{seriesView === 'current' && (
<section className="section-series" id="series" aria-label="Current series">
<div className="section-inner">
<h2 className="section-heading">
@@ -658,9 +774,10 @@ function LandingPage({ content }: { content: SiteContent }) {
</div>
</div>
</section>
)}
{archivedSeries.length > 0 && (
<section className="section-archive-series" aria-label="Archived studies">
{archivedSeries.length > 0 && seriesView === 'archive' && (
<section className="section-archive-series" id="archives" aria-label="Archived studies">
<div className="section-inner">
<h2 className="section-heading">
<span className="ornament"></span> Archived Studies <span className="ornament"></span>
@@ -920,6 +1037,42 @@ function LandingPage({ content }: { content: SiteContent }) {
)
}
function StartHerePage({ content }: { content: SiteContent }) {
return (
<main className="thanks-page" aria-label="Start Here">
<div className="thanks-card" style={{ maxWidth: '980px', width: '100%' }}>
<p className="eyebrow">Verse by Verse with Nate</p>
<h1>{content.startHereHeading}</h1>
<p>
{content.startHereIntro}
</p>
<div className="start-grid" style={{ marginTop: '1.25rem' }}>
<article className="start-card">
<h3>{content.startHereStep1Title}</h3>
<p>{content.startHereStep1Body}</p>
<a href="/#listen" className="btn-secondary">{content.startHereStep1Cta}</a>
</article>
<article className="start-card">
<h3>{content.startHereStep2Title}</h3>
<p>{content.startHereStep2Body}</p>
<a href="/#qa" className="btn-secondary">{content.startHereStep2Cta}</a>
</article>
<article className="start-card">
<h3>{content.startHereStep3Title}</h3>
<p>{content.startHereStep3Body}</p>
<a href="/#contact" className="btn-primary">{content.startHereStep3Cta}</a>
</article>
</div>
<div style={{ marginTop: '1.4rem' }}>
<Link to="/" className="btn-secondary">Back to Site</Link>
</div>
</div>
</main>
)
}
function ThankYouPage() {
const navigate = useNavigate()
const [secondsLeft, setSecondsLeft] = useState(15)
@@ -1203,6 +1356,7 @@ export default function App() {
return (
<Routes>
<Route path="/" element={<LandingPage content={content} />} />
<Route path="/start-here" element={<StartHerePage content={content} />} />
<Route path="/questions" element={<QuestionsPage />} />
<Route path="/thanks" element={<ThankYouPage />} />
<Route path="/subscribe" element={<SubscribePage />} />