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
Publish Container / build (push) Successful in 15m34s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5d5c026762
commit
ea463b5851
6 files changed
+664
-452
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "siteforge",
|
||||
"private": true,
|
||||
"version": "2.37.0",
|
||||
"version": "2.38.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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('; '),
|
||||
)
|
||||
|
||||
@@ -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">← 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">← 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">← 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
@@ -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; }
|
||||
+275
-273
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+202
-171
@@ -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>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user