Polish hero UX and add admin brand kit panel
This commit is contained in:
+162
-8
@@ -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">✦ ✦ ✦</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&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 />} />
|
||||
|
||||
Reference in New Issue
Block a user