Redo detail, study, utility and gate pages; fix admin live preview (CSP frame-src self) and preview every admin section; v2.38.0
Publish Container / build (push) Successful in 15m34s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-13 14:23:21 -04:00
1 parent 5d5c026762
commit ea463b5851
6 files changed
+602 -390

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "siteforge",
"private": true,
"version": "2.37.0",
"version": "2.38.0",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -102,7 +102,7 @@ app.use((_req, res, next) => {
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"media-src 'self' https:",
"frame-src https:",
"frame-src 'self' https:",
"connect-src 'self' https:",
].join('; '),
)
+14 -6
View File
@@ -88,15 +88,22 @@ table[align="left"] { float: none !important; margin-left: auto !important; marg
</style>
</head>`
const topBar = `
<div style="background:#050505;border-bottom:1px solid rgba(184,151,42,0.25);padding:14px 20px;font-family:Georgia,serif">
<div style="max-width:640px;margin:0 auto;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
<a href="${BASE}/" style="color:#e8d9b5;text-decoration:none;font-size:15px;letter-spacing:0.04em">Verse by Verse with Nate</a>
<a href="${BASE}/newsletter" style="color:#B8972A;text-decoration:none;font-size:12px;letter-spacing:0.14em;text-transform:uppercase">&larr; All issues</a>
</div></div>`
const ctaBlock = `
<table border="0" width="100%" cellpadding="0" cellspacing="0" role="presentation" align="center">
<tbody><tr><td align="center" style="padding:32px 20px 48px;background-color:#080807">
<tbody><tr><td align="center" style="padding:40px 20px 56px;background-color:#080807;border-top:1px solid rgba(184,151,42,0.2)">
<table width="580" border="0" cellpadding="0" cellspacing="0" role="presentation" style="max-width:580px;width:100%">
<tbody><tr><td align="center" style="padding:0">
<p style="margin:0 0 16px;font-family:Georgia,serif;font-size:13px;color:#6b6044">
<a href="${BASE}/newsletter" style="color:#6b6044;text-decoration:none">&larr; Newsletter Archive</a>
</p>
<a href="${BASE}/subscribe" style="display:inline-block;padding:12px 32px;font-family:Georgia,serif;font-size:12px;letter-spacing:0.16em;text-transform:uppercase;color:#B8972A;border:1px solid #B8972A;text-decoration:none">Subscribe by Email</a>
<tbody><tr><td align="center" style="padding:0;font-family:Georgia,serif">
<p style="margin:0 0 8px;font-size:12px;letter-spacing:0.18em;text-transform:uppercase;color:#B8972A">Stay in the Word</p>
<p style="margin:0 0 22px;font-size:22px;line-height:1.3;color:#f0e9cc">Get the next issue in your inbox</p>
<a href="${BASE}/subscribe" style="display:inline-block;padding:13px 34px;font-size:12px;letter-spacing:0.16em;text-transform:uppercase;color:#0c0c08;background:#B8972A;text-decoration:none;border-radius:4px;font-weight:bold">Subscribe by Email</a>
<p style="margin:26px 0 0;font-size:13px"><a href="${BASE}/newsletter" style="color:#8a7e55;text-decoration:none">&larr; Back to the newsletter archive</a></p>
</td></tr></tbody></table>
</td></tr></tbody></table>
</body>`
@@ -104,6 +111,7 @@ table[align="left"] { float: none !important; margin-left: auto !important; marg
let html = stripUnsubscribeLink(issue.html)
html = html.replace(/<title>[\s\S]*?<\/title>/i, '')
html = /<\/head>/i.test(html) ? html.replace(/<\/head>/i, metaBlock) : html.replace(/<head[^>]*>/i, m => `${m}${metaBlock.replace('</head>', '')}`)
html = html.replace(/<body[^>]*>/i, m => `${m}${topBar}`)
html = /<\/body>/i.test(html) ? html.replace(/<\/body>/i, ctaBlock) : `${html}${ctaBlock}`
return html
}
+171
View File
@@ -13979,3 +13979,174 @@ a.nl-featured:hover { transform: translateY(-2px); border-color: rgba(201, 168,
.nug-tag--active, .nug-tag--active:hover { background: #c9a84c; color: #0c0c08; border-color: #c9a84c; }
.nug-tag--small { font-size: 0.74rem; padding: 0.15rem 0.6rem; }
.nug-card-tags { display: flex; flex-wrap: wrap; gap: 0.35rem; }
/* ── Episode detail ── */
.ed-hero { padding: 2.5rem 0 2rem; background: radial-gradient(ellipse at top left, rgba(201, 168, 76, 0.09), transparent 60%); border-bottom: 1px solid rgba(201, 168, 76, 0.15); }
.ed-hero-inner { }
.ed-title { font-family: var(--brand-font-heading); font-size: clamp(1.9rem, 4.5vw, 3rem); line-height: 1.12; color: #f0e9cc; margin: 0.2rem 0 0.8rem; }
.ed-summary { font-size: 1.1rem; line-height: 1.65; color: #a89a6a; margin: 0 0 1.5rem; }
.ed-player { margin-bottom: 1rem; }
.ed-listen { display: inline-block; }
.ed-body { padding: 2.5rem 0 4rem; }
.ed-body-grid { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(240px, 1fr); gap: 2.5rem; align-items: start; }
.ed-body-grid--single { grid-template-columns: minmax(0, 760px); justify-content: center; }
.ed-block { margin-bottom: 2.25rem; }
.ed-heading { font-family: var(--brand-font-heading); font-size: 1.5rem; color: #e8d9b5; margin: 0 0 0.8rem; }
.ed-text { color: #c9bf9a; line-height: 1.75; margin: 0; }
.ed-quote { margin: 0 0 1rem; padding: 0.4rem 0 0.4rem 1.1rem; border-left: 3px solid #c9a84c; color: #e8d9b5; font-style: italic; line-height: 1.6; }
.ed-questions { margin: 0; padding-left: 1.3rem; color: #c9bf9a; line-height: 1.65; display: grid; gap: 0.7rem; }
.ed-questions li::marker { color: #c9a84c; font-weight: 700; }
.ed-side { display: grid; gap: 1rem; position: sticky; top: 5rem; }
.ed-card { background: #111; border: 1px solid rgba(201, 168, 76, 0.18); border-radius: 14px; padding: 1.1rem 1.25rem; }
.ed-side-heading { font-size: 0.75rem; letter-spacing: 0.14em; text-transform: uppercase; color: #c9a84c; margin: 0 0 0.8rem; font-family: inherit; }
.ed-chips { display: flex; flex-wrap: wrap; gap: 0.45rem; }
.ed-chip { font-size: 0.85rem; color: #e8d9b5; border: 1px solid rgba(201, 168, 76, 0.35); border-radius: 999px; padding: 0.25rem 0.8rem; }
.ed-list { margin: 0; padding-left: 1.1rem; color: #c9bf9a; line-height: 1.55; display: grid; gap: 0.4rem; font-size: 0.92rem; }
@media (max-width: 800px) { .ed-body-grid { grid-template-columns: 1fr; } .ed-side { position: static; } }
/* ── Host detail ── */
.hd-hero { padding: 2.5rem 0 2rem; background: radial-gradient(ellipse at top left, rgba(201, 168, 76, 0.09), transparent 60%); border-bottom: 1px solid rgba(201, 168, 76, 0.15); }
.hd-hero-inner { display: flex; gap: 2rem; align-items: center; flex-wrap: wrap; }
.hd-photo { width: 180px; height: 180px; object-fit: cover; border-radius: 50%; border: 3px solid rgba(201, 168, 76, 0.4); box-shadow: 0 10px 36px rgba(0, 0, 0, 0.45); flex-shrink: 0; }
.hd-hero-body { flex: 1; min-width: 240px; }
.hd-name { font-family: var(--brand-font-heading); font-size: clamp(2rem, 5vw, 3rem); color: #f0e9cc; margin: 0.2rem 0 0; line-height: 1.1; }
.hd-body { padding: 2.5rem 0 4rem; }
.hd-body-inner { }
.hd-bio { font-size: 1.08rem; line-height: 1.75; color: #c9bf9a; margin: 0 0 1.25rem; }
.hd-listen { margin-top: 2rem; padding: 1.4rem 1.5rem; background: #111; border: 1px solid rgba(201, 168, 76, 0.2); border-radius: 14px; }
.hd-listen .eyebrow { margin-bottom: 0.8rem; }
.hd-others { margin-top: 2.5rem; }
.hd-back { display: inline-block; margin-top: 2.5rem; }
@media (max-width: 640px) { .hd-photo { width: 120px; height: 120px; } }
/* ── Finished series page ── */
.fsp-hero { padding: 2.5rem 0 2.25rem; background: radial-gradient(ellipse at top left, rgba(201, 168, 76, 0.09), transparent 60%); border-bottom: 1px solid rgba(201, 168, 76, 0.15); }
.fsp-hero-inner { display: flex; gap: 2.5rem; align-items: center; flex-wrap: wrap; }
.fsp-hero-body { flex: 1; min-width: 260px; }
.fsp-title { font-family: var(--brand-font-heading); font-size: clamp(2rem, 5vw, 3.2rem); line-height: 1.1; color: #f0e9cc; margin: 0.2rem 0 0.8rem; }
.fsp-desc { color: #a89a6a; line-height: 1.65; margin: 0 0 1.4rem; max-width: 60ch; }
.fsp-actions { display: flex; flex-wrap: wrap; gap: 0.7rem; }
.fsp-episodes { padding-top: 2rem; padding-bottom: 4rem; }
/* ── Centered utility pages (thanks, subscribe, 404, legal) ── */
.cp-main { padding: 3.5rem 1.25rem 4.5rem; background: radial-gradient(ellipse at top, rgba(201, 168, 76, 0.1), transparent 55%); display: flex; justify-content: center; }
.cp-card { width: min(640px, 100%); background: #121212; border: 1px solid rgba(201, 168, 76, 0.24); border-radius: 18px; box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5); padding: 2.5rem 2rem; text-align: center; }
.cp-icon { display: inline-grid; place-items: center; width: 56px; height: 56px; border-radius: 50%; background: rgba(201, 168, 76, 0.14); border: 1px solid rgba(201, 168, 76, 0.5); color: #c9a84c; font-size: 1.6rem; margin-bottom: 1rem; }
.cp-404 { font-family: var(--brand-font-heading); font-size: clamp(4rem, 14vw, 6.5rem); line-height: 1; margin: 0 0 0.25rem; color: #c9a84c; opacity: 0.85; }
.cp-title { font-family: var(--brand-font-heading); font-size: clamp(1.9rem, 5vw, 2.7rem); color: #f0e9cc; margin: 0.2rem 0 0.8rem; line-height: 1.15; }
.cp-text { color: #a89a6a; line-height: 1.7; margin: 0 auto 1.5rem; max-width: 46ch; }
.cp-text code { color: #e8d9b5; background: rgba(201, 168, 76, 0.1); padding: 0.1rem 0.4rem; border-radius: 4px; word-break: break-all; }
.cp-actions { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; margin: 1.5rem 0 0; }
.cp-note { font-size: 0.85rem; color: #8a7e55; margin: 1.25rem 0 0; }
.cp-note a { color: #c9a84c; }
.cp-form { display: grid; gap: 1rem; text-align: left; margin-top: 0.5rem; }
.cp-form label { display: grid; gap: 0.35rem; font-size: 0.85rem; color: #c9bf9a; }
.cp-form input { padding: 0.7rem 1rem; border-radius: 8px; border: 1px solid rgba(201, 168, 76, 0.3); background: #1a1a12; color: #f0e9cc; font-size: 1rem; outline: none; }
.cp-form input:focus { border-color: rgba(201, 168, 76, 0.7); }
.cp-form .btn-primary { justify-content: center; }
.cp-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; }
.cp-links { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.8rem; margin-top: 1.25rem; text-align: left; }
.cp-link-card { display: grid; gap: 0.2rem; padding: 0.9rem 1rem; background: #17170f; border: 1px solid rgba(201, 168, 76, 0.2); border-radius: 12px; text-decoration: none; transition: transform 160ms ease, border-color 160ms ease; }
.cp-link-card:hover { transform: translateY(-2px); border-color: rgba(201, 168, 76, 0.5); }
.cp-link-card strong { color: #e8d9b5; }
.cp-link-card span { color: #8a7e55; font-size: 0.82rem; }
.cp-legal { max-width: 760px; padding-top: 2.5rem; padding-bottom: 4rem; }
.cp-legal p { color: #c9bf9a; line-height: 1.8; margin: 0 0 1.1rem; }
@media (max-width: 520px) { .cp-card { padding: 1.75rem 1.25rem; } .cp-form-row { grid-template-columns: 1fr; } }
/* ── Study discussion hub ── */
.sdh-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; }
.sdh-card { display: flex; flex-direction: column; gap: 0.6rem; padding: 1.4rem; background: #111; border: 1px solid rgba(201, 168, 76, 0.2); border-radius: 14px; text-decoration: none; transition: transform 160ms ease, border-color 160ms ease; }
.sdh-card:hover { transform: translateY(-2px); border-color: rgba(201, 168, 76, 0.5); }
.sdh-card h3 { margin: 0; font-family: var(--brand-font-heading); font-size: 1.35rem; color: #f0e9cc; }
.sdh-card p { margin: 0; color: #a89a6a; line-height: 1.55; font-size: 0.95rem; }
.sdh-badge { align-self: flex-start; font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase; color: #8a7e55; border: 1px solid rgba(201, 168, 76, 0.25); border-radius: 999px; padding: 0.2rem 0.65rem; }
.sdh-badge--on { color: #c9a84c; border-color: rgba(201, 168, 76, 0.6); }
.sdh-go { margin-top: auto; padding-top: 0.5rem; color: #c9a84c; font-weight: 600; font-size: 0.9rem; }
/* ── Study overview ── */
.si-hero { padding: 2.5rem 0 2.25rem; background: radial-gradient(ellipse at top left, rgba(201, 168, 76, 0.1), transparent 60%); border-bottom: 1px solid rgba(201, 168, 76, 0.15); }
.si-hero-inner { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2.5rem; align-items: center; }
.si-title { font-family: var(--brand-font-heading); font-size: clamp(2rem, 5vw, 3.2rem); line-height: 1.1; color: #f0e9cc; margin: 0.2rem 0 0.8rem; }
.si-desc { color: #a89a6a; line-height: 1.65; margin: 0 0 1.1rem; max-width: 62ch; }
.si-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.4rem; }
.si-chips span { font-size: 0.8rem; color: #c9bf9a; border: 1px solid rgba(201, 168, 76, 0.25); border-radius: 999px; padding: 0.2rem 0.75rem; }
.si-actions { display: flex; flex-wrap: wrap; gap: 0.7rem; }
.si-progress { display: flex; align-items: center; gap: 1.1rem; padding: 1.2rem 1.5rem; background: #111; border: 1px solid rgba(201, 168, 76, 0.25); border-radius: 16px; }
.si-ring { --pct: 0%; width: 88px; height: 88px; border-radius: 50%; display: grid; place-items: center; background: conic-gradient(#c9a84c var(--pct), rgba(201, 168, 76, 0.15) 0); position: relative; }
.si-ring::before { content: ''; position: absolute; inset: 8px; border-radius: 50%; background: #111; }
.si-ring span { position: relative; font-weight: 700; color: #e8d9b5; }
.si-progress-big { margin: 0; font-family: var(--brand-font-heading); font-size: 1.6rem; color: #f0e9cc; }
.si-progress-sub { margin: 0; color: #a89a6a; font-size: 0.9rem; }
.si-notice { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: center; margin-top: 1.5rem; padding: 1rem 1.25rem; background: #17170f; border: 1px solid rgba(201, 168, 76, 0.3); border-radius: 12px; color: #a89a6a; }
.si-notice strong { color: #e8d9b5; }
.si-notice .btn-primary { margin-left: auto; }
.si-lessons { padding: 2rem 0 4rem; }
.si-chapter { margin-bottom: 1.25rem; }
.si-chapter-head { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; cursor: pointer; padding: 0.6rem 0; border-bottom: 1px solid rgba(201, 168, 76, 0.18); list-style: none; }
.si-chapter-head::-webkit-details-marker { display: none; }
.si-chapter-head h2 { margin: 0; font-family: var(--brand-font-heading); font-size: 1.4rem; color: #e8d9b5; }
.si-chapter-head span { font-size: 0.85rem; color: #8a7e55; }
.si-list { display: grid; gap: 0.7rem; margin-top: 1rem; }
.si-row { display: flex; gap: 1rem; align-items: center; padding: 1rem 1.2rem; background: #111; border: 1px solid rgba(201, 168, 76, 0.18); border-radius: 14px; text-decoration: none; transition: transform 160ms ease, border-color 160ms ease; }
.si-row:hover { transform: translateY(-1px); border-color: rgba(201, 168, 76, 0.5); }
.si-row--disabled { opacity: 0.55; cursor: default; }
.si-row--disabled:hover { transform: none; border-color: rgba(201, 168, 76, 0.18); }
.si-row--done { border-color: rgba(127, 212, 138, 0.35); }
.si-num { flex: 0 0 auto; width: 40px; height: 40px; display: grid; place-items: center; border-radius: 50%; border: 1px solid rgba(201, 168, 76, 0.45); color: #c9a84c; font-weight: 700; }
.si-row--done .si-num { background: rgba(127, 212, 138, 0.15); border-color: #7fd48a; color: #7fd48a; }
.si-row-body { flex: 1; min-width: 0; }
.si-row-top { display: flex; flex-wrap: wrap; gap: 0.4rem 0.7rem; align-items: center; }
.si-row-top h3 { margin: 0; font-size: 1.1rem; color: #f0e9cc; }
.si-ref { margin: 0.15rem 0 0; font-size: 0.8rem; letter-spacing: 0.06em; color: #c9a84c; }
.si-summary { margin: 0.35rem 0 0; color: #a89a6a; font-size: 0.92rem; line-height: 1.5; }
.si-arrow { color: #c9a84c; font-size: 1.2rem; }
@media (max-width: 800px) { .si-hero-inner { grid-template-columns: 1fr; } .si-progress { justify-self: start; } }
.si-focus { margin: 0.4rem 0 0; font-size: 0.85rem; font-style: italic; color: #8a7e55; line-height: 1.45; }
.si-focus span { font-style: normal; color: #c9a84c; }
/* ── Study lesson page ── */
.section-study-classroom { padding-top: 2rem; }
.study-classroom-shell { grid-template-columns: minmax(0, 1fr) 320px; gap: 2.5rem; }
.sl-head { padding-bottom: 1.5rem; margin-bottom: 0.5rem; border-bottom: 1px solid rgba(201, 168, 76, 0.18); }
.sl-head-meta { display: flex; flex-wrap: wrap; gap: 0.5rem 0.75rem; align-items: center; margin: 0.4rem 0 0.6rem; }
.sl-lesson-pill { font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; color: #0c0c08; background: #c9a84c; border-radius: 999px; padding: 0.2rem 0.75rem; font-weight: 700; }
.sl-ref { font-size: 0.85rem; letter-spacing: 0.08em; color: #c9a84c; }
.sl-done { font-size: 0.8rem; color: #7fd48a; border: 1px solid rgba(127, 212, 138, 0.5); border-radius: 999px; padding: 0.15rem 0.65rem; }
.sl-title { font-family: var(--brand-font-heading); font-size: clamp(2rem, 4.5vw, 2.9rem); line-height: 1.12; color: #f0e9cc; margin: 0 0 0.7rem; }
.sl-summary { font-size: 1.1rem; line-height: 1.65; color: #a89a6a; max-width: 62ch; margin: 0 0 1.1rem; }
.sl-progress { display: grid; gap: 0.4rem; max-width: 420px; font-size: 0.8rem; color: #8a7e55; }
.sl-progress .study-lesson-progress-bar { height: 5px; margin: 0; }
.study-classroom-main .study-class-block { margin-top: 1.5rem; padding: 1.6rem 1.75rem; border-radius: 16px; background: #111; border-color: rgba(201, 168, 76, 0.2); }
.study-classroom-main .study-class-block h2 { font-size: 1.5rem; color: #e8d9b5; padding-right: 5rem; }
.study-classroom-main .study-detail-copy { font-size: 1.08rem; line-height: 1.85; color: #cfc6a4; margin-bottom: 1.1rem; max-width: 68ch; }
.study-classroom-main .study-detail-copy:last-child { margin-bottom: 0; }
.study-classroom-main .study-detail-copy--scripture { font-family: var(--brand-font-heading); font-size: 1.2rem; line-height: 1.9; color: #f0e9cc; padding-left: 1.1rem; border-left: 3px solid rgba(201, 168, 76, 0.55); }
.study-classroom-main .study-detail-list--questions { padding-left: 0; list-style: none; counter-reset: q; display: grid; gap: 1.25rem; }
.study-classroom-main .study-question-item { counter-increment: q; position: relative; padding-left: 2.6rem; }
.study-classroom-main .study-question-item::before { content: counter(q); position: absolute; left: 0; top: 0; width: 1.9rem; height: 1.9rem; display: grid; place-items: center; border-radius: 50%; border: 1px solid rgba(201, 168, 76, 0.5); color: #c9a84c; font-weight: 700; font-size: 0.85rem; }
.study-classroom-main .study-question-text { font-size: 1.02rem; color: #e8d9b5; }
.study-classroom-sidebar { top: 5rem; }
.study-class-side-block { border-radius: 14px; background: #111; border-color: rgba(201, 168, 76, 0.2); }
.study-class-side-block h3 { font-size: 0.75rem; letter-spacing: 0.14em; text-transform: uppercase; color: #c9a84c; font-family: inherit; }
.sl-pager { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 2rem; }
.sl-pager-link { display: grid; gap: 0.25rem; padding: 1rem 1.25rem; background: #111; border: 1px solid rgba(201, 168, 76, 0.22); border-radius: 14px; text-decoration: none; transition: border-color 160ms ease, transform 160ms ease; }
a.sl-pager-link:hover { border-color: rgba(201, 168, 76, 0.6); transform: translateY(-1px); }
.sl-pager-link span { font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase; color: #8a7e55; }
.sl-pager-link strong { color: #e8d9b5; font-weight: 600; }
.sl-pager-link--next { text-align: right; }
.sl-pager-link--wait { opacity: 0.7; }
@media (max-width: 960px) {
.study-classroom-shell { grid-template-columns: 1fr; }
.study-classroom-sidebar { position: static; }
}
@media (max-width: 560px) {
.study-classroom-main .study-class-block { padding: 1.2rem 1.1rem; }
.sl-pager { grid-template-columns: 1fr; }
.sl-pager-link--next { text-align: left; }
}
#root .ed-heading { font-size: 1.5rem; }
.ed-hero-inner > * { max-width: 760px; }
#root .ed-side .ed-side-heading { font-size: 0.75rem; letter-spacing: 0.14em; text-transform: uppercase; color: #c9a84c; margin: 0 0 0.8rem; font-family: inherit; font-weight: 600; line-height: 1.2; }
.auth-card--center .study-signup-form { text-align: left; }
.cp-main--bare { min-height: 100vh; align-items: center; }
+237 -235
View File
@@ -891,14 +891,13 @@ function DownloadDetailPage({ content }: { content: SiteContent }) {
if (!resource) {
return (
<main className="thanks-page" aria-label="Download not found">
<div className="thanks-card">
<CenteredPageFrame content={content} label="Download not found">
<p className="cp-404" aria-hidden="true">?</p>
<p className="eyebrow">Resources</p>
<h1>Download not found</h1>
<p>The download you requested is not available right now.</p>
<Link to="/resources" className="btn-primary">Back to Resources</Link>
</div>
</main>
<h1 className="cp-title">Download not found</h1>
<p className="cp-text">The download you requested is not available right now.</p>
<div className="cp-actions"><Link to="/resources" className="btn-primary">Back to Resources</Link></div>
</CenteredPageFrame>
)
}
@@ -1606,23 +1605,31 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {
}
const questions = episode.discussionQuestions ?? []
const themes = episode.showNotesThemes ?? []
const scripture = episode.showNotesScripture ?? []
const quotes = episode.showNotesQuotes ?? []
const resources = episode.showNotesResources ?? []
const notesText = episode.showNotesSummary || (!themes.length ? episode.showNotes : '')
const hasMain = Boolean(notesText) || quotes.length > 0 || questions.length > 0
const hasSide = themes.length > 0 || scripture.length > 0 || resources.length > 0
return (
<div className="site">
<SiteHeader content={content} />
<SiteSearchBar 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>
{episode.episodeNumber && (
<p className="episode-detail-number">Episode {episode.episodeNumber}</p>
)}
<h1 className="episode-detail-title">{episode.title}</h1>
{episode.summary && <p className="episode-detail-summary">{episode.summary}</p>}
<section className="ed-hero" aria-label={episode.title}>
<div className="section-inner ed-hero-inner">
<nav className="breadcrumb" aria-label="Breadcrumb">
<Link to="/episodes">Episodes</Link>
<span aria-hidden="true"> › </span>
<span>{episode.episodeNumber ? `Episode ${episode.episodeNumber}` : episode.title}</span>
</nav>
{episode.episodeNumber && <p className="eyebrow">Episode {episode.episodeNumber}</p>}
<h1 className="ed-title">{episode.title}</h1>
{episode.summary && <p className="ed-summary">{episode.summary}</p>}
{(episode.embedUrl || episode.url) && (
<div className="episode-detail-embed">
<div className="ed-player">
{resolvedEmbedUrl ? (
<EpisodeAudioPlayer src={resolvedEmbedUrl} title={episode.title} size="full" spotifyUrl={episode.url || content.platformSpotifyUrl} />
) : (
@@ -1630,67 +1637,66 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {
)}
</div>
)}
{episode.url && (
<a href={episode.url} target="_blank" rel="noreferrer" className="btn-primary episode-detail-listen-btn">
Listen on Podcast Platform →
</a>
<a href={episode.url} target="_blank" rel="noreferrer" className="btn-secondary ed-listen">Listen on your podcast app →</a>
)}
</div>
</section>
{(episode.showNotesSummary || (episode.showNotesThemes ?? []).length > 0 || (episode.showNotesScripture ?? []).length > 0 || (episode.showNotesQuotes ?? []).length > 0 || (episode.showNotesResources ?? []).length > 0 || episode.showNotes) && (
<div className="episode-detail-show-notes">
<h2 className="episode-detail-section-heading">Show Notes</h2>
{episode.showNotesSummary
? <p>{episode.showNotesSummary}</p>
: episode.showNotes && !(episode.showNotesThemes ?? []).length && <p>{episode.showNotes}</p>
}
{(episode.showNotesThemes ?? []).length > 0 && (
<div className="episode-detail-show-notes-section">
<h3 className="episode-detail-show-notes-heading">Key Themes</h3>
<ul className="episode-detail-show-notes-list">
{episode.showNotesThemes!.map((t, i) => <li key={i}>{t}</li>)}
</ul>
{(hasMain || hasSide) && (
<section className="ed-body" aria-label="Show notes">
<div className={`section-inner ed-body-grid${hasMain && hasSide ? '' : ' ed-body-grid--single'}`}>
{hasMain && (
<div className="ed-main">
{notesText && (
<div className="ed-block">
<h2 className="ed-heading">Show Notes</h2>
<p className="ed-text">{notesText}</p>
</div>
)}
{(episode.showNotesScripture ?? []).length > 0 && (
<div className="episode-detail-show-notes-section">
<h3 className="episode-detail-show-notes-heading">Scripture References</h3>
<ul className="episode-detail-show-notes-list">
{episode.showNotesScripture!.map((s, i) => <li key={i}>{s}</li>)}
</ul>
{quotes.length > 0 && (
<div className="ed-block">
<h2 className="ed-heading">Key Quotes</h2>
{quotes.map((q, i) => <blockquote key={i} className="ed-quote">{q}</blockquote>)}
</div>
)}
{(episode.showNotesQuotes ?? []).length > 0 && (
<div className="episode-detail-show-notes-section">
<h3 className="episode-detail-show-notes-heading">Key Quotes</h3>
{episode.showNotesQuotes!.map((q, i) => (
<blockquote key={i} className="episode-detail-show-notes-quote">{q}</blockquote>
))}
</div>
)}
{(episode.showNotesResources ?? []).length > 0 && (
<div className="episode-detail-show-notes-section">
<h3 className="episode-detail-show-notes-heading">Resources</h3>
<ul className="episode-detail-show-notes-list">
{episode.showNotesResources!.map((r, i) => <li key={i}>{r}</li>)}
</ul>
</div>
)}
</div>
)}
{questions.length > 0 && (
<div className="episode-detail-questions">
<h2 className="episode-detail-section-heading">Discussion Questions</h2>
<ol className="episode-detail-questions-list">
{questions.map((q, i) => (
<li key={i}>{q}</li>
))}
<div className="ed-block">
<h2 className="ed-heading">Discussion Questions</h2>
<ol className="ed-questions">
{questions.map((q, i) => <li key={i}>{q}</li>)}
</ol>
</div>
)}
</div>
)}
{hasSide && (
<aside className="ed-side">
{scripture.length > 0 && (
<div className="ed-card">
<h2 className="ed-side-heading">Scripture</h2>
<div className="ed-chips">
{scripture.map((r, i) => <span key={i} className="ed-chip">{r}</span>)}
</div>
</div>
)}
{themes.length > 0 && (
<div className="ed-card">
<h2 className="ed-side-heading">Key Themes</h2>
<ul className="ed-list">{themes.map((t, i) => <li key={i}>{t}</li>)}</ul>
</div>
)}
{resources.length > 0 && (
<div className="ed-card">
<h2 className="ed-side-heading">Resources</h2>
<ul className="ed-list">{resources.map((r, i) => <li key={i}>{r}</li>)}</ul>
</div>
)}
</aside>
)}
</div>
</section>
)}
<SiteFooter content={content} />
<AnalyticsConsentBanner content={content} />
</div>
@@ -2045,13 +2051,8 @@ function FinishedSeriesPage({ content }: { content: SiteContent }) {
<div className="site">
<SiteHeader content={content} />
<SiteSearchBar content={content} />
<main className="page-inner">
<nav className="breadcrumb" aria-label="Breadcrumb">
<Link to="/finished">Finished Books</Link>
<span aria-hidden="true"> › </span>
<span>{book.title}</span>
</nav>
<div className="finished-series-hero">
<section className="fsp-hero" aria-label={book.title}>
<div className="section-inner fsp-hero-inner">
{book.imageUrl && (
<div className="finished-series-cd" aria-hidden="true">
<div className="finished-series-cd-disc">
@@ -2061,32 +2062,20 @@ function FinishedSeriesPage({ content }: { content: SiteContent }) {
</div>
</div>
)}
<div>
<p className="finished-book-card-season">Season {book.season}</p>
<h1 className="finished-series-title">{book.title}</h1>
{book.description && <p className="finished-series-desc">{book.description}</p>}
<div className="finished-series-actions">
<a href="#episodes" className="finished-series-btn-stream">▶ Stream Episodes</a>
{book.downloadUrl && (
<a
href={book.downloadUrl}
target="_blank"
rel="noopener noreferrer"
className="finished-series-btn-download"
>
⬇ Download Album
</a>
)}
{book.studySlug && (
<Link to={`/study/${book.studySlug}`} className="finished-series-btn-download">
📖 Study This Book
</Link>
)}
{book.guide?.title && (
<Link to={`/downloads/book-guide--${book.id}`} className="finished-series-btn-download">
📄 {book.guide.title}
</Link>
)}
<div className="fsp-hero-body">
<nav className="breadcrumb" aria-label="Breadcrumb">
<Link to="/finished">Finished Books</Link>
<span aria-hidden="true"> › </span>
<span>{book.title}</span>
</nav>
<p className="eyebrow">Season {book.season}{!loading && episodes.length > 0 ? ` · ${episodes.length} episodes` : ''}</p>
<h1 className="fsp-title">{book.title}</h1>
{book.description && <p className="fsp-desc">{book.description}</p>}
<div className="fsp-actions">
<a href="#episodes" className="btn-primary">▶ Stream episodes</a>
{book.studySlug && <Link to={`/study/${book.studySlug}`} className="btn-secondary">Study this book</Link>}
{book.downloadUrl && <a href={book.downloadUrl} target="_blank" rel="noopener noreferrer" className="btn-secondary">Download album</a>}
{book.guide?.title && <Link to={`/downloads/book-guide--${book.id}`} className="btn-secondary">{book.guide.title}</Link>}
</div>
</div>
{book.downloadQrUrl && book.downloadUrl && (
@@ -2098,9 +2087,12 @@ function FinishedSeriesPage({ content }: { content: SiteContent }) {
</div>
)}
</div>
</section>
<main className="section-inner fsp-episodes">
<FinishedSeriesEpisodes highlights={book.highlights ?? []} episodes={episodes} loading={loading} />
</main>
<SiteFooter content={content} />
<AnalyticsConsentBanner content={content} />
</div>
)
}
@@ -2562,35 +2554,57 @@ function NuggetsHostDetailPage({ content }: { content: SiteContent }) {
)
}
const otherHosts = (nuggets.hosts ?? []).filter(h => !h.archived && h.id !== host.id)
return (
<div className="site">
<SiteHeader content={content} />
<SiteSearchBar content={content} />
<PageBanner eyebrow={nuggets.title} title={host.name} desc={host.role} />
<section className="section-nuggets-body" aria-label={`${host.name} profile`}>
<div className="section-inner nuggets-host-detail-layout">
<div className="nuggets-host-detail-photo-col">
{host.imageUrl && (
<img src={host.imageUrl} alt={host.name} className="nuggets-host-detail-photo" />
)}
<section className="hd-hero" aria-label={`${host.name} profile`}>
<div className="section-inner hd-hero-inner">
{host.imageUrl && <img src={host.imageUrl} alt={host.name} className="hd-photo" />}
<div className="hd-hero-body">
<nav className="breadcrumb" aria-label="Breadcrumb">
<Link to="/nuggets">{nuggets.title}</Link>
<span aria-hidden="true"> › </span>
<span>{host.name}</span>
</nav>
<p className="eyebrow">{host.role || 'Host'}</p>
<h1 className="hd-name">{host.name}</h1>
</div>
<div className="nuggets-host-detail-body">
</div>
</section>
<section className="hd-body">
<div className="section-inner hd-body-inner">
{host.bio && host.bio.split(/\n\n+/).map((para, i) => (
<p key={i} className="nuggets-host-detail-bio">{para.replace(/\n/g, ' ')}</p>
<p key={i} className="hd-bio">{para.replace(/\n/g, ' ')}</p>
))}
{(nuggets.spotifyUrl || nuggets.appleUrl) && (
<div className="nuggets-host-detail-listen">
<p className="eyebrow" style={{ marginBottom: '0.75rem' }}>Listen to {nuggets.title}</p>
<div className="hd-listen">
<p className="eyebrow">Listen to {nuggets.title}</p>
<div className="nuggets-platform-links">
{nuggets.spotifyUrl && <a href={nuggets.spotifyUrl} target="_blank" rel="noopener noreferrer" className="btn-primary">Listen on Spotify</a>}
{nuggets.appleUrl && <a href={nuggets.appleUrl} target="_blank" rel="noopener noreferrer" className="btn-secondary">Listen on Apple Podcasts</a>}
</div>
</div>
)}
<div style={{ marginTop: '2rem' }}>
<Link to="/nuggets" className="nuggets-host-detail-back">&larr; Back to {nuggets.title}</Link>
{otherHosts.length > 0 && (
<div className="hd-others">
<p className="eyebrow">Also on {nuggets.title}</p>
<div className="nug-hosts-grid">
{otherHosts.map(h => (
<Link key={h.id} to={`/nuggets/hosts/${h.id}`} className="nug-host-card">
{h.imageUrl && <img src={h.imageUrl} alt={h.name} className="nug-host-img" loading="lazy" />}
<div>
<span className="nug-host-name">{h.name}</span>
{h.role && <span className="nug-host-role">{h.role}</span>}
</div>
</Link>
))}
</div>
</div>
)}
<Link to="/nuggets" className="btn-secondary hd-back">← All {nuggets.title} episodes</Link>
</div>
</section>
<SiteFooter content={content} />
@@ -3059,19 +3073,26 @@ function StartHerePage({ content }: { content: SiteContent }) {
)
}
function ThankYouPage() {
function CenteredPageFrame({ content, children, label }: { content: SiteContent; children: React.ReactNode; label: string }) {
return (
<div className="site">
<SiteHeader content={content} />
<main className="cp-main" aria-label={label}>
<div className="cp-card">{children}</div>
</main>
<SiteFooter content={content} />
<AnalyticsConsentBanner content={content} />
</div>
)
}
function ThankYouPage({ content }: { content: SiteContent }) {
const navigate = useNavigate()
const [secondsLeft, setSecondsLeft] = useState(15)
useEffect(() => {
const timeoutId = window.setTimeout(() => {
navigate('/')
}, 15000)
const intervalId = window.setInterval(() => {
setSecondsLeft(s => (s > 0 ? s - 1 : 0))
}, 1000)
const timeoutId = window.setTimeout(() => navigate('/'), 15000)
const intervalId = window.setInterval(() => setSecondsLeft(s => (s > 0 ? s - 1 : 0)), 1000)
return () => {
window.clearTimeout(timeoutId)
window.clearInterval(intervalId)
@@ -3079,23 +3100,21 @@ function ThankYouPage() {
}, [navigate])
return (
<main className="thanks-page" aria-label="Thank you">
<div className="thanks-card">
<p className="eyebrow">Message Received</p>
<h1>Thank You</h1>
<p>
Your message was sent successfully. We appreciate you reaching out and will get back to you soon.
</p>
<p className="thanks-countdown">Returning to the main site in {secondsLeft} seconds...</p>
<button type="button" className="btn-primary" onClick={() => navigate('/')}>
Go Back Now
</button>
<CenteredPageFrame content={content} label="Thank you">
<span className="cp-icon" aria-hidden="true">✓</span>
<p className="eyebrow">Message received</p>
<h1 className="cp-title">Thank you</h1>
<p className="cp-text">Your message was sent. We appreciate you reaching out and will get back to you soon.</p>
<div className="cp-actions">
<Link to="/" className="btn-primary">Back to home</Link>
<Link to="/episodes" className="btn-secondary">Browse episodes</Link>
</div>
</main>
<p className="cp-note">Returning to the home page in {secondsLeft}s</p>
</CenteredPageFrame>
)
}
function SubscribePage() {
function SubscribePage({ content }: { content: SiteContent }) {
const navigate = useNavigate()
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
@@ -3139,15 +3158,11 @@ function SubscribePage() {
}
return (
<main className="thanks-page" aria-label="Subscribe">
<div className="thanks-card subscribe-card">
<CenteredPageFrame content={content} label="Subscribe">
<p className="eyebrow">Verse by Verse with Nate</p>
<h1>Subscribe for Updates</h1>
<p>
Get ministry updates and new episode announcements by email.
</p>
<form className="contact-form subscribe-form" onSubmit={handleSubmit} noValidate>
<h1 className="cp-title">Subscribe for updates</h1>
<p className="cp-text">New episodes, study resources and ministry updates, sent to your inbox. No spam, and you can unsubscribe any time.</p>
<form className="cp-form" onSubmit={handleSubmit} noValidate>
<input
type="text"
className="contact-honeypot"
@@ -3157,79 +3172,60 @@ function SubscribePage() {
value={honey}
onChange={e => setHoney(e.target.value)}
/>
<div className="cp-form-row">
<label>
First Name
<input
type="text"
name="firstName"
required
autoComplete="given-name"
value={firstName}
onChange={e => setFirstName(e.target.value)}
/>
First name
<input type="text" name="firstName" required autoComplete="given-name" value={firstName} onChange={e => setFirstName(e.target.value)} />
</label>
<label>
Last Name
<input
type="text"
name="lastName"
required
autoComplete="family-name"
value={lastName}
onChange={e => setLastName(e.target.value)}
/>
Last name
<input type="text" name="lastName" autoComplete="family-name" value={lastName} onChange={e => setLastName(e.target.value)} />
</label>
</div>
<label>
Email
<input
type="email"
name="email"
required
autoComplete="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<input type="email" name="email" required autoComplete="email" value={email} onChange={e => setEmail(e.target.value)} />
</label>
{status === 'error' && <p className="contact-error">{errorMsg}</p>}
{status === 'error' && <p className="contact-error" role="alert">{errorMsg}</p>}
<button type="submit" className="btn-primary" disabled={status === 'submitting'}>
{status === 'submitting' ? 'Subscribing…' : 'Subscribe'}
</button>
<Link to="/" className="btn-secondary">Back to Site</Link>
</form>
</div>
</main>
<p className="cp-note">Prefer to browse first? <Link to="/newsletter">Read past issues</Link></p>
</CenteredPageFrame>
)
}
function SubscribeThankYouPage() {
function SubscribeThankYouPage({ content }: { content: SiteContent }) {
return (
<main className="thanks-page" aria-label="Subscription confirmed">
<div className="thanks-card">
<CenteredPageFrame content={content} label="Subscription confirmed">
<span className="cp-icon" aria-hidden="true">✓</span>
<p className="eyebrow">Verse by Verse with Nate</p>
<h1>You are subscribed</h1>
<p>
Thank you for subscribing. You will receive future ministry updates and episode announcements.
</p>
<Link to="/" className="btn-primary">Back to Site</Link>
<h1 className="cp-title">You’re subscribed</h1>
<p className="cp-text">Thank you for subscribing. You’ll receive new episode announcements and ministry updates.</p>
<div className="cp-links">
<Link to="/episodes" className="cp-link-card"><strong>Listen now</strong><span>Start with the latest episode</span></Link>
<Link to="/study" className="cp-link-card"><strong>Join a study</strong><span>Go deeper, one lesson at a time</span></Link>
<Link to="/newsletter" className="cp-link-card"><strong>Past issues</strong><span>Read the newsletter archive</span></Link>
</div>
</main>
<div className="cp-actions"><Link to="/" className="btn-secondary">Back to home</Link></div>
</CenteredPageFrame>
)
}
function LegalPage({ title, body }: { title: string; body: string[] }) {
function LegalPage({ content, title, body }: { content: SiteContent; title: string; body: string[] }) {
return (
<main className="thanks-page" aria-label={title}>
<div className="thanks-card">
<p className="eyebrow">Verse by Verse with Nate</p>
<h1>{title}</h1>
<div className="site">
<SiteHeader content={content} />
<PageBanner eyebrow="Verse by Verse with Nate" title={title} />
<main className="section-inner cp-legal" aria-label={title}>
{body.map((line, idx) => (
<p key={`${title}-${idx}`}>{line}</p>
))}
<Link to="/" className="btn-primary">Back to Site</Link>
</div>
</main>
<SiteFooter content={content} />
<AnalyticsConsentBanner content={content} />
</div>
)
}
@@ -3239,7 +3235,7 @@ function LegacyCertificateRedirect() {
return <Navigate to={`/certificate/${encodeURIComponent(token ?? '')}`} replace />
}
function PublicCertificatePage() {
function PublicCertificatePage({ content }: { content: SiteContent }) {
const { token } = useParams<{ token: string }>()
const [cert, setCert] = useState<{ studyTitle: string; displayName: string; issuedAt: string } | null>(null)
const [status, setStatus] = useState<'loading' | 'not-found' | 'ready'>('loading')
@@ -3273,13 +3269,12 @@ function PublicCertificatePage() {
}
if (status === 'not-found' || !cert) {
return (
<main className="thanks-page">
<div className="thanks-card">
<h1>Certificate Not Found</h1>
<p>This certificate link is invalid or has been removed.</p>
<Link to="/" className="btn-primary">Back to Site</Link>
</div>
</main>
<CenteredPageFrame content={content} label="Certificate not found">
<p className="eyebrow">Certificate</p>
<h1 className="cp-title">Certificate not found</h1>
<p className="cp-text">This certificate link is invalid or has been removed.</p>
<div className="cp-actions"><Link to="/" className="btn-primary">Back to site</Link></div>
</CenteredPageFrame>
)
}
@@ -3713,11 +3708,11 @@ function PreviewPage() {
if (status === 'loading') {
return (
<main className="thanks-page" aria-label="Draft preview loading">
<div className="thanks-card">
<main className="cp-main cp-main--bare" aria-label="Draft preview loading">
<div className="cp-card">
<p className="eyebrow">Admin Preview</p>
<h1>Loading draft preview...</h1>
<p>Only signed-in admins can view this page.</p>
<h1 className="cp-title">Loading draft preview…</h1>
<p className="cp-text">Only signed-in admins can view this page.</p>
</div>
</main>
)
@@ -3725,22 +3720,33 @@ function PreviewPage() {
if (status === 'error' || !content) {
return (
<main className="thanks-page" aria-label="Draft preview unavailable">
<div className="thanks-card">
<main className="cp-main cp-main--bare" aria-label="Draft preview unavailable">
<div className="cp-card">
<p className="eyebrow">Admin Preview</p>
<h1>Draft preview unavailable</h1>
<p>Sign into admin and save a draft first.</p>
<Link to="/admin" className="btn-primary">Back to Admin</Link>
<h1 className="cp-title">Draft preview unavailable</h1>
<p className="cp-text">Sign into admin and save a draft first.</p>
<div className="cp-actions"><Link to="/admin" className="btn-primary">Back to Admin</Link></div>
</div>
</main>
)
}
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 === 'episode-highlights') {
return <EpisodesPage content={content} />
// Admin sends views like "about:bio" or "podcast:highlights"; match on the section prefix
// so every admin section previews the public page it edits.
const section = previewView.split(':')[0]
const view = previewView
if (section === 'start-here') return <StartHerePage content={content} />
if (section === 'about') return <AboutPage content={content} />
if (section === 'contact') return <ContactPage content={content} />
if (view === 'podcast:finished-books' || view === 'books') return <FinishedBooksPage content={content} />
if (section === 'podcast') return <EpisodesPage content={content} />
if (section === 'nuggets') return <NuggetsPage content={content} />
if (view === 'downloads' || view === 'recommended-resources' || view === 'custom-links') return <ResourcesPage content={content} />
if (view === 'colossians-study') return <StudyRouteFrame content={content} child={<StudyLandingPage content={content} />} />
if (view === 'questions') return <QuestionsPage content={content} />
if (view === 'newsletter-archive' || view === 'broadcast') return <NewsletterArchivePage content={content} />
if (view === 'legal') {
return <LegalPage content={content} title={content.legal?.privacyTitle ?? DEFAULTS.legal.privacyTitle} body={content.legal?.privacyBody ?? DEFAULTS.legal.privacyBody} />
}
return <LandingPage content={content} />
}
@@ -3896,10 +3902,10 @@ export default function App() {
<Route path="/questions" element={<QuestionsPage content={content} />} />
<Route path="/newsletter" element={<NewsletterArchivePage content={content} />} />
<Route path="/scripture" element={<ScriptureIndexPage content={content} />} />
<Route path="/thanks" element={<ThankYouPage />} />
<Route path="/subscribe" element={<SubscribePage />} />
<Route path="/subscribe/thanks" element={<SubscribeThankYouPage />} />
<Route path="/certificate/:token" element={<PublicCertificatePage />} />
<Route path="/thanks" element={<ThankYouPage content={content} />} />
<Route path="/subscribe" element={<SubscribePage content={content} />} />
<Route path="/subscribe/thanks" element={<SubscribeThankYouPage content={content} />} />
<Route path="/certificate/:token" element={<PublicCertificatePage content={content} />} />
<Route path="/host" element={<NuggetsHostPortal />} />
<Route path="/admin" element={<AdminShell content={content} onSave={setContent} />} />
<Route path="/preview" element={<PreviewPage />} />
@@ -3907,6 +3913,7 @@ export default function App() {
path="/privacy"
element={(
<LegalPage
content={content}
title={content.legal?.privacyTitle ?? DEFAULTS.legal.privacyTitle}
body={content.legal?.privacyBody ?? DEFAULTS.legal.privacyBody}
/>
@@ -3916,19 +3923,20 @@ export default function App() {
path="/terms"
element={(
<LegalPage
content={content}
title={content.legal?.termsTitle ?? DEFAULTS.legal.termsTitle}
body={content.legal?.termsBody ?? DEFAULTS.legal.termsBody}
/>
)}
/>
<Route path="*" element={<NotFoundPage />} />
<Route path="*" element={<NotFoundPage content={content} />} />
</Routes>
<BackToTopButton />
</>
)
}
function NotFoundPage() {
function NotFoundPage({ content }: { content: SiteContent }) {
const location = useLocation()
const [showForm, setShowForm] = useState(false)
const [form, setForm] = useState({
@@ -3968,24 +3976,19 @@ function NotFoundPage() {
}
return (
<main className="thanks-page" aria-label="Page not found">
<div className="thanks-card">
<img
src="/book_icon.png"
alt=""
aria-hidden="true"
style={{ width: '72px', marginBottom: '0.75rem', opacity: 0.55 }}
/>
<p className="eyebrow">404</p>
<h1>Page Not Found</h1>
<p>
The path <code style={{ fontSize: '0.95em', opacity: 0.85 }}>{location.pathname}</code>{' '}
doesn't exist — it may have moved or been removed.
<CenteredPageFrame content={content} label="Page not found">
<p className="cp-404" aria-hidden="true">404</p>
<h1 className="cp-title">Page not found</h1>
<p className="cp-text">
<code>{location.pathname}</code> doesn’t exist. It may have moved or been removed.
</p>
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center', flexWrap: 'wrap', marginBottom: '2rem' }}>
<Link to="/" className="btn-primary">← Back to Home</Link>
<Link to="/episodes" className="btn-secondary">Browse Episodes</Link>
<div className="cp-links">
<Link to="/episodes" className="cp-link-card"><strong>Episodes</strong><span>Listen to the podcast</span></Link>
<Link to="/study" className="cp-link-card"><strong>Bible studies</strong><span>Lessons and quizzes</span></Link>
<Link to="/questions" className="cp-link-card"><strong>Q&amp;A</strong><span>Questions and answers</span></Link>
<Link to="/resources" className="cp-link-card"><strong>Resources</strong><span>Downloads and guides</span></Link>
</div>
<div className="cp-actions"><Link to="/" className="btn-primary">← Back to home</Link></div>
<div style={{ borderTop: '1px solid rgba(201,168,76,0.15)', paddingTop: '1.5rem' }}>
{submitStatus !== 'sent' ? (
<>
@@ -4055,8 +4058,7 @@ function NotFoundPage() {
<p style={{ color: '#6fcf97' }}>Thanks! Nate will look into it.</p>
)}
</div>
</div>
</main>
</CenteredPageFrame>
)
}
+178 -147
View File
@@ -1501,8 +1501,9 @@ export function StudyResetPasswordPage() {
}, [token, newPassword, confirmPassword])
return (
<main className="thanks-page" aria-label="Reset password">
<div className="thanks-card study-signup-page-card">
<main className="auth-page" aria-label="Reset password">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">Free Student Account</p>
<h1>Reset Your Password</h1>
@@ -1549,6 +1550,7 @@ export function StudyResetPasswordPage() {
<Link to="/study/signup" className="btn-secondary">Back to Sign In</Link>
</div>
</div>
</div>
</main>
)
}
@@ -1574,18 +1576,15 @@ export function StudyVerifyEmailPage() {
}, [token])
return (
<main className="thanks-page" aria-label="Verify email">
<div className="thanks-card study-signup-page-card">
<main className="auth-page" aria-label="Verify email">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
{status === 'ok' && <span className="cp-icon" aria-hidden="true">✓</span>}
<p className="eyebrow">Free Student Account</p>
<h1>Verify Your Email</h1>
<h1>{status === 'ok' ? 'Email Verified' : status === 'error' ? 'Verification Problem' : 'Verifying Your Email'}</h1>
{status === 'checking' && <p style={{ color: '#b09e79' }}>Verifying…</p>}
{status === 'ok' && (
<>
<p className="study-signup-success">Your email address is verified.</p>
<p style={{ color: '#b09e79', marginBottom: '1.5rem', fontSize: '0.95rem' }}>Thanks for confirming — you're all set.</p>
</>
)}
{status === 'checking' && <p>Checking your link…</p>}
{status === 'ok' && <p className="study-signup-success">Your email address is verified. You’re all set.</p>}
{status === 'error' && <p className="study-signup-error">{message}</p>}
<div className="study-signup-actions" style={{ marginTop: '1.5rem' }}>
@@ -1593,6 +1592,7 @@ export function StudyVerifyEmailPage() {
<Link to="/study" className="btn-secondary">Back to Studies</Link>
</div>
</div>
</div>
</main>
)
}
@@ -1660,13 +1660,17 @@ export function ColossiansStudyIndexPage({ content }: Props) {
if (!study) {
return (
<main className="thanks-page" aria-label="Study not found">
<div className="thanks-card">
<main className="auth-page" aria-label="Study not found">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">Study Hub</p>
<h1>Study not found</h1>
<p>The study you requested is not available yet.</p>
<div className="study-signup-actions">
<Link to="/study" className="btn-primary">Back to Studies</Link>
</div>
</div>
</div>
</main>
)
}
@@ -1707,108 +1711,89 @@ export function ColossiansStudyIndexPage({ content }: Props) {
const studyGuide = getStudyGuideLink(content, study)
const pct = sections.length > 0 ? Math.round((completedCount / sections.length) * 100) : 0
return (
<main className="study-index-page" aria-label={`${study.title} study`}>
<section className="section-study-course-hero">
<div className="section-inner study-course-hero-inner">
<section className="si-hero">
<div className="section-inner si-hero-inner">
<div className="si-hero-main">
<Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Studies', href: '/study' }, { label: study.title }]} />
<p className="eyebrow">Online Bible Class</p>
<h1>{study.title}</h1>
<p className="study-course-hero-copy">{study.description}</p>
<div className="study-course-meta">
<h1 className="si-title">{study.title}</h1>
<p className="si-desc">{study.description}</p>
<div className="si-chips">
<span>{releasedSections.length} of {sections.length} lessons available</span>
<span>Text + commentary + discussion</span>
<span>Student notes enabled</span>
{isSelfPaced && <span>Finished study — go at your own pace</span>}
<span>Commentary + discussion</span>
<span>Personal notes</span>
{isSelfPaced && <span>Self-paced</span>}
</div>
{allLessonsDone && (
<p className="study-completed-banner" role="status">🎉 You've completed this study. Your lessons, notes and certificate are yours to keep.</p>
)}
{enrolled && !progressLoading && sections.length > 0 && (
<div className="study-progress" role="group" aria-label="Your progress">
<div className="study-progress-label">
<span>Your progress</span>
<strong>{completedCount} of {sections.length} lessons completed</strong>
</div>
<div className="study-progress-track" role="progressbar" aria-valuemin={0} aria-valuemax={sections.length} aria-valuenow={completedCount}>
<div className="study-progress-fill" style={{ width: `${Math.round((completedCount / sections.length) * 100)}%` }} />
</div>
</div>
)}
<div className="study-course-hero-actions">
{enrolled && resumeSection && <Link to={`/study/${study.slug}/${resumeSection.id}`} className="btn-primary">{allLessonsDone ? 'Review Lessons' : hasStarted ? 'Resume Where You Left Off' : 'Start Class'}</Link>}
{!auth.authenticated && <Link to="/study/signup" className="btn-primary">Sign In to Enroll</Link>}
<div className="si-actions">
{enrolled && resumeSection && <Link to={`/study/${study.slug}/${resumeSection.id}`} className="btn-primary">{allLessonsDone ? 'Review lessons' : hasStarted ? 'Resume where you left off' : 'Start class'}</Link>}
{!auth.authenticated && <Link to="/study/signup" className="btn-primary">Sign in to enroll</Link>}
{auth.authenticated && !enrolled && (
<button type="button" className="btn-primary" disabled={enrolling} onClick={enrollInStudy}>
{enrolling ? 'Enrolling...' : 'Enroll in This Study'}
{enrolling ? 'Enrolling…' : 'Enroll in this study'}
</button>
)}
{studyGuide && (studyGuide.downloadUrl
? <a href={studyGuide.downloadUrl} target="_blank" rel="noopener noreferrer" download className="btn-secondary">📄 Download Study Guide</a>
: <Link to={`/downloads/${studyGuide.pageId}`} className="btn-secondary">📄 Get the Study Guide</Link>)}
{allLessonsDone && <Link to="/study/account" className="btn-secondary">🏅 My Certificate</Link>}
{enrolled && <Link to={`/study/${study.slug}/notes`} className="btn-secondary">My Notes</Link>}
{enrolled && <Link to={`/study/${study.slug}/notes`} className="btn-secondary">My notes</Link>}
{enrolled && studyStatus !== 'planned' && <Link to={`/study/${study.slug}/community`} className="btn-secondary">Community</Link>}
{auth.authenticated && <Link to="/study/account" className="btn-secondary">My Account</Link>}
{studyGuide && (studyGuide.downloadUrl
? <a href={studyGuide.downloadUrl} target="_blank" rel="noopener noreferrer" download className="btn-secondary">Study guide</a>
: <Link to={`/downloads/${studyGuide.pageId}`} className="btn-secondary">Study guide</Link>)}
{allLessonsDone && <Link to="/study/account" className="btn-secondary">My certificate</Link>}
</div>
{enrollMessage && <p className="study-note-status" style={{ marginTop: '0.75rem' }}>{enrollMessage}</p>}
{enrollMessage && <p className="study-note-status" role="status">{enrollMessage}</p>}
</div>
{enrolled && !progressLoading && sections.length > 0 && (
<aside className="si-progress" aria-label="Your progress">
<div className="si-ring" style={{ ['--pct' as string]: `${pct}%` }}>
<span>{pct}%</span>
</div>
<div>
<p className="si-progress-big">{completedCount} of {sections.length}</p>
<p className="si-progress-sub">{allLessonsDone ? 'Study complete 🎉' : 'lessons completed'}</p>
</div>
</aside>
)}
</div>
</section>
{auth.checked && auth.authenticated && !enrolled && (
<section className="section-study-module" aria-label="Enrollment required">
<div className="section-inner">
<article className="study-module-row study-module-row--disabled" aria-disabled="true">
<div className="study-module-row-left">
<p className="study-module-lesson">Enrollment Required</p>
<h3>Enroll before entering lessons</h3>
<p>You must enroll in this study before opening lessons or notes.</p>
<div style={{ marginTop: '0.75rem' }}>
<section className="section-inner si-notice" aria-label="Enrollment required">
<strong>Enroll to open the lessons.</strong>
<span>Enrollment is free and lets you save notes and track progress.</span>
<button type="button" className="btn-primary" disabled={enrolling} onClick={enrollInStudy}>
{enrolling ? 'Enrolling...' : `Enroll in ${study.title}`}
{enrolling ? 'Enrolling…' : `Enroll in ${study.title}`}
</button>
</div>
</div>
</article>
</div>
</section>
)}
{sections.length === 0 && (
<section className="section-study-module">
<div className="section-inner">
<article className="study-module-row study-module-row--disabled" aria-disabled="true">
<div className="study-module-row-left">
<p className="study-module-lesson">Planned</p>
<h3>Lessons are being prepared</h3>
<p>Use the admin Studies editor to add section lessons and publish when ready.</p>
</div>
</article>
</div>
<section className="section-inner si-notice">
<strong>Lessons are being prepared.</strong>
<span>Check back soon.</span>
</section>
)}
{sections.length > 0 && (
<section className="section-study-module" aria-label="Course lessons">
<section className="si-lessons" aria-label="Course lessons">
<div className="section-inner">
<div className="study-module-header">
<p>Course Lessons</p>
<h2>{study.title}</h2>
</div>
{chaptersWithLessons.map(({ chapter }) => {
const chapterSections = sections.filter(item => item.chapter === chapter)
const chapterDone = chapterSections.filter(item => studyProgress.completedSectionIds.includes(item.id)).length
return (
<details className="study-chapter" key={chapter} open={chaptersWithLessons.length === 1 || chapter === resumeChapter}>
<summary className="study-chapter-head">
<h3>Chapter {chapter}</h3>
<details className="si-chapter" key={chapter} open={chaptersWithLessons.length === 1 || chapter === resumeChapter}>
<summary className="si-chapter-head">
<h2>Chapter {chapter}</h2>
<span>
{enrolled && !progressLoading
? `${chapterDone} of ${chapterSections.length} completed`
: `${chapterSections.length} ${chapterSections.length === 1 ? 'lesson' : 'lessons'}`}
</span>
</summary>
<div className="study-module-list">
<div className="si-list">
{chapterSections.map(section => {
const sectionIdx = sections.indexOf(section)
const lessonNumber = sectionIdx + 1
@@ -1819,34 +1804,30 @@ export function ColossiansStudyIndexPage({ content }: Props) {
const completed = studyProgress.completedSectionIds.includes(section.id)
const lockedByProgress = enrolled && !isSelfPaced && !coming && sectionIdx > 0 && !prevCompleted && !completed
const isDisabled = coming || !enrolled || lockedByProgress
const done = completed && enrolled
return (
<Link key={section.id} to={isDisabled ? '#' : `/study/${study.slug}/${section.id}`} className={`study-module-row${isDisabled ? ' study-module-row--disabled' : ''}${coming ? ' study-module-row--coming' : ''}${completed && enrolled ? ' study-module-row--done' : ''}`} aria-disabled={isDisabled || undefined} onClick={e => isDisabled && e.preventDefault()}>
<div className="study-module-row-left">
<div className="study-lesson-meta">
<p className="study-module-lesson">Lesson {lessonNumber}</p>
<Link
key={section.id}
to={isDisabled ? '#' : `/study/${study.slug}/${section.id}`}
className={`si-row${isDisabled ? ' si-row--disabled' : ''}${done ? ' si-row--done' : ''}`}
aria-disabled={isDisabled || undefined}
onClick={e => isDisabled && e.preventDefault()}
>
<span className="si-num" aria-hidden="true">{done ? '✓' : lessonNumber}</span>
<div className="si-row-body">
<div className="si-row-top">
<h3>{section.title}</h3>
{isNew && <span className="lesson-badge lesson-badge--new">New</span>}
{coming && <span className="lesson-badge lesson-badge--coming">Coming {getReleaseDateDisplay(section)}</span>}
{!coming && !enrolled && <span className="lesson-badge lesson-badge--enroll">Enroll to open</span>}
{lockedByProgress && <span className="lesson-badge lesson-badge--locked">Finish the previous lesson first</span>}
{enrolled && !lockedByProgress && completed && (
<span className="lesson-badge lesson-badge--done">
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true">
<circle cx="6.5" cy="6.5" r="6" stroke="currentColor" strokeWidth="1.2" />
<path d="M3.5 6.5l2 2 3.5-3.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Completed
</span>
)}
</div>
<h3>{section.title}</h3>
<p>{section.summary}</p>
</div>
<div className="study-module-row-right">
<p className="study-section-reference">{section.reference}</p>
<p className="study-module-focus">Focus question</p>
<p>{getPrimaryQuestion(section)}</p>
{section.reference && <p className="si-ref">{section.reference}</p>}
{section.summary && <p className="si-summary">{section.summary}</p>}
<p className="si-focus"><span>Focus:</span> {getPrimaryQuestion(section)}</p>
</div>
{!isDisabled && <span className="si-arrow" aria-hidden="true">→</span>}
</Link>
)
})}
@@ -2208,12 +2189,14 @@ export function ColossiansStudySectionPage({ content }: Props) {
if (!study || !section) {
return (
<main className="thanks-page" aria-label="Section not found">
<div className="thanks-card">
<main className="auth-page" aria-label="Section not found">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">Study Hub</p>
<h1>Section not found</h1>
<p>The section you requested is not available yet.</p>
<Link to="/study" className="btn-primary">Back to Studies</Link>
<div className="study-signup-actions"><Link to="/study" className="btn-primary">Back to Studies</Link></div>
</div>
</div>
</main>
)
@@ -2222,12 +2205,14 @@ export function ColossiansStudySectionPage({ content }: Props) {
if (!sectionIsReleased) {
const releaseDateDisplay = getReleaseDateDisplay(section)
return (
<main className="thanks-page" aria-label="Lesson not yet available">
<div className="thanks-card">
<main className="auth-page" aria-label="Lesson not yet available">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">{study.title}</p>
<h1>Lesson Coming Soon</h1>
<p>{releaseDateDisplay ? `This lesson will be available on ${releaseDateDisplay}.` : 'This lesson will be available soon.'}</p>
<Link to={`/study/${study.slug}`} className="btn-primary">Back to Lessons</Link>
<div className="study-signup-actions"><Link to={`/study/${study.slug}`} className="btn-primary">Back to Lessons</Link></div>
</div>
</div>
</main>
)
@@ -2235,11 +2220,13 @@ export function ColossiansStudySectionPage({ content }: Props) {
if (!auth.checked) {
return (
<main className="thanks-page" aria-label="Loading">
<div className="thanks-card">
<main className="auth-page" aria-label="Loading">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">{study.title}</p>
<p>Loading lesson...</p>
</div>
</div>
</main>
)
}
@@ -2320,17 +2307,24 @@ export function ColossiansStudySectionPage({ content }: Props) {
<section className="section-study-classroom" onContextMenu={handleLessonContentContextMenu}>
<div className="section-inner study-classroom-shell">
<div className="study-classroom-main">
<header className="sl-head">
<Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Studies', href: '/study' }, { label: study.title, href: `/study/${study.slug}` }, { label: `Lesson ${lessonNumber}` }]} />
<Link to={`/study/${study.slug}`} className="study-detail-back">Back to {study.title}</Link>
<p className="study-lesson-label">Lesson {lessonNumber} of {sections.length}</p>
<div className="sl-head-meta">
<span className="sl-lesson-pill">Lesson {lessonNumber} of {sections.length}</span>
{section.reference && <span className="sl-ref">{section.reference}</span>}
{lessonCompleted && <span className="sl-done">✓ Completed</span>}
</div>
<h1 className="sl-title">{section.title}</h1>
{section.summary && <p className="sl-summary">{section.summary}</p>}
{sections.length > 0 && (
<div className="study-lesson-progress-bar" role="progressbar" aria-valuenow={completedSectionIds.length} aria-valuemax={sections.length} aria-label={`${completedSectionIds.length} of ${sections.length} lessons completed`}>
<div className="sl-progress" aria-label={`${completedSectionIds.length} of ${sections.length} lessons completed`}>
<div className="study-lesson-progress-bar" role="progressbar" aria-valuenow={completedSectionIds.length} aria-valuemax={sections.length}>
<div className="study-lesson-progress-fill" style={{ width: `${Math.round((completedSectionIds.length / sections.length) * 100)}%` }} />
</div>
<span>{completedSectionIds.length} of {sections.length} lessons completed in {study.title}</span>
</div>
)}
<h1>{section.title}</h1>
<p className="study-detail-reference">{section.reference}</p>
<p className="study-detail-summary">{section.summary}</p>
</header>
{lessonAudioEmbedUrl && (
<article className="study-class-block">
@@ -2544,6 +2538,31 @@ export function ColossiansStudySectionPage({ content }: Props) {
</p>
<StudySectionComments studySlug={currentStudySlug} sectionId={section.id} isEnrolled={isEnrolled} />
</article>}
<nav className="sl-pager" aria-label="Lesson navigation">
{previousSection ? (
<Link to={`/study/${study.slug}/${previousSection.id}`} className="sl-pager-link">
<span>← Previous lesson</span>
<strong>{previousSection.title}</strong>
</Link>
) : <span />}
{nextSection && isSectionReleased(nextSection) && lessonCompleted ? (
<Link to={`/study/${study.slug}/${nextSection.id}`} className="sl-pager-link sl-pager-link--next">
<span>Next lesson →</span>
<strong>{nextSection.title}</strong>
</Link>
) : nextSection ? (
<div className="sl-pager-link sl-pager-link--next sl-pager-link--wait">
<span>Next lesson</span>
<strong>{!isSectionReleased(nextSection) ? `Available ${getReleaseDateDisplay(nextSection)}` : 'Complete this lesson to continue'}</strong>
</div>
) : (
<Link to={`/study/${study.slug}`} className="sl-pager-link sl-pager-link--next">
<span>Finished</span>
<strong>Back to all lessons →</strong>
</Link>
)}
</nav>
</div>
<aside className="study-classroom-sidebar" aria-label="Lesson tools">
@@ -2760,12 +2779,14 @@ export function ColossiansStudyNotesPage({ content }: Props) {
if (!study) {
return (
<main className="thanks-page" aria-label="Study not found">
<div className="thanks-card">
<main className="auth-page" aria-label="Study not found">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">Study Hub</p>
<h1>Study not found</h1>
<p>The study you requested is not available yet.</p>
<Link to="/study" className="btn-primary">Back to Studies</Link>
<div className="study-signup-actions"><Link to="/study" className="btn-primary">Back to Studies</Link></div>
</div>
</div>
</main>
)
@@ -4133,12 +4154,14 @@ export function StudyQuizPage({ content }: Props) {
if (!study || !section) {
return (
<main className="thanks-page" aria-label="Discussion questions not found">
<div className="thanks-card">
<main className="auth-page" aria-label="Discussion questions not found">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">Reflection Questions</p>
<h1>Discussion questions not found</h1>
<p>The reflection questions you requested are not available.</p>
<Link to="/study" className="btn-primary">Back to Studies</Link>
<div className="study-signup-actions"><Link to="/study" className="btn-primary">Back to Studies</Link></div>
</div>
</div>
</main>
)
@@ -4150,20 +4173,24 @@ export function StudyQuizPage({ content }: Props) {
if (!auth.checked) {
return (
<main className="thanks-page" aria-label="Loading reflection questions">
<div className="thanks-card"><p>Loading reflection questions...</p></div>
<main className="auth-page" aria-label="Loading reflection questions">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center"><p>Loading reflection questions...</p></div>
</div>
</main>
)
}
if (!auth.authenticated) {
return (
<main className="thanks-page" aria-label="Sign in required">
<div className="thanks-card">
<main className="auth-page" aria-label="Sign in required">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">Reflection Questions</p>
<h1>Sign In Required</h1>
<p>You need to sign in before you can view and save your discussion answers.</p>
<Link to="/study/signup" className="btn-primary">Sign In</Link>
<div className="study-signup-actions"><Link to="/study/signup" className="btn-primary">Sign In</Link></div>
</div>
</div>
</main>
)
@@ -4171,12 +4198,14 @@ export function StudyQuizPage({ content }: Props) {
if (!isEnrolled) {
return (
<main className="thanks-page" aria-label="Enrollment required">
<div className="thanks-card">
<main className="auth-page" aria-label="Enrollment required">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">Reflection Questions</p>
<h1>Enroll to Access Reflection Questions</h1>
<p>Please enroll in {study.title} to open the reflection questions for this lesson.</p>
<Link to={`/study/${study.slug}`} className="btn-primary">Go to Study</Link>
<div className="study-signup-actions"><Link to={`/study/${study.slug}`} className="btn-primary">Go to Study</Link></div>
</div>
</div>
</main>
)
@@ -4312,20 +4341,24 @@ export function StudyDiscussionHubPage({ content }: Props) {
if (!auth.checked) {
return (
<main className="thanks-page" aria-label="Loading">
<div className="thanks-card"><p>Loading...</p></div>
<main className="auth-page" aria-label="Loading">
<div className="auth-shell auth-shell--single"><div className="auth-card auth-card--center"><p>Loading…</p></div></div>
</main>
)
}
if (!auth.authenticated) {
return (
<main className="thanks-page" aria-label="Sign in required">
<div className="thanks-card">
<main className="auth-page" aria-label="Sign in required">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">Study Discussion</p>
<h1>Sign In Required</h1>
<p>Create a free student account to see and join the conversation with other students.</p>
<Link to="/study/signup" className="btn-primary" style={{ marginTop: '1rem', display: 'inline-block' }}>Sign In</Link>
<div className="study-signup-actions">
<Link to="/study/signup" className="btn-primary">Create Account or Sign In</Link>
</div>
</div>
</div>
</main>
)
@@ -4344,22 +4377,16 @@ export function StudyDiscussionHubPage({ content }: Props) {
{openDiscussionStudies.length === 0 ? (
<p className="study-detail-copy">There are no studies open for discussion yet. Check back when the next one starts.</p>
) : (
<div className="study-module-list">
<div className="sdh-grid">
{openDiscussionStudies.map(study => {
const enrolled = isEnrolledInStudy(auth, study.slug)
return (
<article key={study.slug} className="study-module-row">
<div className="study-module-row-left">
<p className="study-module-lesson">{enrolled ? 'Enrolled' : 'Study track'}</p>
<Link key={study.slug} to={`/study/${study.slug}/community`} className="sdh-card">
<span className={`sdh-badge${enrolled ? ' sdh-badge--on' : ''}`}>{enrolled ? 'Enrolled' : 'Open to all students'}</span>
<h3>{study.title}</h3>
<p>{study.description || 'Join the discussion for this study track.'}</p>
</div>
<div className="study-module-row-right" style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
<Link to={`/study/${study.slug}/community`} className="btn-primary">
{enrolled ? 'Open Discussion' : 'View Discussion'}
<span className="sdh-go">{enrolled ? 'Open discussion →' : 'View discussion →'}</span>
</Link>
</div>
</article>
)
})}
</div>
@@ -4389,20 +4416,24 @@ export function StudyCommunityPage({ content }: Props) {
if (!auth.checked) {
return (
<main className="thanks-page" aria-label="Loading">
<div className="thanks-card"><p>Loading...</p></div>
<main className="auth-page" aria-label="Loading">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center"><p>Loading...</p></div>
</div>
</main>
)
}
if (!auth.authenticated) {
return (
<main className="thanks-page" aria-label="Sign in required">
<div className="thanks-card">
<main className="auth-page" aria-label="Sign in required">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">Study Discussion</p>
<h1>Sign In Required</h1>
<p>Create a free student account to see and join the conversation.</p>
<Link to="/study/signup" className="btn-primary" style={{ marginTop: '1rem', display: 'inline-block' }}>Sign In</Link>
<div className="study-signup-actions"><Link to="/study/signup" className="btn-primary">Sign In</Link></div>
</div>
</div>
</main>
)