Make all site content editable from admin; add per-page content blocks
This commit is contained in:
+1017
-1445
File diff suppressed because it is too large
Load Diff
+346
@@ -208,6 +208,24 @@
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-start-here {
|
||||
font-family: var(--brand-font-body);
|
||||
font-size: 0.85rem;
|
||||
color: rgba(201, 168, 76, 0.7);
|
||||
text-decoration: none;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 0.25rem 0;
|
||||
border-bottom: 1px solid rgba(201, 168, 76, 0.3);
|
||||
transition: color 200ms, border-color 200ms;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-start-here:hover {
|
||||
color: var(--brand-gold);
|
||||
border-color: var(--brand-gold);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@@ -285,6 +303,25 @@
|
||||
padding: 5rem 0;
|
||||
}
|
||||
|
||||
.episode-series-subtitle {
|
||||
text-align: center;
|
||||
color: var(--brand-gold);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
margin: -0.75rem 0 1.75rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.episode-series-desc {
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 0.95rem;
|
||||
max-width: 56ch;
|
||||
margin: 0 auto 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.embed-wrap {
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 14px;
|
||||
@@ -3463,6 +3500,315 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ── Admin Shell Layout ── */
|
||||
.admin-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: #070707;
|
||||
}
|
||||
|
||||
.admin-topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #070707;
|
||||
border-bottom: 1px solid rgba(201,168,76,0.2);
|
||||
padding: 0.65rem 1.5rem;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-topbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.admin-topbar-ornament {
|
||||
color: var(--brand-gold);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.admin-topbar-title {
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1.1rem;
|
||||
color: var(--brand-warm-white);
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.admin-topbar-sub {
|
||||
font-size: 0.78rem;
|
||||
color: rgba(201,168,76,0.5);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.admin-topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-topbar-meta {
|
||||
font-size: 0.78rem;
|
||||
color: rgba(240,234,216,0.45);
|
||||
}
|
||||
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── Sidebar ── */
|
||||
.admin-sidebar {
|
||||
width: 210px;
|
||||
min-width: 210px;
|
||||
background: #09090a;
|
||||
border-right: 1px solid rgba(201,168,76,0.1);
|
||||
padding: 1.25rem 0 2rem;
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 51px; /* matches topbar height */
|
||||
height: calc(100vh - 51px);
|
||||
}
|
||||
|
||||
.admin-sidebar-meta-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 1.1rem 1rem;
|
||||
border-bottom: 1px solid rgba(201,168,76,0.08);
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.admin-meta-link {
|
||||
display: block;
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(240,234,216,0.45);
|
||||
font-size: 0.82rem;
|
||||
padding: 0.25rem 0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
transition: color 150ms;
|
||||
}
|
||||
|
||||
.admin-meta-link:hover {
|
||||
color: var(--brand-warm-white);
|
||||
}
|
||||
|
||||
.admin-nav-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.admin-nav-label {
|
||||
display: block;
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(201,168,76,0.45);
|
||||
padding: 0 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.admin-nav-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
border-left: 2px solid transparent;
|
||||
color: rgba(240,234,216,0.6);
|
||||
padding: 0.4rem 1.1rem;
|
||||
font-size: 0.88rem;
|
||||
cursor: pointer;
|
||||
transition: color 150ms, background 150ms;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.admin-nav-item:hover {
|
||||
color: var(--brand-warm-white);
|
||||
background: rgba(201,168,76,0.06);
|
||||
}
|
||||
|
||||
.admin-nav-item--active {
|
||||
color: var(--brand-gold) !important;
|
||||
background: rgba(201,168,76,0.1);
|
||||
border-left-color: var(--brand-gold);
|
||||
}
|
||||
|
||||
/* ── Content Panel ── */
|
||||
.admin-panel {
|
||||
flex: 1;
|
||||
padding: 2rem 2.5rem;
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-panel-section {
|
||||
max-width: 740px;
|
||||
}
|
||||
|
||||
.admin-panel-head {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid rgba(201,168,76,0.12);
|
||||
}
|
||||
|
||||
.admin-panel-head h2 {
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1.6rem;
|
||||
color: var(--brand-warm-white);
|
||||
margin: 0 0 0.35rem;
|
||||
}
|
||||
|
||||
.admin-panel-head p {
|
||||
color: rgba(240,234,216,0.5);
|
||||
font-size: 0.88rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-panel-subhead {
|
||||
margin: 2rem 0 1rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 1px solid rgba(201,168,76,0.08);
|
||||
}
|
||||
|
||||
.admin-panel-subhead h3 {
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1.05rem;
|
||||
color: var(--brand-gold);
|
||||
margin: 0;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.admin-panel-subhead p {
|
||||
color: rgba(240,234,216,0.45);
|
||||
font-size: 0.82rem;
|
||||
margin: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.admin-section-header {
|
||||
margin: 2rem 0 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid rgba(201,168,76,0.08);
|
||||
}
|
||||
|
||||
.admin-section-header h3 {
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1.15rem;
|
||||
color: var(--brand-warm-white);
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
.admin-section-header p {
|
||||
color: rgba(240,234,216,0.45);
|
||||
font-size: 0.84rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-archive-helper {
|
||||
background: rgba(201,168,76,0.05);
|
||||
border: 1px solid rgba(201,168,76,0.15);
|
||||
border-radius: 6px;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.admin-archive-helper h3 {
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1rem;
|
||||
color: var(--brand-gold);
|
||||
margin: 0 0 0.35rem;
|
||||
}
|
||||
|
||||
.admin-archive-helper p {
|
||||
color: rgba(240,234,216,0.55);
|
||||
font-size: 0.85rem;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
/* ── Preview pane ── */
|
||||
.admin-layout--split {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-layout--split .admin-panel {
|
||||
max-width: none;
|
||||
min-width: 320px;
|
||||
flex: 1 1 40%;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid rgba(201,168,76,0.1);
|
||||
}
|
||||
|
||||
.admin-preview-pane {
|
||||
flex: 1 1 55%;
|
||||
min-width: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
position: sticky;
|
||||
top: 51px;
|
||||
height: calc(100vh - 51px);
|
||||
}
|
||||
|
||||
.admin-preview-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.45rem 1rem;
|
||||
background: #0a0a08;
|
||||
border-bottom: 1px solid rgba(201,168,76,0.15);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-preview-label {
|
||||
font-size: 0.78rem;
|
||||
font-family: var(--brand-font-body);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--brand-gold);
|
||||
}
|
||||
|
||||
.admin-preview-hint {
|
||||
font-size: 0.72rem;
|
||||
color: rgba(240,234,216,0.35);
|
||||
}
|
||||
|
||||
.admin-preview-iframe {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn-admin-reset--active {
|
||||
background: rgba(201,168,76,0.15);
|
||||
color: var(--brand-gold);
|
||||
border-color: rgba(201,168,76,0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-sidebar {
|
||||
display: none;
|
||||
}
|
||||
.admin-panel {
|
||||
padding: 1.25rem 1rem;
|
||||
}
|
||||
.admin-topbar-sub {
|
||||
display: none;
|
||||
}
|
||||
.admin-preview-pane {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.series-toggle-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
+274
-188
@@ -4,18 +4,12 @@ import AdminPage from './AdminPage'
|
||||
import QASection from './components/QASection'
|
||||
import ContactForm from './components/ContactForm'
|
||||
import { FacebookIcon, SpotifyIcon, YouTubeIcon, AmazonMusicIcon } from './icons'
|
||||
import type { SiteContent } from './content'
|
||||
import type { SiteContent, ArchivedSeries } from './content'
|
||||
import { DEFAULTS } from './content'
|
||||
import './App.css'
|
||||
|
||||
const SPOTIFY_SHOW_URL = '/spotify'
|
||||
const SPOTIFY_EMBED_URL =
|
||||
'https://open.spotify.com/embed/show/0Gq1TzoJOdReSZ1gYQi8Xl?utm_source=generator&theme=0'
|
||||
const SPOTIFY_CREATOR_URL = 'https://creators.spotify.com/pod/profile/nmemmert/'
|
||||
const APPLE_PODCASTS_URL = '/apple'
|
||||
const YOUTUBE_URL = 'https://www.youtube.com/@blackzebraem5558'
|
||||
const AMAZON_MUSIC_URL = '/amazon'
|
||||
const FACEBOOK_URL = 'https://facebook.com/versebyversewithnate'
|
||||
const CONSENT_KEY = 'vbn_analytics_consent_choice'
|
||||
|
||||
function StudyDownloadForm({ buttonText = 'Download Guide' }: { buttonText?: string }) {
|
||||
@@ -265,7 +259,7 @@ function resolveDownloadPageResource(content: SiteContent, pageId: string | unde
|
||||
return null
|
||||
}
|
||||
|
||||
function AnalyticsConsentBanner() {
|
||||
function AnalyticsConsentBanner({ content }: { content: SiteContent }) {
|
||||
const [choice, setChoice] = useState<'unknown' | 'accepted' | 'declined'>(() => {
|
||||
const saved = localStorage.getItem(CONSENT_KEY)
|
||||
if (saved === 'accepted' || saved === 'declined') return saved
|
||||
@@ -304,9 +298,7 @@ function AnalyticsConsentBanner() {
|
||||
|
||||
return (
|
||||
<div className="consent-banner" role="region" aria-label="Analytics consent">
|
||||
<p>
|
||||
We use optional analytics cookies to measure visits and location trends for site improvement.
|
||||
</p>
|
||||
<p>{content.cookieBannerText || 'We use optional analytics cookies to measure visits and location trends for site improvement.'}</p>
|
||||
<div className="consent-actions">
|
||||
<button type="button" className="btn-primary" onClick={accept}>Accept</button>
|
||||
<button type="button" className="btn-secondary" onClick={decline}>Decline</button>
|
||||
@@ -332,79 +324,9 @@ function formatPubDate(raw: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function LatestEpisodesList() {
|
||||
const [episodes, setEpisodes] = useState<Episode[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [failed, setFailed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/episodes')
|
||||
.then(r => (r.ok ? r.json() : Promise.reject(new Error('fetch failed'))))
|
||||
.then((data: { episodes: Episode[] }) => {
|
||||
if (data.episodes.length === 0) setFailed(true)
|
||||
else setEpisodes(data.episodes)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(() => {
|
||||
setFailed(true)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="episode-list-loading">
|
||||
{[1, 2, 3].map(i => <div key={i} className="episode-skeleton" aria-hidden="true" />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
return (
|
||||
<div className="embed-wrap">
|
||||
<iframe
|
||||
title="Verse by Verse with Nate"
|
||||
src={SPOTIFY_EMBED_URL}
|
||||
width="100%"
|
||||
height="352"
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
style={{ borderRadius: '14px' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="episode-list">
|
||||
{episodes.map((ep, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={ep.link || SPOTIFY_SHOW_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="episode-card"
|
||||
>
|
||||
<div className="episode-card-meta">
|
||||
{ep.episode && <span className="episode-number">Ep. {ep.episode}</span>}
|
||||
{ep.pubDate && <span className="episode-date">{formatPubDate(ep.pubDate)}</span>}
|
||||
{ep.duration && <span className="episode-duration">{ep.duration}</span>}
|
||||
</div>
|
||||
<h3 className="episode-title">{ep.title}</h3>
|
||||
{ep.description && <p className="episode-desc">{ep.description}</p>}
|
||||
<span className="episode-listen-cta">
|
||||
<SpotifyIcon /> Listen →
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SiteHeader() {
|
||||
function SiteHeader({ content }: { content: SiteContent }) {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const spotifyUrl = content.platformSpotifyUrl || '/spotify'
|
||||
|
||||
return (
|
||||
<header className="site-header">
|
||||
@@ -426,12 +348,12 @@ function SiteHeader() {
|
||||
<NavLink to="/about" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>About</NavLink>
|
||||
<NavLink to="/contact" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Contact</NavLink>
|
||||
<a
|
||||
href={SPOTIFY_SHOW_URL}
|
||||
href={spotifyUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="header-cta"
|
||||
>
|
||||
Follow on Spotify
|
||||
{content.headerFollowLabel || 'Follow on Spotify'}
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -440,28 +362,36 @@ function SiteHeader() {
|
||||
}
|
||||
|
||||
function SiteFooter({ content }: { content: SiteContent }) {
|
||||
const spotifyUrl = content.platformSpotifyUrl || '/spotify'
|
||||
const appleUrl = content.platformAppleUrl || '/apple'
|
||||
const youtubeUrl = content.platformYoutubeUrl || 'https://www.youtube.com/@blackzebraem5558'
|
||||
const amazonUrl = content.platformAmazonUrl || '/amazon'
|
||||
const creatorUrl = content.platformCreatorUrl || 'https://creators.spotify.com/pod/profile/nmemmert/'
|
||||
const facebookUrl = content.platformFacebookUrl || 'https://facebook.com/versebyversewithnate'
|
||||
|
||||
return (
|
||||
<footer className="site-footer">
|
||||
<p className="footer-ornament">✦ ✦ ✦</p>
|
||||
<p className="footer-title">Verse by Verse with Nate</p>
|
||||
<p className="footer-sub">A Journey Through Scripture</p>
|
||||
<p className="footer-contact">
|
||||
Contact:{' '}
|
||||
<a href="mailto:hello@versebyversewithnate.us">hello@versebyversewithnate.us</a>
|
||||
</p>
|
||||
<p className="footer-response">We usually reply within 48 hours.</p>
|
||||
<p className="footer-title">{content.footerTitle || 'Verse by Verse with Nate'}</p>
|
||||
<p className="footer-sub">{content.footerSubtitle || 'A Journey Through Scripture'}</p>
|
||||
{content.footerEmail && (
|
||||
<p className="footer-contact">
|
||||
Contact:{' '}
|
||||
<a href={`mailto:${content.footerEmail}`}>{content.footerEmail}</a>
|
||||
</p>
|
||||
)}
|
||||
<nav className="footer-links" aria-label="Footer links">
|
||||
<a href={SPOTIFY_SHOW_URL} target="_blank" rel="noreferrer">Spotify</a>
|
||||
<a href={spotifyUrl} target="_blank" rel="noreferrer">Spotify</a>
|
||||
<span aria-hidden="true">·</span>
|
||||
<a href={APPLE_PODCASTS_URL} target="_blank" rel="noreferrer">Apple Podcasts</a>
|
||||
<a href={appleUrl} target="_blank" rel="noreferrer">Apple Podcasts</a>
|
||||
<span aria-hidden="true">·</span>
|
||||
<a href={YOUTUBE_URL} target="_blank" rel="noreferrer">YouTube</a>
|
||||
<a href={youtubeUrl} target="_blank" rel="noreferrer">YouTube</a>
|
||||
<span aria-hidden="true">·</span>
|
||||
<a href={AMAZON_MUSIC_URL} target="_blank" rel="noreferrer">Amazon Music</a>
|
||||
<a href={amazonUrl} target="_blank" rel="noreferrer">Amazon Music</a>
|
||||
<span aria-hidden="true">·</span>
|
||||
<a href={SPOTIFY_CREATOR_URL} target="_blank" rel="noreferrer">Creator Profile</a>
|
||||
<a href={creatorUrl} target="_blank" rel="noreferrer">Creator Profile</a>
|
||||
<span aria-hidden="true">·</span>
|
||||
<a href={FACEBOOK_URL} target="_blank" rel="noreferrer">Facebook</a>
|
||||
<a href={facebookUrl} target="_blank" rel="noreferrer">Facebook</a>
|
||||
{(content.customLinks ?? []).filter(l => l.placement === 'footer').flatMap(l => [
|
||||
<span key={`sep-${l.id}`} aria-hidden="true">·</span>,
|
||||
<a key={l.id} href={l.url} target="_blank" rel="noreferrer">{l.label}</a>,
|
||||
@@ -472,34 +402,40 @@ function SiteFooter({ content }: { content: SiteContent }) {
|
||||
<span aria-hidden="true">·</span>
|
||||
<Link to="/terms">Terms</Link>
|
||||
</nav>
|
||||
<p className="footer-copy">© 2026 Nate Emmert · Made with faith.</p>
|
||||
<p className="footer-privacy">
|
||||
Privacy: with consent, analytics may store masked IP-based location data (country/state/county/city) and returning visitor activity.
|
||||
</p>
|
||||
<p className="footer-copy">{content.footerCopyright || '© 2026 Nate Emmert · Made with faith.'}</p>
|
||||
{content.footerPrivacyNote && (
|
||||
<p className="footer-privacy">{content.footerPrivacyNote}</p>
|
||||
)}
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
function PlatformButtons({ content }: { content: SiteContent }) {
|
||||
const spotifyUrl = content.platformSpotifyUrl || '/spotify'
|
||||
const youtubeUrl = content.platformYoutubeUrl || 'https://www.youtube.com/@blackzebraem5558'
|
||||
const amazonUrl = content.platformAmazonUrl || '/amazon'
|
||||
const facebookUrl = content.platformFacebookUrl || 'https://facebook.com/versebyversewithnate'
|
||||
const appleUrl = content.platformAppleUrl || '/apple'
|
||||
|
||||
return (
|
||||
<div className="platform-buttons">
|
||||
<a href={SPOTIFY_SHOW_URL} target="_blank" rel="noreferrer" className="platform-btn spotify-btn">
|
||||
<a href={spotifyUrl} target="_blank" rel="noreferrer" className="platform-btn spotify-btn">
|
||||
<SpotifyIcon />
|
||||
Listen on Spotify
|
||||
</a>
|
||||
<a href={YOUTUBE_URL} target="_blank" rel="noreferrer" className="platform-btn youtube-btn">
|
||||
<a href={youtubeUrl} target="_blank" rel="noreferrer" className="platform-btn youtube-btn">
|
||||
<YouTubeIcon />
|
||||
YouTube
|
||||
</a>
|
||||
<a href={AMAZON_MUSIC_URL} target="_blank" rel="noreferrer" className="platform-btn amazon-btn">
|
||||
<a href={amazonUrl} target="_blank" rel="noreferrer" className="platform-btn amazon-btn">
|
||||
<AmazonMusicIcon />
|
||||
Amazon Music
|
||||
</a>
|
||||
<a href={FACEBOOK_URL} target="_blank" rel="noreferrer" className="platform-btn facebook-btn">
|
||||
<a href={facebookUrl} target="_blank" rel="noreferrer" className="platform-btn facebook-btn">
|
||||
<FacebookIcon />
|
||||
Facebook
|
||||
</a>
|
||||
<a href={APPLE_PODCASTS_URL} target="_blank" rel="noreferrer" className="platform-btn apple-btn">
|
||||
<a href={appleUrl} target="_blank" rel="noreferrer" className="platform-btn apple-btn">
|
||||
<img src="/images/apple-podcasts-badge.svg" alt="Listen on Apple Podcasts" className="apple-badge" />
|
||||
</a>
|
||||
{(content.customLinks ?? []).filter(l => l.placement === 'platforms').map(l => (
|
||||
@@ -519,14 +455,14 @@ function AboutSection({ content }: { content: SiteContent }) {
|
||||
<img src={content.aboutPhotoUrl || '/images/nate-photo.jpeg'} alt="Nate Emmert" />
|
||||
<div className="about-photo-divider" aria-hidden="true" />
|
||||
<div className="about-nate-copy">
|
||||
<p className="eyebrow">About Nate</p>
|
||||
<p className="eyebrow">{content.aboutEyebrow || 'About Nate'}</p>
|
||||
<div className="about-nate-rule" aria-hidden="true" />
|
||||
<p>{content.aboutNate}</p>
|
||||
<Link to="/episodes" className="about-nate-cta">Listen Now ↓</Link>
|
||||
<Link to="/episodes" className="about-nate-cta">{content.aboutListenBtnLabel || 'Listen Now ↓'}</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="about-text">
|
||||
<p className="eyebrow">About the Show</p>
|
||||
<p className="eyebrow">{content.aboutShowEyebrow || 'About the Show'}</p>
|
||||
<h2>{content.aboutShowHeading}</h2>
|
||||
<p>{content.aboutShowP1}</p>
|
||||
<p>{content.aboutShowP2}</p>
|
||||
@@ -585,51 +521,59 @@ function ContactSection({ content }: { content: SiteContent }) {
|
||||
<div className="section-inner contact-inner">
|
||||
<div className="contact-copy">
|
||||
<div className="contact-card">
|
||||
<p className="eyebrow">Get in Touch</p>
|
||||
<h2>Contact Nate</h2>
|
||||
<p className="eyebrow">{content.contactEyebrow || 'Get in Touch'}</p>
|
||||
<h2>{content.contactHeading || 'Contact Nate'}</h2>
|
||||
|
||||
<div className="contact-profile">
|
||||
<img
|
||||
src={content.contactPhotoUrl || '/images/nate-contact-photo.png'}
|
||||
alt="Nate"
|
||||
alt={content.contactName || 'Nate'}
|
||||
className="contact-profile-photo"
|
||||
/>
|
||||
<div className="contact-profile-meta">
|
||||
<p className="contact-profile-name">Nate</p>
|
||||
<p className="contact-profile-role">Bible teacher · Lynchburg, VA</p>
|
||||
<p className="contact-profile-name">{content.contactName || 'Nate'}</p>
|
||||
<p className="contact-profile-role">{content.contactRole || 'Bible teacher · Lynchburg, VA'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<blockquote className="contact-quote">
|
||||
<p>"I read every message - nothing blesses me more than hearing how God's Word is at work in your life."</p>
|
||||
</blockquote>
|
||||
{content.contactQuote && (
|
||||
<blockquote className="contact-quote">
|
||||
<p>"{content.contactQuote}"</p>
|
||||
</blockquote>
|
||||
)}
|
||||
|
||||
<p className="contact-intro">
|
||||
Ask a Bible question, share a testimony, or request a topic for a future episode.
|
||||
</p>
|
||||
{content.contactIntro && (
|
||||
<p className="contact-intro">{content.contactIntro}</p>
|
||||
)}
|
||||
|
||||
<div className="contact-points" aria-label="Contact response details">
|
||||
<div className="contact-point">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
<span>I read every message personally</span>
|
||||
</div>
|
||||
<div className="contact-point">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<span>Response within 48 hours</span>
|
||||
</div>
|
||||
{content.contactPoint1 && (
|
||||
<div className="contact-point">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
<span>{content.contactPoint1}</span>
|
||||
</div>
|
||||
)}
|
||||
{content.contactPoint2 && (
|
||||
<div className="contact-point">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<span>{content.contactPoint2}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="contact-scripture">
|
||||
<p>
|
||||
"Your word is a lamp to my feet and a light to my path." -{' '}
|
||||
<span className="contact-scripture-ref">Psalm 119:105</span>
|
||||
</p>
|
||||
</div>
|
||||
{content.contactVerse && (
|
||||
<div className="contact-scripture">
|
||||
<p>
|
||||
"{content.contactVerse}" -{' '}
|
||||
<span className="contact-scripture-ref">{content.contactVerseRef}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ContactForm />
|
||||
@@ -800,10 +744,15 @@ function DownloadDetailPage({ content }: { content: SiteContent }) {
|
||||
)
|
||||
}
|
||||
|
||||
function CustomBlocksSection({ content }: { content: SiteContent }) {
|
||||
function CustomBlocksSection({ content, page }: { content: SiteContent; page: string }) {
|
||||
const blocks = (content.customBlocks ?? []).filter(block => {
|
||||
const blockPage = block.page ?? 'downloads'
|
||||
return blockPage === page
|
||||
})
|
||||
if (blocks.length === 0) return null
|
||||
return (
|
||||
<>
|
||||
{(content.customBlocks ?? []).map(block => (
|
||||
{blocks.map(block => (
|
||||
<section key={block.id} className="section-custom-block" aria-label={block.heading}>
|
||||
<div className="section-inner custom-block-inner">
|
||||
<h2>{block.heading}</h2>
|
||||
@@ -818,7 +767,8 @@ function CustomBlocksSection({ content }: { content: SiteContent }) {
|
||||
function LandingPage({ content }: { content: SiteContent }) {
|
||||
return (
|
||||
<div className="site">
|
||||
<SiteHeader />
|
||||
<SiteHeader content={content} />
|
||||
<CustomBlocksSection content={content} page="homepage" />
|
||||
|
||||
{/* ── HERO ── */}
|
||||
<section className="hero" aria-label="Show introduction">
|
||||
@@ -840,15 +790,16 @@ function LandingPage({ content }: { content: SiteContent }) {
|
||||
<p className="tagline">{content.heroTagline}</p>
|
||||
<div className="hero-ctas">
|
||||
<a
|
||||
href={SPOTIFY_SHOW_URL}
|
||||
href={content.platformSpotifyUrl || '/spotify'}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn-primary"
|
||||
>
|
||||
<SpotifyIcon />
|
||||
Listen on Spotify
|
||||
{content.heroBtnSpotify || 'Listen on Spotify'}
|
||||
</a>
|
||||
<Link to="/episodes" className="btn-secondary">Explore Episodes ↓</Link>
|
||||
<Link to="/episodes" className="btn-secondary">{content.heroBtnEpisodes || 'Explore Episodes ↓'}</Link>
|
||||
<Link to="/start-here" className="btn-start-here">{content.heroBtnStartHere || 'New here? Start here →'}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -871,15 +822,15 @@ function LandingPage({ content }: { content: SiteContent }) {
|
||||
<h3>{content.seriesTitle}</h3>
|
||||
<p>{content.seriesDescription}</p>
|
||||
<a
|
||||
href={content.seriesListenUrl || SPOTIFY_SHOW_URL}
|
||||
href={content.seriesListenUrl || content.platformSpotifyUrl || '/spotify'}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn-primary"
|
||||
>
|
||||
<SpotifyIcon />
|
||||
Listen to Series
|
||||
{content.seriesListenBtnLabel || 'Listen to Series'}
|
||||
</a>
|
||||
<Link to="/resources" className="btn-secondary">View Downloads</Link>
|
||||
<Link to="/resources" className="btn-secondary">{content.seriesDownloadsBtnLabel || 'View Downloads'}</Link>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -889,37 +840,23 @@ function LandingPage({ content }: { content: SiteContent }) {
|
||||
<section className="section-home-jump" aria-label="Homepage navigation">
|
||||
<div className="section-inner">
|
||||
<div className="home-jump-head">
|
||||
<p className="eyebrow">Where to Next</p>
|
||||
<h2 className="section-heading">Choose the page you need.</h2>
|
||||
<p className="eyebrow">{content.whereToNextEyebrow || 'Where to Next'}</p>
|
||||
<h2 className="section-heading">{content.whereToNextHeading || 'Choose the page you need.'}</h2>
|
||||
</div>
|
||||
<div className="home-jump-grid">
|
||||
<Link to="/episodes" className="home-jump-card">
|
||||
<h3>Episodes</h3>
|
||||
<p>Listen to latest episodes and platform links.</p>
|
||||
</Link>
|
||||
<Link to="/resources" className="home-jump-card">
|
||||
<h3>Downloads</h3>
|
||||
<p>Main guide, extra downloads, and past study files.</p>
|
||||
</Link>
|
||||
<Link to="/questions" className="home-jump-card">
|
||||
<h3>Q&A</h3>
|
||||
<p>Browse Bible questions and approved answers.</p>
|
||||
</Link>
|
||||
<Link to="/about" className="home-jump-card">
|
||||
<h3>About</h3>
|
||||
<p>Learn about Nate and the mission of the show.</p>
|
||||
</Link>
|
||||
<Link to="/contact" className="home-jump-card">
|
||||
<h3>Contact</h3>
|
||||
<p>Submit a Bible question, testimony, or topic.</p>
|
||||
</Link>
|
||||
{(content.whereToNextCards ?? []).map(card => (
|
||||
<Link key={card.id} to={card.path} className="home-jump-card">
|
||||
<h3>{card.title}</h3>
|
||||
<p>{card.description}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SiteFooter content={content} />
|
||||
|
||||
<AnalyticsConsentBanner />
|
||||
<AnalyticsConsentBanner content={content} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -931,7 +868,7 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {
|
||||
if (!episode) {
|
||||
return (
|
||||
<div className="site">
|
||||
<SiteHeader />
|
||||
<SiteHeader content={content} />
|
||||
<section className="section-player">
|
||||
<div className="section-inner" style={{ textAlign: 'center', padding: '4rem 1rem' }}>
|
||||
<h2>Episode not found</h2>
|
||||
@@ -940,7 +877,7 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {
|
||||
</div>
|
||||
</section>
|
||||
<SiteFooter content={content} />
|
||||
<AnalyticsConsentBanner />
|
||||
<AnalyticsConsentBanner content={content} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -949,7 +886,7 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {
|
||||
|
||||
return (
|
||||
<div className="site">
|
||||
<SiteHeader />
|
||||
<SiteHeader content={content} />
|
||||
<section className="section-episode-detail" aria-label={episode.title}>
|
||||
<div className="section-inner">
|
||||
<Link to="/episodes" className="episode-detail-back">← Back to Episodes</Link>
|
||||
@@ -1000,28 +937,154 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {
|
||||
</div>
|
||||
</section>
|
||||
<SiteFooter content={content} />
|
||||
<AnalyticsConsentBanner />
|
||||
<AnalyticsConsentBanner content={content} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EpisodesPage({ content }: { content: SiteContent }) {
|
||||
const [allEpisodes, setAllEpisodes] = useState<Episode[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [failed, setFailed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/episodes/all')
|
||||
.then(r => (r.ok ? r.json() : Promise.reject(new Error('fetch failed'))))
|
||||
.then((data: { episodes: Episode[] }) => {
|
||||
if (data.episodes.length === 0) setFailed(true)
|
||||
else setAllEpisodes(data.episodes)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(() => {
|
||||
setFailed(true)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const archivedSeries = content.archivedSeries ?? []
|
||||
|
||||
const archivedEpisodeNums = new Set<number>(
|
||||
archivedSeries.flatMap(s => {
|
||||
if (!s.episodeRange) return []
|
||||
const nums: number[] = []
|
||||
for (let i = s.episodeRange.from; i <= s.episodeRange.to; i++) nums.push(i)
|
||||
return nums
|
||||
})
|
||||
)
|
||||
|
||||
const currentEpisodes = allEpisodes.filter(ep => {
|
||||
const num = parseInt(ep.episode, 10)
|
||||
return isNaN(num) || !archivedEpisodeNums.has(num)
|
||||
})
|
||||
|
||||
function episodesForSeries(series: ArchivedSeries) {
|
||||
if (!series.episodeRange) return []
|
||||
const { from, to } = series.episodeRange
|
||||
return allEpisodes.filter(ep => {
|
||||
const num = parseInt(ep.episode, 10)
|
||||
return !isNaN(num) && num >= from && num <= to
|
||||
})
|
||||
}
|
||||
|
||||
function renderEpisodeCards(episodes: Episode[]) {
|
||||
return (
|
||||
<div className="episode-list">
|
||||
{episodes.map((ep, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={ep.link || content.platformSpotifyUrl || '/spotify'}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="episode-card"
|
||||
>
|
||||
<div className="episode-card-meta">
|
||||
{ep.episode && <span className="episode-number">Ep. {ep.episode}</span>}
|
||||
{ep.pubDate && <span className="episode-date">{formatPubDate(ep.pubDate)}</span>}
|
||||
{ep.duration && <span className="episode-duration">{ep.duration}</span>}
|
||||
</div>
|
||||
<h3 className="episode-title">{ep.title}</h3>
|
||||
{ep.description && <p className="episode-desc">{ep.description}</p>}
|
||||
<span className="episode-listen-cta">
|
||||
<SpotifyIcon /> Listen →
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const spotifyFallback = (
|
||||
<div className="embed-wrap">
|
||||
<iframe
|
||||
title="Verse by Verse with Nate"
|
||||
src={SPOTIFY_EMBED_URL}
|
||||
width="100%"
|
||||
height="352"
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
style={{ borderRadius: '14px' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
const loadingSkeleton = (
|
||||
<div className="episode-list-loading">
|
||||
{[1, 2, 3].map(i => <div key={i} className="episode-skeleton" aria-hidden="true" />)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="site">
|
||||
<SiteHeader />
|
||||
<section className="section-player" aria-label="Podcast episodes">
|
||||
<SiteHeader content={content} />
|
||||
<section className="section-player" aria-label="Current series episodes">
|
||||
<div className="section-inner">
|
||||
<h2 className="section-heading">
|
||||
<span className="ornament">✦</span> Latest Episodes{' '}
|
||||
<span className="ornament">✦</span> {content.seriesLabel || 'Now Playing'}{' '}
|
||||
<span className="ornament">✦</span>
|
||||
</h2>
|
||||
<LatestEpisodesList />
|
||||
{content.seriesTitle && <p className="episode-series-subtitle">{content.seriesTitle}</p>}
|
||||
{loading
|
||||
? loadingSkeleton
|
||||
: failed || currentEpisodes.length === 0
|
||||
? spotifyFallback
|
||||
: renderEpisodeCards(currentEpisodes)}
|
||||
<PlatformButtons content={content} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{archivedSeries.map(series => {
|
||||
const episodes = episodesForSeries(series)
|
||||
return (
|
||||
<section key={series.id} className="section-player" aria-label={series.title}>
|
||||
<div className="section-inner">
|
||||
<h2 className="section-heading">
|
||||
<span className="ornament">✦</span> {series.label || 'Archived Study'}{' '}
|
||||
<span className="ornament">✦</span>
|
||||
</h2>
|
||||
{series.title && <p className="episode-series-subtitle">{series.title}</p>}
|
||||
{series.description && <p className="episode-series-desc">{series.description}</p>}
|
||||
{loading
|
||||
? loadingSkeleton
|
||||
: episodes.length > 0
|
||||
? renderEpisodeCards(episodes)
|
||||
: series.listenUrl
|
||||
? (
|
||||
<a href={series.listenUrl} target="_blank" rel="noreferrer" className="btn-primary" style={{ display: 'inline-block', marginBottom: '1.5rem' }}>
|
||||
Listen to Full Series →
|
||||
</a>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
|
||||
<PodcastHighlightsSection content={content} />
|
||||
<CustomBlocksSection content={content} page="episodes" />
|
||||
<SiteFooter content={content} />
|
||||
<AnalyticsConsentBanner />
|
||||
<AnalyticsConsentBanner content={content} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1029,12 +1092,12 @@ function EpisodesPage({ content }: { content: SiteContent }) {
|
||||
function ResourcesPage({ content }: { content: SiteContent }) {
|
||||
return (
|
||||
<div className="site">
|
||||
<SiteHeader />
|
||||
<SiteHeader content={content} />
|
||||
<StudyGuideSection content={content} />
|
||||
<DownloadLibrarySection content={content} />
|
||||
<CustomBlocksSection content={content} />
|
||||
<CustomBlocksSection content={content} page="downloads" />
|
||||
<SiteFooter content={content} />
|
||||
<AnalyticsConsentBanner />
|
||||
<AnalyticsConsentBanner content={content} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1042,10 +1105,11 @@ function ResourcesPage({ content }: { content: SiteContent }) {
|
||||
function AboutPage({ content }: { content: SiteContent }) {
|
||||
return (
|
||||
<div className="site">
|
||||
<SiteHeader />
|
||||
<SiteHeader content={content} />
|
||||
<AboutSection content={content} />
|
||||
<CustomBlocksSection content={content} page="about" />
|
||||
<SiteFooter content={content} />
|
||||
<AnalyticsConsentBanner />
|
||||
<AnalyticsConsentBanner content={content} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1053,10 +1117,11 @@ function AboutPage({ content }: { content: SiteContent }) {
|
||||
function ContactPage({ content }: { content: SiteContent }) {
|
||||
return (
|
||||
<div className="site">
|
||||
<SiteHeader />
|
||||
<SiteHeader content={content} />
|
||||
<ContactSection content={content} />
|
||||
<CustomBlocksSection content={content} page="contact" />
|
||||
<SiteFooter content={content} />
|
||||
<AnalyticsConsentBanner />
|
||||
<AnalyticsConsentBanner content={content} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1447,6 +1512,7 @@ function QuestionsPage() {
|
||||
function PreviewPage() {
|
||||
const [content, setContent] = useState<SiteContent | null>(null)
|
||||
const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading')
|
||||
const [previewView, setPreviewView] = useState<string>('homepage')
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/admin-content?source=draft')
|
||||
@@ -1464,6 +1530,20 @@ function PreviewPage() {
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Listen for live content updates + view from the admin parent frame
|
||||
useEffect(() => {
|
||||
function handleMessage(e: MessageEvent) {
|
||||
if (e.origin !== window.location.origin) return
|
||||
if (e.data?.type === 'admin-preview-content' && e.data.content) {
|
||||
setContent({ ...DEFAULTS, ...e.data.content })
|
||||
setStatus('ready')
|
||||
if (e.data.view) setPreviewView(e.data.view)
|
||||
}
|
||||
}
|
||||
window.addEventListener('message', handleMessage)
|
||||
return () => window.removeEventListener('message', handleMessage)
|
||||
}, [])
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<main className="thanks-page" aria-label="Draft preview loading">
|
||||
@@ -1489,6 +1569,12 @@ function PreviewPage() {
|
||||
)
|
||||
}
|
||||
|
||||
if (previewView === 'start-here') return <StartHerePage content={content} />
|
||||
if (previewView === 'about') return <AboutPage content={content} />
|
||||
if (previewView === 'contact') return <ContactPage content={content} />
|
||||
if (previewView === 'current-series' || previewView === 'archived-series' || previewView === 'episode-highlights') {
|
||||
return <EpisodesPage content={content} />
|
||||
}
|
||||
return <LandingPage content={content} />
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,14 @@ export interface CustomBlock {
|
||||
id: string
|
||||
heading: string
|
||||
body: string
|
||||
page?: 'homepage' | 'start-here' | 'episodes' | 'downloads' | 'about' | 'contact' | 'questions'
|
||||
}
|
||||
|
||||
export interface WhereToNextCard {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface ArchivedSeriesResourceLink {
|
||||
@@ -39,6 +47,7 @@ export interface ArchivedSeries {
|
||||
studyGuideUrl: string
|
||||
resourceLinks: ArchivedSeriesResourceLink[]
|
||||
notes: ArchivedSeriesNote[]
|
||||
episodeRange?: { from: number; to: number }
|
||||
}
|
||||
|
||||
export interface RedirectRule {
|
||||
@@ -78,8 +87,13 @@ export interface LegalSettings {
|
||||
}
|
||||
|
||||
export interface SiteContent {
|
||||
// ── Hero ──
|
||||
eyebrow: string
|
||||
heroTagline: string
|
||||
heroBtnSpotify: string
|
||||
heroBtnEpisodes: string
|
||||
heroBtnStartHere: string
|
||||
// ── Start Here ──
|
||||
startHereHeading: string
|
||||
startHereIntro: string
|
||||
startHereStep1Title: string
|
||||
@@ -91,24 +105,66 @@ export interface SiteContent {
|
||||
startHereStep3Title: string
|
||||
startHereStep3Body: string
|
||||
startHereStep3Cta: string
|
||||
// ── About ──
|
||||
aboutEyebrow: string
|
||||
aboutListenBtnLabel: string
|
||||
aboutShowEyebrow: string
|
||||
aboutShowHeading: string
|
||||
aboutShowP1: string
|
||||
aboutShowP2: string
|
||||
aboutNate: string
|
||||
aboutPhotoUrl: string
|
||||
aboutVerseArtUrl: string
|
||||
// ── Contact ──
|
||||
contactPhotoUrl: string
|
||||
contactEyebrow: string
|
||||
contactHeading: string
|
||||
contactName: string
|
||||
contactRole: string
|
||||
contactQuote: string
|
||||
contactIntro: string
|
||||
contactPoint1: string
|
||||
contactPoint2: string
|
||||
contactVerse: string
|
||||
contactVerseRef: string
|
||||
// ── Series ──
|
||||
seriesLabel: string
|
||||
seriesTitle: string
|
||||
seriesDescription: string
|
||||
seriesImageUrl: string
|
||||
seriesListenUrl: string
|
||||
seriesListenBtnLabel: string
|
||||
seriesDownloadsBtnLabel: string
|
||||
// ── Downloads ──
|
||||
studyGuideTitle: string
|
||||
studyGuideDescription: string
|
||||
studyGuideDownloadUrl: string
|
||||
studyGuideUrl: string
|
||||
// ── Share ──
|
||||
shareHeading: string
|
||||
shareP: string
|
||||
// ── Homepage navigation cards ──
|
||||
whereToNextEyebrow: string
|
||||
whereToNextHeading: string
|
||||
whereToNextCards: WhereToNextCard[]
|
||||
// ── Global / Footer ──
|
||||
footerTitle: string
|
||||
footerSubtitle: string
|
||||
footerEmail: string
|
||||
footerCopyright: string
|
||||
footerPrivacyNote: string
|
||||
// ── Platform URLs ──
|
||||
platformSpotifyUrl: string
|
||||
platformAppleUrl: string
|
||||
platformYoutubeUrl: string
|
||||
platformAmazonUrl: string
|
||||
platformFacebookUrl: string
|
||||
platformCreatorUrl: string
|
||||
// ── Header ──
|
||||
headerFollowLabel: string
|
||||
// ── Cookie banner ──
|
||||
cookieBannerText: string
|
||||
// ── Arrays ──
|
||||
customLinks: CustomLink[]
|
||||
customBlocks: CustomBlock[]
|
||||
archivedSeries: ArchivedSeries[]
|
||||
@@ -121,6 +177,9 @@ export interface SiteContent {
|
||||
export const DEFAULTS: SiteContent = {
|
||||
eyebrow: 'A Journey Through Scripture',
|
||||
heroTagline: "Exploring God's Word one verse at a time",
|
||||
heroBtnSpotify: 'Listen on Spotify',
|
||||
heroBtnEpisodes: 'Explore Episodes ↓',
|
||||
heroBtnStartHere: 'New here? Start here →',
|
||||
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.',
|
||||
@@ -136,6 +195,9 @@ export const DEFAULTS: SiteContent = {
|
||||
startHereStep3Body:
|
||||
'Use the contact form to submit your Bible question for future Q&A or an upcoming episode.',
|
||||
startHereStep3Cta: 'Submit a Question',
|
||||
aboutEyebrow: 'About Nate',
|
||||
aboutListenBtnLabel: 'Listen Now ↓',
|
||||
aboutShowEyebrow: 'About the Show',
|
||||
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.',
|
||||
@@ -146,12 +208,24 @@ export const DEFAULTS: SiteContent = {
|
||||
aboutPhotoUrl: '/images/nate-photo.jpeg',
|
||||
aboutVerseArtUrl: '/images/hebrews-4-12-verse-art.png',
|
||||
contactPhotoUrl: '/images/nate-contact-photo.png',
|
||||
contactEyebrow: 'Get in Touch',
|
||||
contactHeading: 'Contact Nate',
|
||||
contactName: 'Nate',
|
||||
contactRole: 'Bible teacher · Lynchburg, VA',
|
||||
contactQuote: 'I read every message - nothing blesses me more than hearing how God\'s Word is at work in your life.',
|
||||
contactIntro: 'Ask a Bible question, share a testimony, or request a topic for a future episode.',
|
||||
contactPoint1: 'I read every message personally',
|
||||
contactPoint2: 'Response within 48 hours',
|
||||
contactVerse: 'Your word is a lamp to my feet and a light to my path.',
|
||||
contactVerseRef: 'Psalm 119:105',
|
||||
seriesLabel: 'Now Playing',
|
||||
seriesTitle: 'Study of Titus: Sound Doctrine',
|
||||
seriesDescription:
|
||||
"A deep-dive into Paul's letter to Titus — unpacking what it means to build a church and a life on sound doctrine.",
|
||||
seriesImageUrl: '/images/titus-cover.png',
|
||||
seriesListenUrl: '/spotify',
|
||||
seriesListenBtnLabel: 'Listen to Series',
|
||||
seriesDownloadsBtnLabel: 'View Downloads',
|
||||
studyGuideTitle: 'Companion Study Guide',
|
||||
studyGuideDescription:
|
||||
'Go deeper in your study with the official Verse by Verse companion guide — now available on Amazon.',
|
||||
@@ -159,6 +233,28 @@ export const DEFAULTS: SiteContent = {
|
||||
studyGuideUrl: 'https://a.co/d/01sG2tOJ',
|
||||
shareHeading: 'Help one more person hear the Word this week.',
|
||||
shareP: 'Scan the QR code or text the show link to a friend who needs encouragement today.',
|
||||
whereToNextEyebrow: 'Where to Next',
|
||||
whereToNextHeading: 'Choose the page you need.',
|
||||
whereToNextCards: [
|
||||
{ id: 'episodes', title: 'Episodes', description: 'Listen to latest episodes and platform links.', path: '/episodes' },
|
||||
{ id: 'downloads', title: 'Downloads', description: 'Main guide, extra downloads, and past study files.', path: '/resources' },
|
||||
{ id: 'qa', title: 'Q&A', description: 'Browse Bible questions and approved answers.', path: '/questions' },
|
||||
{ id: 'about', title: 'About', description: 'Learn about Nate and the mission of the show.', path: '/about' },
|
||||
{ id: 'contact', title: 'Contact', description: 'Submit a Bible question, testimony, or topic.', path: '/contact' },
|
||||
],
|
||||
footerTitle: 'Verse by Verse with Nate',
|
||||
footerSubtitle: 'A Journey Through Scripture',
|
||||
footerEmail: 'hello@versebyversewithnate.us',
|
||||
footerCopyright: '© 2026 Nate Emmert · Made with faith.',
|
||||
footerPrivacyNote: 'Privacy: with consent, analytics may store masked IP-based location data (country/state/county/city) and returning visitor activity.',
|
||||
platformSpotifyUrl: '/spotify',
|
||||
platformAppleUrl: '/apple',
|
||||
platformYoutubeUrl: 'https://www.youtube.com/@blackzebraem5558',
|
||||
platformAmazonUrl: '/amazon',
|
||||
platformFacebookUrl: 'https://facebook.com/versebyversewithnate',
|
||||
platformCreatorUrl: 'https://creators.spotify.com/pod/profile/nmemmert/',
|
||||
headerFollowLabel: 'Follow on Spotify',
|
||||
cookieBannerText: 'We use optional analytics cookies to measure visits and location trends for site improvement.',
|
||||
customLinks: [],
|
||||
customBlocks: [],
|
||||
archivedSeries: [],
|
||||
|
||||
Reference in New Issue
Block a user