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
+1 -1
View File
@@ -21,7 +21,7 @@
<link rel="canonical" href="https://necloud.us" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;0,900;1,700&family=Barlow+Condensed:wght@300;400;500;700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400;1,600;1,700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
+262 -45
View File
@@ -73,30 +73,108 @@ interface Question {
}
type StringField = Exclude<keyof SiteContent, 'customLinks' | 'customBlocks' | 'archivedSeries'>
const FIELDS: { key: StringField; label: string; multiline?: boolean }[] = [
{ key: 'eyebrow', label: 'Hero Eyebrow Text' },
{ key: 'heroTagline', label: 'Hero Tagline' },
{ key: 'aboutShowHeading', label: 'About Show — Heading' },
{ key: 'aboutShowP1', label: 'About Show — Paragraph 1', multiline: true },
{ key: 'aboutShowP2', label: 'About Show — Paragraph 2', multiline: true },
{ key: 'aboutNate', label: 'About Nate', multiline: true },
{ key: 'seriesLabel', label: 'Series Label (e.g. "Now Playing")' },
{ key: 'seriesTitle', label: 'Series Title' },
{ key: 'seriesDescription', label: 'Series Description', multiline: true },
{ key: 'seriesImageUrl', label: 'Series Cover Image URL' },
{ key: 'seriesListenUrl', label: 'Series Listen URL' },
{ key: 'studyGuideTitle', label: 'Study Guide Title' },
{ key: 'studyGuideDescription', label: 'Study Guide Description', multiline: true },
{ key: 'studyGuideUrl', label: 'Study Guide URL (Amazon link)' },
{ key: 'shareHeading', label: 'Share Section — Heading' },
{ key: 'shareP', label: 'Share Section — Paragraph', multiline: true },
type MainContentSection = 'hero' | 'start-here' | 'about' | 'series' | 'share'
const MAIN_CONTENT_SECTIONS: Array<{ id: MainContentSection; title: string; description: string }> = [
{
id: 'hero',
title: 'Hero Section',
description: 'Controls the top intro copy on the homepage.',
},
{
id: 'start-here',
title: 'Start Here Page',
description: 'Edit the separate Start Here page heading, intro, and step cards.',
},
{
id: 'about',
title: 'About Section',
description: 'Manage the main show description and Nate bio content.',
},
{
id: 'series',
title: 'Current Series',
description: 'Update the active study card and companion guide content.',
},
{
id: 'share',
title: 'Share Section',
description: 'Controls the copy shown near the QR/share area.',
},
]
const BRAND_KIT_SWATCHES = [
{ name: 'Rich Black', hex: '#0a0a08', role: 'Primary background' },
{ name: 'Soft Black', hex: '#0f0f0c', role: 'Secondary surfaces' },
{ name: 'Panel Black', hex: '#1a1a15', role: 'Cards and panels' },
{ name: 'Deep Brown', hex: '#2a2518', role: 'Borders and dividers' },
{ name: 'Antique Gold', hex: '#c9a84c', role: 'Primary accent' },
{ name: 'Light Gold', hex: '#e0c070', role: 'Hover and highlight' },
{ name: 'Warm White', hex: '#f0ead8', role: 'Primary text' },
{ name: 'Warm Gray', hex: '#7a7060', role: 'Secondary text' },
] as const
const BRAND_KIT_TYPE = [
{
label: 'Brand Font',
spec: 'Cormorant Garamond · 300/400/600/700 · italic supported',
sample: 'Verse by Verse with Nate',
},
{
label: 'Subtitle / Small Caps',
spec: 'Uppercase with tracking for labels, nav, and support text',
sample: 'A Journey Through Scripture',
},
{
label: 'Body / Quote',
spec: 'Cormorant Garamond light/italic for long-form text and pull copy',
sample: 'Verse by verse. Nugget by nugget.',
},
] as const
const BRAND_KIT_VERIFICATION = [
'Global font import updated to Cormorant Garamond.',
'Shared brand variables now define black, gold, border, warm white, and muted text colors.',
'Live site CSS now references the brand variables for core text and accent styling.',
'Admin surfaces inherit the same typography and palette tokens used by the public site.',
] as const
const FIELDS: Array<{ key: StringField; label: string; multiline?: boolean; section: MainContentSection }> = [
{ key: 'eyebrow', label: 'Hero Eyebrow Text', section: 'hero' },
{ key: 'heroTagline', label: 'Hero Tagline', section: 'hero' },
{ key: 'startHereHeading', label: 'Start Here — Heading', section: 'start-here' },
{ key: 'startHereIntro', label: 'Start Here — Intro', multiline: true, section: 'start-here' },
{ key: 'startHereStep1Title', label: 'Start Here — Step 1 Title', section: 'start-here' },
{ key: 'startHereStep1Body', label: 'Start Here — Step 1 Body', multiline: true, section: 'start-here' },
{ key: 'startHereStep1Cta', label: 'Start Here — Step 1 Button Text', section: 'start-here' },
{ key: 'startHereStep2Title', label: 'Start Here — Step 2 Title', section: 'start-here' },
{ key: 'startHereStep2Body', label: 'Start Here — Step 2 Body', multiline: true, section: 'start-here' },
{ key: 'startHereStep2Cta', label: 'Start Here — Step 2 Button Text', section: 'start-here' },
{ key: 'startHereStep3Title', label: 'Start Here — Step 3 Title', section: 'start-here' },
{ key: 'startHereStep3Body', label: 'Start Here — Step 3 Body', multiline: true, section: 'start-here' },
{ key: 'startHereStep3Cta', label: 'Start Here — Step 3 Button Text', section: 'start-here' },
{ key: 'aboutShowHeading', label: 'About Show — Heading', section: 'about' },
{ key: 'aboutShowP1', label: 'About Show — Paragraph 1', multiline: true, section: 'about' },
{ key: 'aboutShowP2', label: 'About Show — Paragraph 2', multiline: true, section: 'about' },
{ key: 'aboutNate', label: 'About Nate', multiline: true, section: 'about' },
{ key: 'seriesLabel', label: 'Series Label (e.g. "Now Playing")', section: 'series' },
{ key: 'seriesTitle', label: 'Series Title', section: 'series' },
{ key: 'seriesDescription', label: 'Series Description', multiline: true, section: 'series' },
{ key: 'seriesImageUrl', label: 'Series Cover Image URL', section: 'series' },
{ key: 'seriesListenUrl', label: 'Series Listen URL', section: 'series' },
{ key: 'studyGuideTitle', label: 'Study Guide Title', section: 'series' },
{ key: 'studyGuideDescription', label: 'Study Guide Description', multiline: true, section: 'series' },
{ key: 'studyGuideUrl', label: 'Study Guide URL (Amazon link)', section: 'series' },
{ key: 'shareHeading', label: 'Share Section — Heading', section: 'share' },
{ key: 'shareP', label: 'Share Section — Paragraph', multiline: true, section: 'share' },
]
export default function AdminPage({ content, onSave, onLogout }: Props) {
const [form, setForm] = useState<SiteContent>(content)
const [lastSavedSnapshot, setLastSavedSnapshot] = useState(() => JSON.stringify(content))
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
const [errorMsg, setErrorMsg] = useState('')
const [adminTab, setAdminTab] = useState<'content' | 'stats' | 'questions'>('content')
const [adminTab, setAdminTab] = useState<'content' | 'stats' | 'questions' | 'brand'>('content')
const [contentTab, setContentTab] = useState<'main' | 'custom'>('main')
const [stats, setStats] = useState<AdminStats | null>(null)
const [statsStatus, setStatsStatus] = useState<'loading' | 'ready' | 'error'>('loading')
@@ -110,6 +188,25 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
const [editingQuestionId, setEditingQuestionId] = useState<string | null>(null)
const [archiveLinkSelectionBySeries, setArchiveLinkSelectionBySeries] = useState<{ [key: string]: string }>({})
const isDirty = JSON.stringify(form) !== lastSavedSnapshot
useEffect(() => {
const nextSnapshot = JSON.stringify(content)
setForm(content)
setLastSavedSnapshot(nextSnapshot)
}, [content])
useEffect(() => {
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
if (!isDirty) return
event.preventDefault()
event.returnValue = ''
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
}, [isDirty])
useEffect(() => {
fetch('/api/admin-stats')
.then(r => (r.ok ? r.json() : Promise.reject(new Error('Failed to load stats'))))
@@ -300,6 +397,23 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
setForm(f => ({ ...f, [key]: value }))
}
function confirmLeaveUnsavedChanges() {
if (!isDirty) return true
return confirm('You have unsaved changes. Leave this section without saving?')
}
function handleAdminTabChange(nextTab: 'content' | 'stats' | 'questions' | 'brand') {
if (nextTab === adminTab) return
if (!confirmLeaveUnsavedChanges()) return
setAdminTab(nextTab)
}
function handleContentTabChange(nextTab: 'main' | 'custom') {
if (nextTab === contentTab) return
if (!confirmLeaveUnsavedChanges()) return
setContentTab(nextTab)
}
function addLink() {
setForm(f => ({
...f,
@@ -581,6 +695,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
throw new Error((data as { message?: string }).message ?? 'Save failed')
}
onSave(form)
setLastSavedSnapshot(JSON.stringify(form))
setStatus('saved')
setTimeout(() => setStatus('idle'), 3500)
} catch (err) {
@@ -656,8 +771,23 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<h1>Site Admin</h1>
<p className="admin-sub">Verse by Verse with Nate</p>
<div className="admin-header-actions">
<Link to="/" className="admin-back"> Back to site</Link>
<button type="button" className="btn-admin-logout" onClick={() => void onLogout()}>
<Link
to="/"
className="admin-back"
onClick={e => {
if (!confirmLeaveUnsavedChanges()) e.preventDefault()
}}
>
Back to site
</Link>
<button
type="button"
className="btn-admin-logout"
onClick={() => {
if (!confirmLeaveUnsavedChanges()) return
void onLogout()
}}
>
Log Out
</button>
</div>
@@ -670,7 +800,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
role="tab"
aria-selected={adminTab === 'content'}
className={`admin-tab ${adminTab === 'content' ? 'admin-tab--active' : ''}`}
onClick={() => setAdminTab('content')}
onClick={() => handleAdminTabChange('content')}
>
Content
</button>
@@ -679,7 +809,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
role="tab"
aria-selected={adminTab === 'stats'}
className={`admin-tab ${adminTab === 'stats' ? 'admin-tab--active' : ''}`}
onClick={() => setAdminTab('stats')}
onClick={() => handleAdminTabChange('stats')}
>
Site Stats
</button>
@@ -688,12 +818,82 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
role="tab"
aria-selected={adminTab === 'questions'}
className={`admin-tab ${adminTab === 'questions' ? 'admin-tab--active' : ''}`}
onClick={() => setAdminTab('questions')}
onClick={() => handleAdminTabChange('questions')}
>
Questions ({questions.length})
</button>
<button
type="button"
role="tab"
aria-selected={adminTab === 'brand'}
className={`admin-tab ${adminTab === 'brand' ? 'admin-tab--active' : ''}`}
onClick={() => handleAdminTabChange('brand')}
>
Brand Kit
</button>
</div>
{adminTab === 'brand' && (
<section className="admin-brand-kit" aria-label="Brand kit">
<div className="admin-stats-head">
<h2>Brand Kit</h2>
<p>Reference palette, typography, and implementation status for the live site.</p>
</div>
<div className="admin-brand-hero">
<img src="/images/podcast-art.jpeg" alt="Verse by Verse with Nate artwork" />
<div>
<p className="admin-brand-kicker">Verse by Verse with Nate</p>
<h3>A Journey Through Scripture</h3>
<p className="admin-brand-tagline">Verse by verse. Nugget by nugget.</p>
</div>
</div>
<div className="admin-brand-grid">
<article className="admin-brand-card">
<h3>Color Palette</h3>
<div className="admin-brand-swatches">
{BRAND_KIT_SWATCHES.map(swatch => (
<div key={swatch.hex} className="admin-brand-swatch">
<div className="admin-brand-swatch-block" style={{ background: swatch.hex }} />
<div className="admin-brand-swatch-copy">
<strong>{swatch.name}</strong>
<span>{swatch.hex}</span>
<p>{swatch.role}</p>
</div>
</div>
))}
</div>
</article>
<article className="admin-brand-card">
<h3>Typography</h3>
<div className="admin-brand-type-list">
{BRAND_KIT_TYPE.map(item => (
<div key={item.label} className="admin-brand-type-row">
<p className="admin-brand-type-label">{item.label}</p>
<p className="admin-brand-type-spec">{item.spec}</p>
<p className="admin-brand-type-sample">{item.sample}</p>
</div>
))}
</div>
</article>
</div>
<article className="admin-brand-card admin-brand-card--verify">
<h3>Verified On Site</h3>
<ul className="admin-brand-checklist">
{BRAND_KIT_VERIFICATION.map(item => (
<li key={item}>{item}</li>
))}
</ul>
<p className="admin-brand-note">
This admin panel reflects the live implementation now loaded from the shared font import and brand tokens.
</p>
</article>
</section>
)}
{adminTab === 'stats' && (
<section className="admin-stats" aria-label="Site hit statistics">
<div className="admin-stats-head">
@@ -967,7 +1167,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
role="tab"
aria-selected={contentTab === 'main'}
className={`admin-tab ${contentTab === 'main' ? 'admin-tab--active' : ''}`}
onClick={() => setContentTab('main')}
onClick={() => handleContentTabChange('main')}
>
Main Content
</button>
@@ -976,12 +1176,14 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
role="tab"
aria-selected={contentTab === 'custom'}
className={`admin-tab ${contentTab === 'custom' ? 'admin-tab--active' : ''}`}
onClick={() => setContentTab('custom')}
onClick={() => handleContentTabChange('custom')}
>
Custom Content
</button>
</div>
{isDirty && <p className="admin-status admin-status--warn">Unsaved changes</p>}
{contentTab === 'main' && (
<>
<div className="admin-archive-helper">
@@ -995,26 +1197,41 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
</button>
</div>
{FIELDS.map(({ key, label, multiline }) => (
<div className="admin-field" key={key}>
<label htmlFor={`field-${key}`}>{label}</label>
{multiline ? (
<textarea
id={`field-${key}`}
value={form[key] as string}
onChange={e => handleChange(key, e.target.value)}
rows={4}
/>
) : (
<input
id={`field-${key}`}
type="text"
value={form[key] as string}
onChange={e => handleChange(key, e.target.value)}
/>
)}
</div>
))}
{MAIN_CONTENT_SECTIONS.map(section => {
const sectionFields = FIELDS.filter(field => field.section === section.id)
return (
<section className="admin-form-section" key={section.id}>
<div className="admin-section-header admin-section-header--compact">
<h3>{section.title}</h3>
<p>{section.description}</p>
</div>
<div className="admin-form-section-fields">
{sectionFields.map(({ key, label, multiline }) => (
<div className="admin-field" key={key}>
<label htmlFor={`field-${key}`}>{label}</label>
{multiline ? (
<textarea
id={`field-${key}`}
value={form[key] as string}
onChange={e => handleChange(key, e.target.value)}
rows={4}
/>
) : (
<input
id={`field-${key}`}
type="text"
value={form[key] as string}
onChange={e => handleChange(key, e.target.value)}
/>
)}
</div>
))}
</div>
</section>
)
})}
</>
)}
+689 -389
View File
File diff suppressed because it is too large Load Diff
+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 />} />
+14 -3
View File
@@ -3,10 +3,21 @@
}
:root {
font-family: 'Barlow Condensed', 'Trebuchet MS', sans-serif;
--brand-black: #0a0a08;
--brand-black-2: #0f0f0c;
--brand-black-3: #1a1a15;
--brand-border: #2a2518;
--brand-gold: #c9a84c;
--brand-gold-light: #e0c070;
--brand-gold-dark: #8a6e28;
--brand-warm-white: #f0ead8;
--brand-muted: #7a7060;
--brand-font-body: 'Cormorant Garamond', Georgia, serif;
--brand-font-heading: 'Cormorant Garamond', Georgia, serif;
font-family: var(--brand-font-body);
line-height: 1.5;
color: #f0e6d0;
background: #0a0a0a;
color: var(--brand-warm-white);
background: var(--brand-black);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;