diff --git a/package.json b/package.json index 73c0702..90ae037 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.37.0", + "version": "2.38.0", "type": "module", "scripts": { "dev": "vite", diff --git a/server.js b/server.js index aa17349..f28862f 100644 --- a/server.js +++ b/server.js @@ -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('; '), ) diff --git a/server/routes/newsletter.js b/server/routes/newsletter.js index 21a0173..b34feb7 100644 --- a/server/routes/newsletter.js +++ b/server/routes/newsletter.js @@ -88,15 +88,22 @@ table[align="left"] { float: none !important; margin-left: auto !important; marg ` + const topBar = ` +
+
+Verse by Verse with Nate +← All issues +
` + const ctaBlock = ` -
+
-
-

-← Newsletter Archive -

-Subscribe by Email +
+

Stay in the Word

+

Get the next issue in your inbox

+Subscribe by Email +

← Back to the newsletter archive

` @@ -104,6 +111,7 @@ table[align="left"] { float: none !important; margin-left: auto !important; marg let html = stripUnsubscribeLink(issue.html) html = html.replace(/[\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 } diff --git a/src/App.css b/src/App.css index f064940..7147eaa 100644 --- a/src/App.css +++ b/src/App.css @@ -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; } diff --git a/src/App.tsx b/src/App.tsx index 8d7d75f..925797a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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"> - <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> + <CenteredPageFrame content={content} label="Download not found"> + <p className="cp-404" aria-hidden="true">?</p> + <p className="eyebrow">Resources</p> + <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> - )} - - {(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> - </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> - </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> - ))} - </ol> - </div> + <a href={episode.url} target="_blank" rel="noreferrer" className="btn-secondary ed-listen">Listen on your podcast app →</a> )} </div> </section> + + {(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> + )} + {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> + )} + {questions.length > 0 && ( + <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"> - {host.bio && host.bio.split(/\n\n+/).map((para, i) => ( - <p key={i} className="nuggets-host-detail-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="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> + </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="hd-bio">{para.replace(/\n/g, ' ')}</p> + ))} + {(nuggets.spotifyUrl || nuggets.appleUrl) && ( + <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 style={{ marginTop: '2rem' }}> - <Link to="/nuggets" className="nuggets-host-detail-back">← Back to {nuggets.title}</Link> </div> - </div> + )} + {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,97 +3158,74 @@ function SubscribePage() { } return ( - <main className="thanks-page" aria-label="Subscribe"> - <div className="thanks-card subscribe-card"> - <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> - <input - type="text" - className="contact-honeypot" - tabIndex={-1} - autoComplete="off" - aria-hidden="true" - value={honey} - onChange={e => setHoney(e.target.value)} - /> + <CenteredPageFrame content={content} label="Subscribe"> + <p className="eyebrow">Verse by Verse with Nate</p> + <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" + tabIndex={-1} + autoComplete="off" + aria-hidden="true" + 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> - <label> - Email - <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>} - - <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> + </div> + <label> + Email + <input type="email" name="email" required autoComplete="email" value={email} onChange={e => setEmail(e.target.value)} /> + </label> + {status === 'error' && <p className="contact-error" role="alert">{errorMsg}</p>} + <button type="submit" className="btn-primary" disabled={status === 'submitting'}> + {status === 'submitting' ? 'Subscribing…' : 'Subscribe'} + </button> + </form> + <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"> - <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> + <CenteredPageFrame content={content} label="Subscription confirmed"> + <span className="cp-icon" aria-hidden="true">✓</span> + <p className="eyebrow">Verse by Verse with Nate</p> + <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> + </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&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> ) } diff --git a/src/studyPages.tsx b/src/studyPages.tsx index c8e27ff..07daf5f 100644 --- a/src/studyPages.tsx +++ b/src/studyPages.tsx @@ -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,12 +1660,16 @@ export function ColossiansStudyIndexPage({ content }: Props) { if (!study) { return ( - <main className="thanks-page" aria-label="Study not found"> - <div className="thanks-card"> - <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> + <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"> - <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"> - <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>} - </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> + <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 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>Commentary + discussion</span> + <span>Personal notes</span> + {isSelfPaced && <span>Self-paced</span>} </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>} - {auth.authenticated && !enrolled && ( - <button type="button" className="btn-primary" disabled={enrolling} onClick={enrollInStudy}> - {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 && 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>} + <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'} + </button> + )} + {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>} + {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" role="status">{enrollMessage}</p>} </div> - {enrollMessage && <p className="study-note-status" style={{ marginTop: '0.75rem' }}>{enrollMessage}</p>} + + {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' }}> - <button type="button" className="btn-primary" disabled={enrolling} onClick={enrollInStudy}> - {enrolling ? 'Enrolling...' : `Enroll in ${study.title}`} - </button> - </div> - </div> - </article> - </div> + <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}`} + </button> </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"> - <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> - {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="study-lesson-progress-fill" style={{ width: `${Math.round((completedSectionIds.length / sections.length) * 100)}%` }} /> + <header className="sl-head"> + <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Studies', href: '/study' }, { label: study.title, href: `/study/${study.slug}` }, { label: `Lesson ${lessonNumber}` }]} /> + <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>{section.title}</h1> - <p className="study-detail-reference">{section.reference}</p> - <p className="study-detail-summary">{section.summary}</p> + <h1 className="sl-title">{section.title}</h1> + {section.summary && <p className="sl-summary">{section.summary}</p>} + {sections.length > 0 && ( + <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> + )} + </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"> - <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> + <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> + <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> - <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'} - </Link> - </div> - </article> + <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> + <span className="sdh-go">{enrolled ? 'Open discussion →' : 'View discussion →'}</span> + </Link> ) })} </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> )