Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c342f14bd | ||
|
|
3cf7e02561 | ||
|
|
d06fcba60b |
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "2.25.2",
|
"version": "2.28.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "2.25.2",
|
"version": "2.28.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.1135.0",
|
"@aws-sdk/client-s3": "^3.1135.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.25.2",
|
"version": "2.28.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+324
-454
@@ -978,124 +978,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── About ── */
|
/* ── About ── */
|
||||||
.section-about {
|
|
||||||
background: #0d0d0d;
|
|
||||||
padding: 5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-about .section-inner {
|
|
||||||
max-width: 1160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-inner {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(340px, 420px) 1fr;
|
|
||||||
gap: 4.5rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-photo {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-photo img {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 14px;
|
|
||||||
box-shadow:
|
|
||||||
0 12px 50px rgba(0, 0, 0, 0.55),
|
|
||||||
0 0 0 1px rgba(201, 168, 76, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-photo-divider {
|
|
||||||
width: 86%;
|
|
||||||
height: 1px;
|
|
||||||
margin: 0.3rem auto 0.45rem;
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(201, 168, 76, 0),
|
|
||||||
rgba(201, 168, 76, 0.6) 20%,
|
|
||||||
rgba(201, 168, 76, 0.9) 50%,
|
|
||||||
rgba(201, 168, 76, 0.6) 80%,
|
|
||||||
rgba(201, 168, 76, 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-nate-copy {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-nate-copy .eyebrow {
|
|
||||||
margin-bottom: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-nate-rule {
|
|
||||||
width: 170px;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg, var(--brand-gold), rgba(201, 168, 76, 0.18));
|
|
||||||
margin-bottom: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-nate-copy p {
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: var(--brand-warm-white);
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
padding: 0;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-nate-cta {
|
|
||||||
display: inline-block;
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--brand-gold);
|
|
||||||
text-decoration: none;
|
|
||||||
margin-top: 0.35rem;
|
|
||||||
padding-bottom: 0.1rem;
|
|
||||||
border-bottom: 1px solid rgba(201, 168, 76, 0.45);
|
|
||||||
transition: color 180ms ease, border-color 180ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-nate-cta:hover {
|
|
||||||
color: var(--brand-warm-white);
|
|
||||||
border-bottom-color: rgba(246, 232, 186, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-verse-art {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 1.1rem;
|
|
||||||
border-radius: 14px;
|
|
||||||
box-shadow:
|
|
||||||
0 12px 50px rgba(0, 0, 0, 0.55),
|
|
||||||
0 0 0 1px rgba(201, 168, 76, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-text h2 {
|
|
||||||
font-family: var(--brand-font-heading);
|
|
||||||
font-size: clamp(1.8rem, 3vw, 2.4rem);
|
|
||||||
color: var(--brand-warm-white);
|
|
||||||
margin: 0.5rem 0 1.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-text p {
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: var(--brand-warm-white);
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
padding: 0;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ── Homepage Newsletter ── */
|
/* ── Homepage Newsletter ── */
|
||||||
.section-newsletter {
|
.section-newsletter {
|
||||||
@@ -3827,138 +3738,41 @@
|
|||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-contact {
|
|
||||||
background: #090909;
|
|
||||||
border-top: 1px solid rgba(201, 168, 76, 0.18);
|
|
||||||
border-bottom: 1px solid rgba(201, 168, 76, 0.18);
|
|
||||||
padding: 5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-inner {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 2.5rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-card {
|
|
||||||
background: #111111;
|
|
||||||
border: 1px solid rgba(201, 168, 76, 0.2);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2.3rem;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-card .eyebrow {
|
|
||||||
margin: 0 0 0.45rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-card h2 {
|
|
||||||
margin: 0 0 1.75rem;
|
|
||||||
font-family: var(--brand-font-heading);
|
|
||||||
color: var(--brand-warm-white);
|
|
||||||
font-size: clamp(1.45rem, 2vw, 1.9rem);
|
|
||||||
line-height: 1.3;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-profile {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-profile-photo {
|
|
||||||
width: 108px;
|
|
||||||
height: 108px;
|
|
||||||
border-radius: 999px;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center 22%;
|
|
||||||
border: 2px solid var(--brand-gold);
|
|
||||||
flex-shrink: 0;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 4px rgba(201, 168, 76, 0.16),
|
|
||||||
0 10px 28px rgba(0, 0, 0, 0.42);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-profile-name {
|
|
||||||
margin: 0 0 0.15rem;
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-size: 1.45rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--brand-warm-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-profile-role {
|
|
||||||
margin: 0;
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--brand-gold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-quote {
|
|
||||||
border-left: 2px solid var(--brand-gold);
|
|
||||||
padding-left: 1rem;
|
|
||||||
margin: 0 0 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-quote p {
|
|
||||||
margin: 0;
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-size: 1.35rem;
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #d4c4a0;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-intro {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #9a9994;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-points {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-point {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.65rem;
|
|
||||||
color: #8f8e89;
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-size: 0.92rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-point svg {
|
|
||||||
color: var(--brand-gold);
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-scripture {
|
|
||||||
margin-top: 1.75rem;
|
|
||||||
padding-top: 1.5rem;
|
|
||||||
border-top: 1px solid #2c2116;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-scripture p {
|
|
||||||
margin: 0;
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-size: 0.88rem;
|
|
||||||
font-style: italic;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #a09888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-scripture-ref {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.contact-form {
|
.contact-form {
|
||||||
background: #121212;
|
background: #121212;
|
||||||
@@ -5768,11 +5582,7 @@ a.study-track-card:hover {
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-download-library {
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at top center, rgba(201, 168, 76, 0.1), transparent 26%),
|
|
||||||
var(--brand-black);
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-library-head {
|
.download-library-head {
|
||||||
max-width: 760px;
|
max-width: 760px;
|
||||||
@@ -5788,74 +5598,21 @@ a.study-track-card:hover {
|
|||||||
margin: 0.85rem auto 0;
|
margin: 0.85rem auto 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-tag-filter {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 auto 2rem;
|
|
||||||
max-width: 760px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-tag-chip {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid rgba(201, 168, 76, 0.3);
|
|
||||||
border-radius: 999px;
|
|
||||||
color: rgba(255, 255, 255, 0.65);
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-size: 0.88rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.03em;
|
|
||||||
padding: 0.35rem 1rem;
|
|
||||||
transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-tag-chip:hover {
|
|
||||||
background: rgba(201, 168, 76, 0.1);
|
|
||||||
border-color: rgba(201, 168, 76, 0.55);
|
|
||||||
color: var(--brand-warm-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-tag-chip--active {
|
|
||||||
background: rgba(201, 168, 76, 0.15);
|
|
||||||
border-color: var(--brand-gold);
|
|
||||||
color: var(--brand-gold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-clear-link {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-size: 0.82rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--brand-muted);
|
|
||||||
padding: 0.35rem 0.5rem;
|
|
||||||
transition: color 140ms ease;
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-clear-link:hover {
|
|
||||||
color: var(--brand-warm-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-empty-state {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--brand-muted);
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
padding: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-filter-bar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Related episodes on download detail pages */
|
/* Related episodes on download detail pages */
|
||||||
.download-related-episodes {
|
.download-related-episodes {
|
||||||
@@ -5987,118 +5744,36 @@ a.study-track-card:hover {
|
|||||||
padding: 4rem 0;
|
padding: 4rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.books-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
||||||
gap: 2rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-card {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.25rem;
|
|
||||||
background: rgba(255,255,255,0.03);
|
|
||||||
border: 1px solid rgba(201, 168, 76, 0.1);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-card-cover {
|
|
||||||
width: 80px;
|
|
||||||
height: 110px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-card-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-card-title {
|
|
||||||
font-family: var(--brand-font-heading);
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--brand-warm-white);
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-card-description {
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 300;
|
|
||||||
color: var(--brand-muted);
|
|
||||||
line-height: 1.6;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-card-btn {
|
|
||||||
align-self: flex-start;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
padding: 0.4rem 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ── Recommended Resources section ── */
|
/* ── Recommended Resources section ── */
|
||||||
.recommended-resources-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
||||||
gap: 1.25rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommended-resource-card {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.25rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
text-decoration: none;
|
|
||||||
background: rgba(255,255,255,0.03);
|
|
||||||
border: 1px solid rgba(201, 168, 76, 0.1);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.25rem;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommended-resource-card:hover {
|
|
||||||
border-color: rgba(201, 168, 76, 0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommended-resource-image {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 6px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommended-resource-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommended-resource-title {
|
|
||||||
font-family: var(--brand-font-heading);
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--brand-warm-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommended-resource-description {
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 300;
|
|
||||||
color: var(--brand-muted);
|
|
||||||
line-height: 1.6;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommended-resource-action {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--brand-gold);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ── Interior page banner ── */
|
/* ── Interior page banner ── */
|
||||||
.section-page-banner {
|
.section-page-banner {
|
||||||
@@ -6132,18 +5807,9 @@ a.study-track-card:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Featured download card ── */
|
/* ── Featured download card ── */
|
||||||
.resource-download-card--featured {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
border-color: rgba(201, 168, 76, 0.35);
|
|
||||||
background: rgba(201, 168, 76, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-link-image--featured {
|
|
||||||
width: 80px !important;
|
|
||||||
height: 110px !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-badge {
|
.featured-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -6161,37 +5827,13 @@ a.study-track-card:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Books + Recommended Resources tabs ── */
|
/* ── Books + Recommended Resources tabs ── */
|
||||||
.books-resources-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.25rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
border-bottom: 1px solid rgba(201, 168, 76, 0.15);
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.books-resources-tab {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
padding: 0.6rem 1.1rem;
|
|
||||||
margin-bottom: -1px;
|
|
||||||
font-family: var(--brand-font-body);
|
|
||||||
font-size: 0.92rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 160ms ease, border-color 160ms ease;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.books-resources-tab:hover {
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.books-resources-tab--active {
|
|
||||||
color: var(--brand-gold);
|
|
||||||
border-bottom-color: var(--brand-gold);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Study Guide section ── */
|
/* ── Study Guide section ── */
|
||||||
.section-guide {
|
.section-guide {
|
||||||
@@ -7936,15 +7578,9 @@ a.study-track-card:hover {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-inner {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-photo {
|
|
||||||
max-width: 260px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.series-card {
|
.series-card {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -8000,19 +7636,11 @@ a.study-track-card:hover {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-inner {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-card {
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 1.45rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-quote p {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-logo-img {
|
.header-logo-img {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -12973,14 +12601,14 @@ a.study-track-card:hover {
|
|||||||
.em-webhook-add-title { font-size: 0.85rem; font-weight: 600; color: #c0bcd4; margin: 0 0 0.75rem; }
|
.em-webhook-add-title { font-size: 0.85rem; font-weight: 600; color: #c0bcd4; margin: 0 0 0.75rem; }
|
||||||
|
|
||||||
/* ── Ko-fi support section ───────────────────────────────────────────────── */
|
/* ── Ko-fi support section ───────────────────────────────────────────────── */
|
||||||
.section-kofi-support { background: var(--brand-black-2, #0f0f0c); padding: 4.5rem 1.5rem; text-align: center; border-top: 1px solid var(--brand-border, #2a2518); border-bottom: 1px solid var(--brand-border, #2a2518); }
|
|
||||||
.kofi-support-inner { max-width: 520px; margin: 0 auto; display: flex; flex-direction: column; align-items: center; gap: 1.25rem; }
|
|
||||||
.kofi-support-inner .eyebrow { color: var(--brand-gold, #c9a84c); }
|
|
||||||
.kofi-support-inner h2 { font-family: var(--brand-font-heading, 'Cormorant Garamond', serif); font-size: 2.2rem; font-weight: 600; color: var(--brand-warm-white, #f0ead8); margin: 0; }
|
|
||||||
.kofi-support-desc { font-size: 1rem; line-height: 1.75; color: var(--brand-muted, #b0a48c); max-width: 460px; margin: 0; }
|
|
||||||
.kofi-qr-wrap { display: flex; flex-direction: column; align-items: center; gap: 0.6rem; margin: 0.5rem 0; }
|
|
||||||
.kofi-qr-img { width: 180px; height: 180px; border-radius: 8px; border: 1px solid var(--brand-border, #2a2518); }
|
|
||||||
.kofi-qr-caption { font-size: 0.78rem; color: var(--brand-muted, #b0a48c); letter-spacing: 0.03em; }
|
|
||||||
.kofi-btn { display: inline-block; background: transparent; color: var(--brand-gold, #c9a84c); border: 1px solid var(--brand-gold, #c9a84c); font-size: 0.9rem; font-weight: 600; padding: 0.7rem 1.75rem; border-radius: 4px; text-decoration: none; letter-spacing: 0.04em; transition: background 0.2s, color 0.2s; }
|
.kofi-btn { display: inline-block; background: transparent; color: var(--brand-gold, #c9a84c); border: 1px solid var(--brand-gold, #c9a84c); font-size: 0.9rem; font-weight: 600; padding: 0.7rem 1.75rem; border-radius: 4px; text-decoration: none; letter-spacing: 0.04em; transition: background 0.2s, color 0.2s; }
|
||||||
.kofi-btn:hover { background: var(--brand-gold, #c9a84c); color: var(--brand-black, #0a0a08); }
|
.kofi-btn:hover { background: var(--brand-gold, #c9a84c); color: var(--brand-black, #0a0a08); }
|
||||||
|
|
||||||
@@ -13777,9 +13405,7 @@ a.study-track-card:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* About copy was set larger than body text everywhere else */
|
/* About copy was set larger than body text everywhere else */
|
||||||
.about-inner p {
|
|
||||||
font-size: 1.0625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nothing readable below 12px */
|
/* Nothing readable below 12px */
|
||||||
.study-module-lesson,
|
.study-module-lesson,
|
||||||
@@ -13830,7 +13456,251 @@ a.study-track-card:hover {
|
|||||||
font-size: clamp(1.4rem, 2.4vw, 1.625rem);
|
font-size: clamp(1.4rem, 2.4vw, 1.625rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-inner p {
|
|
||||||
font-size: 1.0625rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Resources page (tabs + compact grids) ─────────────────────────────── */
|
||||||
|
.res-hub { padding: 2rem 0 3.5rem; }
|
||||||
|
.res-tabs { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
|
||||||
|
.res-tab {
|
||||||
|
font: inherit; font-size: 0.95rem; cursor: pointer; padding: 0.5rem 1.1rem; border-radius: 999px;
|
||||||
|
color: var(--brand-muted); background: transparent; border: 1px solid var(--brand-border);
|
||||||
|
transition: border-color 150ms, color 150ms, background 150ms;
|
||||||
|
}
|
||||||
|
.res-tab span { opacity: 0.6; margin-left: 0.25rem; font-size: 0.85em; }
|
||||||
|
.res-tab:hover { border-color: var(--brand-gold-dark); color: var(--brand-warm-white); }
|
||||||
|
.res-tab--on, .res-tab--on:hover { background: var(--brand-gold); border-color: var(--brand-gold); color: var(--brand-black); }
|
||||||
|
|
||||||
|
.res-featured { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; margin-bottom: 1.75rem; }
|
||||||
|
.res-feature {
|
||||||
|
display: flex; gap: 1rem; align-items: center; padding: 1rem; text-decoration: none; color: inherit;
|
||||||
|
background: linear-gradient(135deg, rgba(201, 168, 76, 0.12), rgba(201, 168, 76, 0.03));
|
||||||
|
border: 1px solid var(--brand-gold-dark); border-radius: 14px; transition: transform 150ms, border-color 150ms;
|
||||||
|
}
|
||||||
|
.res-feature:hover { transform: translateY(-2px); border-color: var(--brand-gold); }
|
||||||
|
.res-feature-img { width: 84px; height: 84px; flex-shrink: 0; object-fit: cover; border-radius: 10px; }
|
||||||
|
.res-feature-body { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; }
|
||||||
|
.res-badge {
|
||||||
|
align-self: flex-start; font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 700;
|
||||||
|
color: var(--brand-black); background: var(--brand-gold); padding: 0.1rem 0.55rem; border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.res-tags { display: flex; gap: 0.45rem; flex-wrap: wrap; margin-bottom: 1.25rem; }
|
||||||
|
.res-chip {
|
||||||
|
font: inherit; font-size: 0.82rem; cursor: pointer; padding: 0.25rem 0.8rem; border-radius: 999px;
|
||||||
|
color: var(--brand-muted); background: transparent; border: 1px solid var(--brand-border);
|
||||||
|
}
|
||||||
|
.res-chip:hover { border-color: var(--brand-gold-dark); color: var(--brand-warm-white); }
|
||||||
|
.res-chip--on { background: rgba(201, 168, 76, 0.18); border-color: var(--brand-gold-dark); color: var(--brand-gold-light); }
|
||||||
|
.res-empty { color: var(--brand-muted); padding: 1rem 0; }
|
||||||
|
|
||||||
|
.res-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; }
|
||||||
|
.res-card, .res-book {
|
||||||
|
display: flex; gap: 0.9rem; align-items: flex-start; padding: 0.95rem; text-decoration: none; color: inherit;
|
||||||
|
background: var(--brand-black-3); border: 1px solid var(--brand-border); border-radius: 12px;
|
||||||
|
transition: border-color 150ms, transform 150ms;
|
||||||
|
}
|
||||||
|
.res-card:hover { border-color: var(--brand-gold-dark); transform: translateY(-2px); }
|
||||||
|
.res-card-img { width: 64px; height: 64px; flex-shrink: 0; object-fit: cover; border-radius: 8px; }
|
||||||
|
.res-book-cover { width: 76px; flex-shrink: 0; aspect-ratio: 2 / 3; object-fit: cover; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); }
|
||||||
|
.res-card-body, .res-book-body { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; flex: 1; }
|
||||||
|
|
||||||
|
.res-title { font-family: var(--brand-font-heading); font-size: 1.25rem; font-weight: 600; line-height: 1.25; color: var(--brand-warm-white); overflow-wrap: anywhere; }
|
||||||
|
.res-desc { margin: 0; font-size: 0.92rem; line-height: 1.55; color: var(--brand-muted); }
|
||||||
|
.res-clamped { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
|
.res-card-tags { font-size: 0.75rem; color: var(--brand-gold-dark); letter-spacing: 0.03em; }
|
||||||
|
.res-action { margin-top: 0.15rem; font-size: 0.85rem; color: var(--brand-gold); }
|
||||||
|
.res-more { align-self: flex-start; font: inherit; font-size: 0.82rem; padding: 0; background: none; border: 0; cursor: pointer; color: var(--brand-gold); text-decoration: underline; }
|
||||||
|
.res-book-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.4rem; }
|
||||||
|
.res-btn { font-size: 0.78rem !important; padding: 0.45rem 0.95rem !important; }
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.res-hub { padding: 1.25rem 0 2.5rem; }
|
||||||
|
.res-tabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; margin-left: -0.25rem; padding: 0 0.25rem 0.25rem; }
|
||||||
|
.res-tab { flex-shrink: 0; }
|
||||||
|
.res-grid { grid-template-columns: 1fr; gap: 0.75rem; }
|
||||||
|
.res-feature { align-items: flex-start; }
|
||||||
|
.res-feature-img { width: 64px; height: 64px; }
|
||||||
|
.res-title { font-size: 1.1rem; }
|
||||||
|
.res-card, .res-book { padding: 0.8rem; gap: 0.75rem; }
|
||||||
|
.res-card-img { width: 52px; height: 52px; }
|
||||||
|
.res-book-cover { width: 64px; }
|
||||||
|
/* Shorter previews on phones; the tag chips above already cover tags */
|
||||||
|
.res-card .res-clamped, .res-feature .res-clamped { -webkit-line-clamp: 2 !important; }
|
||||||
|
.res-card-tags { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Download page ─────────────────────────────────────────────────────── */
|
||||||
|
.dl-page { padding: 1.5rem 0 3.5rem; }
|
||||||
|
.dl-back { display: inline-block; margin-bottom: 1rem; font-size: 0.9rem; color: var(--brand-muted); text-decoration: none; }
|
||||||
|
.dl-back:hover { color: var(--brand-gold); }
|
||||||
|
.dl-card {
|
||||||
|
display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 2rem; align-items: start;
|
||||||
|
background: var(--brand-black-3); border: 1px solid var(--brand-border); border-radius: 16px; padding: 1.75rem;
|
||||||
|
}
|
||||||
|
.dl-art img { width: 100%; max-height: 320px; object-fit: contain; border-radius: 10px; box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
|
||||||
|
.dl-body { min-width: 0; }
|
||||||
|
.dl-body .eyebrow { margin: 0 0 0.35rem; }
|
||||||
|
#root > :not(.admin-shell) .dl-card h1 { font-size: clamp(1.7rem, 3.2vw, 2.25rem); line-height: 1.15; margin: 0 0 0.75rem; }
|
||||||
|
.dl-summary { margin: 0 0 0.9rem; font-size: 1rem; line-height: 1.65; color: var(--brand-muted); white-space: pre-line; }
|
||||||
|
.dl-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 1.1rem; }
|
||||||
|
.dl-tag { font-size: 0.78rem; padding: 0.15rem 0.7rem; border-radius: 999px; border: 1px solid var(--brand-gold-dark); color: var(--brand-gold-light); }
|
||||||
|
.dl-form .study-download-form { margin-top: 0.4rem; }
|
||||||
|
.dl-form .study-download-grid label:nth-child(3) { grid-column: 1 / -1; }
|
||||||
|
/* The consent line is a sentence, not a field label */
|
||||||
|
.dl-form .study-download-form label.contact-consent {
|
||||||
|
flex-direction: row; align-items: flex-start; gap: 0.6rem;
|
||||||
|
text-transform: none; letter-spacing: normal; font-size: 0.9rem; font-weight: 400; color: var(--brand-muted);
|
||||||
|
}
|
||||||
|
.dl-form .study-download-form label.contact-consent input { margin-top: 0.25rem; flex-shrink: 0; }
|
||||||
|
.dl-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-top: 1.1rem; font-size: 0.88rem; color: var(--brand-muted); }
|
||||||
|
.dl-actions .btn-secondary { padding: 0.5rem 1.1rem; font-size: 0.8rem; }
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.dl-page { padding: 1rem 0 2.5rem; }
|
||||||
|
.dl-card { grid-template-columns: minmax(0, 1fr); gap: 1rem; padding: 1.1rem; }
|
||||||
|
.dl-art { display: flex; justify-content: center; }
|
||||||
|
.dl-art img { width: auto; max-width: 150px; max-height: 200px; }
|
||||||
|
.dl-form .study-download-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0.7rem; }
|
||||||
|
.dl-form .study-download-form { padding: 0.85rem; }
|
||||||
|
.dl-form .study-download-form input[type='text'],
|
||||||
|
.dl-form .study-download-form input[type='email'] { min-width: 0; width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.dl-form .study-download-grid { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Contact page ──────────────────────────────────────────────────────── */
|
||||||
|
.cx-section { padding: 2.5rem 0 2rem; }
|
||||||
|
.cx-layout { display: grid; grid-template-columns: minmax(0, 340px) minmax(0, 1fr); gap: 2.5rem; align-items: start; }
|
||||||
|
.cx-side { position: sticky; top: 9rem; display: flex; flex-direction: column; gap: 1.1rem; }
|
||||||
|
.cx-profile { display: flex; align-items: center; gap: 0.9rem; }
|
||||||
|
.cx-photo { width: 68px; height: 68px; border-radius: 50%; object-fit: cover; border: 2px solid var(--brand-gold-dark); flex-shrink: 0; }
|
||||||
|
.cx-name { margin: 0; font-family: var(--brand-font-heading); font-size: 1.5rem; font-weight: 600; color: var(--brand-warm-white); line-height: 1.1; }
|
||||||
|
.cx-role { margin: 0.2rem 0 0; font-size: 0.88rem; color: var(--brand-muted); }
|
||||||
|
.cx-quote { margin: 0; padding: 0 0 0 1rem; border-left: 2px solid var(--brand-gold-dark); font-size: 1rem; line-height: 1.6; font-style: italic; color: var(--brand-warm-white); }
|
||||||
|
.cx-intro { margin: 0; font-size: 0.98rem; line-height: 1.6; color: var(--brand-muted); }
|
||||||
|
.cx-points { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||||
|
.cx-point { display: flex; align-items: flex-start; gap: 0.6rem; font-size: 0.9rem; color: var(--brand-muted); }
|
||||||
|
.cx-point svg { margin-top: 0.2rem; flex-shrink: 0; color: var(--brand-gold); }
|
||||||
|
.cx-verse { margin: 0; font-size: 0.88rem; line-height: 1.55; color: var(--brand-muted); }
|
||||||
|
.cx-verse span { color: var(--brand-gold); white-space: nowrap; }
|
||||||
|
.cx-qa-link { align-self: flex-start; font-size: 0.9rem; color: var(--brand-gold); text-decoration: none; }
|
||||||
|
.cx-qa-link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.cx-form-card { background: var(--brand-black-3); border: 1px solid var(--brand-border); border-radius: 16px; padding: 1.75rem; }
|
||||||
|
.cx-form { display: flex; flex-direction: column; gap: 1.1rem; }
|
||||||
|
.cx-form label { display: flex; flex-direction: column; gap: 0.35rem; font-size: 0.85rem; font-weight: 500; color: var(--brand-muted); }
|
||||||
|
.cx-form input[type='text'], .cx-form input[type='email'], .cx-form textarea {
|
||||||
|
font: inherit; font-size: 1rem; font-weight: 400; color: var(--brand-warm-white); background: var(--brand-black);
|
||||||
|
border: 1px solid var(--brand-border); border-radius: 8px; padding: 0.7rem 0.85rem; width: 100%; min-width: 0;
|
||||||
|
transition: border-color 150ms, box-shadow 150ms;
|
||||||
|
}
|
||||||
|
.cx-form textarea { resize: vertical; min-height: 9rem; line-height: 1.55; }
|
||||||
|
.cx-form input:focus, .cx-form textarea:focus { outline: none; border-color: var(--brand-gold); box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.16); }
|
||||||
|
.cx-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1rem; }
|
||||||
|
.cx-count { align-self: flex-end; font-size: 0.75rem; color: var(--brand-muted); opacity: 0.8; }
|
||||||
|
|
||||||
|
.cx-types { border: 0; margin: 0; padding: 0; min-width: 0; }
|
||||||
|
.cx-types legend { padding: 0; margin-bottom: 0.55rem; font-size: 0.85rem; font-weight: 500; color: var(--brand-muted); }
|
||||||
|
.cx-type-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||||
|
.cx-form .cx-type { position: relative; flex-direction: row; gap: 0; cursor: pointer; }
|
||||||
|
.cx-type input { position: absolute; opacity: 0; width: 1px; height: 1px; }
|
||||||
|
.cx-type span {
|
||||||
|
font-size: 0.9rem; padding: 0.4rem 0.95rem; border-radius: 999px; color: var(--brand-muted);
|
||||||
|
border: 1px solid var(--brand-border); transition: border-color 150ms, background 150ms, color 150ms;
|
||||||
|
}
|
||||||
|
.cx-type:hover span { border-color: var(--brand-gold-dark); color: var(--brand-warm-white); }
|
||||||
|
.cx-type input:checked + span { background: var(--brand-gold); border-color: var(--brand-gold); color: var(--brand-black); font-weight: 600; }
|
||||||
|
.cx-type input:focus-visible + span { outline: 2px solid var(--brand-gold-light); outline-offset: 2px; }
|
||||||
|
.cx-hint { margin: 0.6rem 0 0; font-size: 0.88rem; color: var(--brand-muted); }
|
||||||
|
|
||||||
|
.cx-checks { display: flex; flex-direction: column; gap: 0.6rem; }
|
||||||
|
.cx-form label.cx-check { flex-direction: row; align-items: flex-start; gap: 0.6rem; font-size: 0.9rem; font-weight: 400; cursor: pointer; }
|
||||||
|
.cx-check input { margin-top: 0.22rem; flex-shrink: 0; accent-color: var(--brand-gold); }
|
||||||
|
.cx-submit { align-self: flex-start; justify-content: center; min-width: 200px; }
|
||||||
|
|
||||||
|
.cx-support { padding: 1rem 0 3.5rem; }
|
||||||
|
.cx-support-card {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: 2rem; padding: 1.5rem 1.75rem;
|
||||||
|
background: linear-gradient(135deg, rgba(201, 168, 76, 0.1), rgba(201, 168, 76, 0.02));
|
||||||
|
border: 1px solid var(--brand-gold-dark); border-radius: 16px;
|
||||||
|
}
|
||||||
|
.cx-support-copy { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; min-width: 0; }
|
||||||
|
.cx-support-copy .eyebrow { margin: 0; }
|
||||||
|
#root > :not(.admin-shell) .cx-support h2 { font-size: clamp(1.4rem, 2.6vw, 1.75rem); margin: 0; line-height: 1.2; }
|
||||||
|
.cx-support-copy p:not(.eyebrow) { margin: 0 0 0.4rem; font-size: 0.95rem; line-height: 1.6; color: var(--brand-muted); max-width: 52ch; }
|
||||||
|
.cx-support-qr { margin: 0; flex-shrink: 0; text-align: center; }
|
||||||
|
.cx-support-qr img { width: 120px; height: 120px; border-radius: 10px; background: #fff; padding: 6px; display: block; }
|
||||||
|
.cx-support-qr figcaption { margin-top: 0.4rem; font-size: 0.78rem; color: var(--brand-muted); }
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.cx-layout { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; }
|
||||||
|
.cx-side { position: static; order: 2; }
|
||||||
|
.cx-form-card { order: 1; }
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.cx-section { padding: 1.5rem 0 1rem; }
|
||||||
|
.cx-form-card { padding: 1.1rem; }
|
||||||
|
.cx-submit { align-self: stretch; }
|
||||||
|
.cx-support-card { flex-direction: column; align-items: flex-start; gap: 1.1rem; padding: 1.25rem; }
|
||||||
|
.cx-support-qr { align-self: center; }
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.cx-row { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── About page ────────────────────────────────────────────────────────── */
|
||||||
|
.ab-section { padding: 2.5rem 0 1.5rem; }
|
||||||
|
.ab-intro { display: grid; grid-template-columns: minmax(0, 300px) minmax(0, 1fr); gap: 2.5rem; align-items: center; }
|
||||||
|
.ab-photo { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 16px; border: 1px solid var(--brand-gold-dark); box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5); }
|
||||||
|
.ab-intro-copy { display: flex; flex-direction: column; gap: 0.9rem; align-items: flex-start; }
|
||||||
|
.ab-intro-copy .eyebrow { margin: 0; }
|
||||||
|
.ab-bio { margin: 0; font-size: 1.1rem; line-height: 1.75; color: var(--brand-warm-white); max-width: 60ch; }
|
||||||
|
.ab-cta-row { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 0.4rem; }
|
||||||
|
|
||||||
|
.ab-show {
|
||||||
|
display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 340px); gap: 2.5rem; align-items: center;
|
||||||
|
margin-top: 3rem; padding: 2rem; border-radius: 16px;
|
||||||
|
background: var(--brand-black-3); border: 1px solid var(--brand-border);
|
||||||
|
}
|
||||||
|
.ab-show-copy { display: flex; flex-direction: column; gap: 0.8rem; }
|
||||||
|
.ab-show-copy .eyebrow { margin: 0; }
|
||||||
|
#root > :not(.admin-shell) .ab-show-copy h2 { margin: 0; line-height: 1.15; }
|
||||||
|
.ab-show-copy p:not(.eyebrow) { margin: 0; font-size: 1.02rem; line-height: 1.7; color: var(--brand-muted); max-width: 60ch; }
|
||||||
|
.ab-verse-art { width: 100%; height: auto; border-radius: 12px; box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5); }
|
||||||
|
|
||||||
|
.ab-paths { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; margin-top: 1.5rem; }
|
||||||
|
.ab-path {
|
||||||
|
display: flex; flex-direction: column; gap: 0.35rem; padding: 1.15rem; text-decoration: none; color: inherit;
|
||||||
|
background: transparent; border: 1px solid var(--brand-border); border-radius: 12px;
|
||||||
|
transition: border-color 150ms, transform 150ms;
|
||||||
|
}
|
||||||
|
.ab-path:hover { border-color: var(--brand-gold-dark); transform: translateY(-2px); }
|
||||||
|
.ab-path-title { font-family: var(--brand-font-heading); font-size: 1.35rem; font-weight: 600; color: var(--brand-warm-white); }
|
||||||
|
.ab-path-text { font-size: 0.92rem; line-height: 1.5; color: var(--brand-muted); }
|
||||||
|
.ab-path-go { margin-top: 0.2rem; font-size: 0.85rem; color: var(--brand-gold); }
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.ab-intro { grid-template-columns: minmax(0, 220px) minmax(0, 1fr); gap: 1.75rem; }
|
||||||
|
.ab-show { grid-template-columns: minmax(0, 1fr); padding: 1.5rem; }
|
||||||
|
.ab-verse-art { max-width: 420px; justify-self: center; }
|
||||||
|
.ab-paths { grid-template-columns: minmax(0, 1fr); gap: 0.6rem; }
|
||||||
|
.ab-path { padding: 0.9rem 1rem; }
|
||||||
|
.ab-path-go { display: none; }
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.ab-section { padding: 1.5rem 0 1rem; }
|
||||||
|
.ab-intro { grid-template-columns: minmax(0, 1fr); gap: 1.25rem; }
|
||||||
|
.ab-photo { max-width: 200px; aspect-ratio: 1 / 1; }
|
||||||
|
.ab-bio { font-size: 1.02rem; }
|
||||||
|
.ab-show { margin-top: 2rem; padding: 1.15rem; gap: 1.25rem; }
|
||||||
|
.ab-cta-row .btn-primary, .ab-cta-row .btn-secondary { flex: 1 1 100%; justify-content: center; text-align: center; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.section-testimonials { padding-top: 2rem; padding-bottom: 2rem; }
|
||||||
|
.cx-support { padding-bottom: 2rem; }
|
||||||
|
}
|
||||||
|
|
||||||
+160
-351
@@ -14,6 +14,7 @@ import { GlobalSearch } from './components/GlobalSearch'
|
|||||||
import { EpisodeAudioPlayer } from './components/EpisodeAudioPlayer'
|
import { EpisodeAudioPlayer } from './components/EpisodeAudioPlayer'
|
||||||
import { NuggetsHostPortal } from './components/NuggetsHostPortal'
|
import { NuggetsHostPortal } from './components/NuggetsHostPortal'
|
||||||
import { HeaderAccount } from './components/HeaderAccount'
|
import { HeaderAccount } from './components/HeaderAccount'
|
||||||
|
import { ResourcesHub } from './components/ResourcesHub'
|
||||||
import { BIBLE_BOOKS } from '../shared/bible-books.js'
|
import { BIBLE_BOOKS } from '../shared/bible-books.js'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import { sendEvent, useScrollDepthTracking, useTimeOnPage, useUTMCapture, useLinkClickTracking, checkAnalyticsConsentResponse } from './analytics'
|
import { sendEvent, useScrollDepthTracking, useTimeOnPage, useUTMCapture, useLinkClickTracking, checkAnalyticsConsentResponse } from './analytics'
|
||||||
@@ -636,56 +637,83 @@ function KofiSupportSection({ content }: { content: SiteContent }) {
|
|||||||
const qrUrl = content.platformKofiQrUrl
|
const qrUrl = content.platformKofiQrUrl
|
||||||
if (!kofiUrl && !qrUrl) return null
|
if (!kofiUrl && !qrUrl) return null
|
||||||
return (
|
return (
|
||||||
<section className="section-kofi-support" aria-label="Support the show">
|
<section className="cx-support" aria-label="Support the show">
|
||||||
<div className="section-inner kofi-support-inner">
|
<div className="section-inner">
|
||||||
<p className="eyebrow">Support the Ministry</p>
|
<div className="cx-support-card">
|
||||||
<h2>Help Keep the Word Going</h2>
|
<div className="cx-support-copy">
|
||||||
<p className="kofi-support-desc">
|
<p className="eyebrow">Support the Ministry</p>
|
||||||
This podcast and study resources are listener-supported. If God has used this ministry in your life,
|
<h2>Help Keep the Word Going</h2>
|
||||||
consider buying me a coffee — it goes directly toward keeping the teaching free for everyone.
|
<p>
|
||||||
</p>
|
This podcast and its study resources are listener-supported. If God has used this ministry in your life,
|
||||||
|
consider buying me a coffee. It goes straight toward keeping the teaching free for everyone.
|
||||||
|
</p>
|
||||||
|
{kofiUrl && (
|
||||||
|
<a href={kofiUrl} target="_blank" rel="noreferrer" className="kofi-btn">
|
||||||
|
☕ Support on Ko-fi
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{qrUrl && (
|
{qrUrl && (
|
||||||
<div className="kofi-qr-wrap">
|
<figure className="cx-support-qr">
|
||||||
<img src={qrUrl} alt="Ko-fi donation QR code" className="kofi-qr-img" />
|
<img src={qrUrl} alt="Ko-fi donation QR code" />
|
||||||
<p className="kofi-qr-caption">Scan to give on Ko-fi</p>
|
<figcaption>Scan to give on Ko-fi</figcaption>
|
||||||
</div>
|
</figure>
|
||||||
)}
|
|
||||||
{kofiUrl && (
|
|
||||||
<a href={kofiUrl} target="_blank" rel="noreferrer" className="kofi-btn">
|
|
||||||
☕ Support on Ko-fi
|
|
||||||
</a>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AboutSection({ content }: { content: SiteContent }) {
|
function AboutSection({ content }: { content: SiteContent }) {
|
||||||
|
const paths = [
|
||||||
|
{ to: '/start-here', title: 'New here?', text: 'A short path through the show, one step at a time.' },
|
||||||
|
{ to: '/episodes', title: 'Listen', text: 'Every episode, newest first.' },
|
||||||
|
{ to: '/study', title: 'Study with us', text: 'Self-paced Bible studies with notes and discussion.' },
|
||||||
|
]
|
||||||
return (
|
return (
|
||||||
<section className="section-about" aria-label="About the show">
|
<section className="ab-section" aria-label="About the show">
|
||||||
<div className="section-inner about-inner">
|
<div className="section-inner">
|
||||||
<div className="about-photo">
|
<div className="ab-intro">
|
||||||
<img src={content.aboutPhotoUrl || '/images/nate-photo.jpeg'} alt="Nate Emmert" />
|
<img
|
||||||
<div className="about-photo-divider" aria-hidden="true" />
|
src={content.aboutPhotoUrl || '/images/nate-photo.jpeg'}
|
||||||
<div className="about-nate-copy">
|
alt="Nate Emmert"
|
||||||
|
className="ab-photo"
|
||||||
|
/>
|
||||||
|
<div className="ab-intro-copy">
|
||||||
<p className="eyebrow">{content.aboutEyebrow || 'About Nate'}</p>
|
<p className="eyebrow">{content.aboutEyebrow || 'About Nate'}</p>
|
||||||
<div className="about-nate-rule" aria-hidden="true" />
|
<p className="ab-bio">{content.aboutNate}</p>
|
||||||
<p>{content.aboutNate}</p>
|
<div className="ab-cta-row">
|
||||||
<Link to="/episodes" className="about-nate-cta">{content.aboutListenBtnLabel || 'Listen Now ↓'}</Link>
|
<Link to="/episodes" className="btn-primary">{content.aboutListenBtnLabel || 'Listen Now ↓'}</Link>
|
||||||
|
<Link to="/start-here" className="btn-secondary">New here? Start here</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="about-text">
|
|
||||||
{content.aboutShowEyebrow && <p className="eyebrow">{content.aboutShowEyebrow}</p>}
|
<div className="ab-show">
|
||||||
<h2>{content.aboutShowHeading}</h2>
|
<div className="ab-show-copy">
|
||||||
<p>{content.aboutShowP1}</p>
|
{content.aboutShowEyebrow && <p className="eyebrow">{content.aboutShowEyebrow}</p>}
|
||||||
<p>{content.aboutShowP2}</p>
|
<h2>{content.aboutShowHeading}</h2>
|
||||||
<div className="rule-divider" aria-hidden="true" />
|
<p>{content.aboutShowP1}</p>
|
||||||
|
<p>{content.aboutShowP2}</p>
|
||||||
|
</div>
|
||||||
<img
|
<img
|
||||||
src={content.aboutVerseArtUrl || '/images/hebrews-4-12-verse-art.png'}
|
src={content.aboutVerseArtUrl || '/images/hebrews-4-12-verse-art.png'}
|
||||||
alt="Hebrews 4:12 verse artwork"
|
alt="Hebrews 4:12 verse artwork"
|
||||||
className="about-verse-art"
|
className="ab-verse-art"
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="ab-paths">
|
||||||
|
{paths.map(p => (
|
||||||
|
<Link key={p.to} to={p.to} className="ab-path">
|
||||||
|
<span className="ab-path-title">{p.title}</span>
|
||||||
|
<span className="ab-path-text">{p.text}</span>
|
||||||
|
<span className="ab-path-go">Go →</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
@@ -693,64 +721,59 @@ function AboutSection({ content }: { content: SiteContent }) {
|
|||||||
|
|
||||||
|
|
||||||
function ContactSection({ content }: { content: SiteContent }) {
|
function ContactSection({ content }: { content: SiteContent }) {
|
||||||
|
const point = (text: string, icon: ReactElement) => (
|
||||||
|
<div className="cx-point">
|
||||||
|
{icon}
|
||||||
|
<span>{text}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<section className="section-contact" id="contact" aria-label="Contact form">
|
<section className="cx-section" id="contact" aria-label="Contact form">
|
||||||
<div className="section-inner contact-inner">
|
<div className="section-inner cx-layout">
|
||||||
<div className="contact-copy">
|
<aside className="cx-side">
|
||||||
<div className="contact-card">
|
<div className="cx-profile">
|
||||||
<div className="contact-profile">
|
<img
|
||||||
<img
|
src={content.contactPhotoUrl || '/images/nate-contact-photo.png'}
|
||||||
src={content.contactPhotoUrl || '/images/nate-contact-photo.png'}
|
alt={content.contactName || 'Nate'}
|
||||||
alt={content.contactName || 'Nate'}
|
className="cx-photo"
|
||||||
className="contact-profile-photo"
|
/>
|
||||||
/>
|
<div>
|
||||||
<div className="contact-profile-meta">
|
<p className="cx-name">{content.contactName || 'Nate'}</p>
|
||||||
<p className="contact-profile-name">{content.contactName || 'Nate'}</p>
|
<p className="cx-role">{content.contactRole || 'Bible teacher · Lynchburg, VA'}</p>
|
||||||
<p className="contact-profile-role">{content.contactRole || 'Bible teacher · Lynchburg, VA'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{content.contactQuote && (
|
|
||||||
<blockquote className="contact-quote">
|
|
||||||
<p>"{content.contactQuote}"</p>
|
|
||||||
</blockquote>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{content.contactIntro && (
|
|
||||||
<p className="contact-intro">{content.contactIntro}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="contact-points" aria-label="Contact response details">
|
|
||||||
{content.contactPoint1 && (
|
|
||||||
<div className="contact-point">
|
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
||||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
||||||
</svg>
|
|
||||||
<span>{content.contactPoint1}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{content.contactPoint2 && (
|
|
||||||
<div className="contact-point">
|
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<polyline points="12 6 12 12 16 14" />
|
|
||||||
</svg>
|
|
||||||
<span>{content.contactPoint2}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{content.contactVerse && (
|
|
||||||
<div className="contact-scripture">
|
|
||||||
<p>
|
|
||||||
"{content.contactVerse}" -{' '}
|
|
||||||
<span className="contact-scripture-ref">{content.contactVerseRef}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{content.contactQuote && <blockquote className="cx-quote">“{content.contactQuote}”</blockquote>}
|
||||||
|
{content.contactIntro && <p className="cx-intro">{content.contactIntro}</p>}
|
||||||
|
|
||||||
|
{(content.contactPoint1 || content.contactPoint2) && (
|
||||||
|
<div className="cx-points" aria-label="Contact response details">
|
||||||
|
{content.contactPoint1 && point(content.contactPoint1, (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||||
|
</svg>
|
||||||
|
))}
|
||||||
|
{content.contactPoint2 && point(content.contactPoint2, (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<polyline points="12 6 12 12 16 14" />
|
||||||
|
</svg>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{content.contactVerse && (
|
||||||
|
<p className="cx-verse">
|
||||||
|
“{content.contactVerse}” <span>{content.contactVerseRef}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Link to="/questions" className="cx-qa-link">Browse answered questions →</Link>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div className="cx-form-card">
|
||||||
|
<ContactForm />
|
||||||
</div>
|
</div>
|
||||||
<ContactForm />
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
@@ -854,131 +877,6 @@ function PageBanner({ eyebrow, title, desc }: { eyebrow: string; title: string;
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DownloadLibrarySection({ content }: { content: SiteContent }) {
|
|
||||||
const resources = (content.customLinks ?? []).filter(link => link.placement === 'resources')
|
|
||||||
const [activeTag, setActiveTag] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const hasPrimaryGuide = !!(content.studyGuideTitle)
|
|
||||||
const allTags = Array.from(new Set(resources.flatMap(r => r.tags ?? []))).filter(Boolean)
|
|
||||||
|
|
||||||
const filteredResources = resources.filter(r => {
|
|
||||||
return !activeTag || (r.tags ?? []).includes(activeTag)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!hasPrimaryGuide && !content.secondSeries?.guide?.title && resources.length === 0) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="section-resources section-download-library" aria-label="Download library">
|
|
||||||
<div className="section-inner">
|
|
||||||
{hasPrimaryGuide && (
|
|
||||||
<Link to="/downloads/primary-guide" className="resource-download-card resource-download-card--link resource-download-card--featured">
|
|
||||||
<div className="resource-download-header">
|
|
||||||
<img
|
|
||||||
src={content.seriesImageUrl || '/images/titus-cover.png'}
|
|
||||||
alt={content.seriesTitle}
|
|
||||||
className="resource-link-image resource-link-image--featured"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div className="resource-download-meta">
|
|
||||||
<div className="featured-badge">Featured</div>
|
|
||||||
<span className="resource-link-label">{content.studyGuideTitle}</span>
|
|
||||||
{content.studyGuideDescription && (
|
|
||||||
<p className="resource-link-description">{content.studyGuideDescription}</p>
|
|
||||||
)}
|
|
||||||
<span className="resource-link-action">Download free guide →</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{content.secondSeries?.guide?.title && (
|
|
||||||
<Link to="/downloads/second-guide" className="resource-download-card resource-download-card--link resource-download-card--featured">
|
|
||||||
<div className="resource-download-header">
|
|
||||||
{content.secondSeries.imageUrl && (
|
|
||||||
<img
|
|
||||||
src={content.secondSeries.imageUrl}
|
|
||||||
alt={content.secondSeries.title}
|
|
||||||
className="resource-link-image resource-link-image--featured"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="resource-download-meta">
|
|
||||||
<div className="featured-badge">Featured</div>
|
|
||||||
<span className="resource-link-label">{content.secondSeries.guide.title}</span>
|
|
||||||
{content.secondSeries.guide.description && (
|
|
||||||
<p className="resource-link-description">{content.secondSeries.guide.description}</p>
|
|
||||||
)}
|
|
||||||
<span className="resource-link-action">Download free guide →</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{resources.length > 0 && (
|
|
||||||
<>
|
|
||||||
{allTags.length > 0 && (
|
|
||||||
<div className="download-filter-bar">
|
|
||||||
<div className="download-tag-filter">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`download-tag-chip${activeTag === null ? ' download-tag-chip--active' : ''}`}
|
|
||||||
onClick={() => setActiveTag(null)}
|
|
||||||
>
|
|
||||||
All topics
|
|
||||||
</button>
|
|
||||||
{allTags.map(tag => (
|
|
||||||
<button
|
|
||||||
key={tag}
|
|
||||||
type="button"
|
|
||||||
className={`download-tag-chip${activeTag === tag ? ' download-tag-chip--active' : ''}`}
|
|
||||||
onClick={() => setActiveTag(prev => prev === tag ? null : tag)}
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
{activeTag !== null && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="download-clear-link"
|
|
||||||
onClick={() => setActiveTag(null)}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{filteredResources.length > 0 ? (
|
|
||||||
<div className="resources-list">
|
|
||||||
{filteredResources.map(resource => (
|
|
||||||
<Link key={resource.id} to={`/downloads/${buildCustomDownloadPageId(resource.id)}`} className="resource-download-card resource-download-card--link">
|
|
||||||
<div className="resource-download-header">
|
|
||||||
{resource.imageUrl && (
|
|
||||||
<img src={resource.imageUrl} alt={resource.label} className="resource-link-image" loading="lazy" />
|
|
||||||
)}
|
|
||||||
<div className="resource-download-meta">
|
|
||||||
<span className="resource-link-label">{resource.label}</span>
|
|
||||||
{resource.description && <p className="resource-link-description">{resource.description}</p>}
|
|
||||||
{(resource.tags ?? []).length > 0 && (
|
|
||||||
<div className="resource-link-tags">{(resource.tags ?? []).join(', ')}</div>
|
|
||||||
)}
|
|
||||||
<span className="resource-link-action">Open download page →</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="download-empty-state">No resources tagged "{activeTag}".</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DownloadDetailPage({ content }: { content: SiteContent }) {
|
function DownloadDetailPage({ content }: { content: SiteContent }) {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
const resource = resolveDownloadPageResource(content, id)
|
const resource = resolveDownloadPageResource(content, id)
|
||||||
@@ -1002,44 +900,57 @@ function DownloadDetailPage({ content }: { content: SiteContent }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="thanks-page download-detail-page" aria-label={resource.label}>
|
<div className="site">
|
||||||
<div className="thanks-card download-detail-card">
|
<SiteHeader content={content} />
|
||||||
<p className="eyebrow">Resources</p>
|
<SiteSearchBar content={content} />
|
||||||
<h1>{resource.label}</h1>
|
<main className="dl-page" aria-label={resource.label}>
|
||||||
<p>{resource.summary}</p>
|
<div className="section-inner">
|
||||||
|
<Link to="/resources" className="dl-back">← Back to Resources</Link>
|
||||||
|
<article className="dl-card">
|
||||||
|
{resource.imageUrl && (
|
||||||
|
<div className="dl-art">
|
||||||
|
<img src={resource.imageUrl} alt={resource.label} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="dl-body">
|
||||||
|
<p className="eyebrow">Free download</p>
|
||||||
|
<h1>{resource.label}</h1>
|
||||||
|
{resource.summary && <p className="dl-summary">{resource.summary}</p>}
|
||||||
|
{resource.tags.length > 0 && (
|
||||||
|
<div className="dl-tags">
|
||||||
|
{resource.tags.map(tag => <span key={tag} className="dl-tag">{tag}</span>)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{resource.imageUrl && (
|
<div className="dl-form">
|
||||||
<div className="download-detail-art">
|
{/* The title is already on the page, so the button stays short */}
|
||||||
<img src={resource.imageUrl} alt={resource.label} className="guide-cover-img" />
|
{resource.isPrimaryGuide
|
||||||
</div>
|
? <StudyDownloadForm buttonText="Download now" />
|
||||||
)}
|
: <ResourceDownloadForm resourceId={resource.resourceId} buttonText="Download now" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
{resource.tags.length > 0 && (
|
{(resource.amazonUrl || resource.appleBooksUrl) && (
|
||||||
<div className="resource-link-tags">{resource.tags.join(', ')}</div>
|
<div className="dl-actions">
|
||||||
)}
|
<span>Or get it from:</span>
|
||||||
|
{resource.amazonUrl && (
|
||||||
<div className="download-detail-form-wrap">
|
<a href={resource.amazonUrl} target="_blank" rel="noreferrer" className="btn-secondary">
|
||||||
{resource.isPrimaryGuide
|
{resource.amazonLabel || 'Get it on Amazon'}
|
||||||
? <StudyDownloadForm buttonText={resource.buttonText} />
|
</a>
|
||||||
: <ResourceDownloadForm resourceId={resource.resourceId} buttonText={resource.buttonText} />}
|
)}
|
||||||
|
{resource.appleBooksUrl && (
|
||||||
|
<a href={resource.appleBooksUrl} target="_blank" rel="noreferrer" className="btn-secondary">
|
||||||
|
{resource.appleBooksLabel || 'Get it on Apple Books'}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
<div className="download-detail-actions">
|
<SiteFooter content={content} />
|
||||||
{resource.amazonUrl && (
|
<AnalyticsConsentBanner content={content} />
|
||||||
<a href={resource.amazonUrl} target="_blank" rel="noreferrer" className="btn-secondary">
|
</div>
|
||||||
{resource.amazonLabel || 'Get it on Amazon'}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{resource.appleBooksUrl && (
|
|
||||||
<a href={resource.appleBooksUrl} target="_blank" rel="noreferrer" className="btn-secondary">
|
|
||||||
{resource.appleBooksLabel || 'Get it on Apple Books'}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
<Link to="/resources" className="btn-secondary">Back to Resources</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2341,107 +2252,6 @@ function NuggetsHostDetailPage({ content }: { content: SiteContent }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BooksAndResourcesSection({ content }: { content: SiteContent }) {
|
|
||||||
const books = (content.customLinks ?? []).filter(link => link.placement === 'books')
|
|
||||||
const recommended = (content.customLinks ?? []).filter(link => link.placement === 'recommended')
|
|
||||||
const [activeTab, setActiveTab] = useState<'books' | 'recommended'>('books')
|
|
||||||
|
|
||||||
const hasBooks = books.length > 0
|
|
||||||
const hasRecommended = recommended.length > 0
|
|
||||||
const showTabs = hasBooks && hasRecommended
|
|
||||||
|
|
||||||
if (!hasBooks && !hasRecommended) return <TruthForLifeWidget type="resourcead" />
|
|
||||||
|
|
||||||
const tab = showTabs ? activeTab : (hasBooks ? 'books' : 'recommended')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="section-resources section-books-resources" aria-label="Books and recommended resources">
|
|
||||||
<div className="section-inner">
|
|
||||||
{showTabs ? (
|
|
||||||
<div className="books-resources-tabs">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`books-resources-tab${tab === 'books' ? ' books-resources-tab--active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('books')}
|
|
||||||
>
|
|
||||||
Reading List
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`books-resources-tab${tab === 'recommended' ? ' books-resources-tab--active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('recommended')}
|
|
||||||
>
|
|
||||||
Recommended Resources
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="download-library-head">
|
|
||||||
<p className="eyebrow">{tab === 'books' ? 'Reading List' : 'Recommended'}</p>
|
|
||||||
<h2 className="section-heading">{tab === 'books' ? 'Books worth your time.' : 'Resources worth your time.'}</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tab === 'books' && (
|
|
||||||
<>
|
|
||||||
{showTabs && (
|
|
||||||
<div className="download-library-head" style={{ marginBottom: '1.5rem' }}>
|
|
||||||
<h2 className="section-heading">Books worth your time.</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="books-list">
|
|
||||||
{books.map(book => (
|
|
||||||
<div key={book.id} className="book-card">
|
|
||||||
{book.imageUrl && (
|
|
||||||
<img src={book.imageUrl} alt={book.label} className="book-card-cover" loading="lazy" />
|
|
||||||
)}
|
|
||||||
<div className="book-card-meta">
|
|
||||||
<span className="book-card-title">{book.label}</span>
|
|
||||||
{book.description && <p className="book-card-description">{book.description}</p>}
|
|
||||||
{(book.amazonUrl || book.url) && (
|
|
||||||
<a href={book.amazonUrl || book.url} target="_blank" rel="noopener noreferrer" className="btn-primary book-card-btn">
|
|
||||||
{book.amazonUrl ? (book.amazonLabel || 'View on Amazon') : (book.amazonLabel || 'Learn More')}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{book.appleBooksUrl && (
|
|
||||||
<a href={book.appleBooksUrl} target="_blank" rel="noopener noreferrer" className="btn-secondary book-card-btn">
|
|
||||||
{book.appleBooksLabel || 'Get it on Apple Books'}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tab === 'recommended' && (
|
|
||||||
<>
|
|
||||||
{showTabs && (
|
|
||||||
<div className="download-library-head" style={{ marginBottom: '1.5rem' }}>
|
|
||||||
<h2 className="section-heading">Recommended Resources</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="recommended-resources-list">
|
|
||||||
{recommended.map(item => (
|
|
||||||
<a key={item.id} href={item.url} target="_blank" rel="noopener noreferrer" className="recommended-resource-card">
|
|
||||||
{item.imageUrl && (
|
|
||||||
<img src={item.imageUrl} alt={item.label} className="recommended-resource-image" loading="lazy" />
|
|
||||||
)}
|
|
||||||
<div className="recommended-resource-meta">
|
|
||||||
<span className="recommended-resource-title">{item.label}</span>
|
|
||||||
{item.description && <p className="recommended-resource-description">{item.description}</p>}
|
|
||||||
<span className="recommended-resource-action">Learn more →</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResourcesPage({ content }: { content: SiteContent }) {
|
function ResourcesPage({ content }: { content: SiteContent }) {
|
||||||
const siteTitle = content.seo?.title || DEFAULTS.seo.title
|
const siteTitle = content.seo?.title || DEFAULTS.seo.title
|
||||||
usePageMeta(
|
usePageMeta(
|
||||||
@@ -2453,9 +2263,8 @@ function ResourcesPage({ content }: { content: SiteContent }) {
|
|||||||
<SiteHeader content={content} />
|
<SiteHeader content={content} />
|
||||||
<SiteSearchBar content={content} />
|
<SiteSearchBar content={content} />
|
||||||
<PageBanner eyebrow="Resources" title="Study Guides & Resources" desc="Free guides, worksheets, and curated resources to help you go deeper in Scripture." />
|
<PageBanner eyebrow="Resources" title="Study Guides & Resources" desc="Free guides, worksheets, and curated resources to help you go deeper in Scripture." />
|
||||||
<DownloadLibrarySection content={content} />
|
<ResourcesHub content={content} downloadPath={id => `/downloads/${buildCustomDownloadPageId(id)}`} fallback={<TruthForLifeWidget type="resourcead" />} />
|
||||||
<CustomBlocksSection content={content} page="downloads" />
|
<CustomBlocksSection content={content} page="downloads" />
|
||||||
<BooksAndResourcesSection content={content} />
|
|
||||||
<SiteFooter content={content} />
|
<SiteFooter content={content} />
|
||||||
<AnalyticsConsentBanner content={content} />
|
<AnalyticsConsentBanner content={content} />
|
||||||
</div>
|
</div>
|
||||||
@@ -2710,8 +2519,8 @@ function AboutPage({ content }: { content: SiteContent }) {
|
|||||||
<SiteSearchBar content={content} />
|
<SiteSearchBar content={content} />
|
||||||
<PageBanner eyebrow="About" title="About the Show" />
|
<PageBanner eyebrow="About" title="About the Show" />
|
||||||
<AboutSection content={content} />
|
<AboutSection content={content} />
|
||||||
<KofiSupportSection content={content} />
|
|
||||||
<TestimonialsSection testimonials={content.testimonials ?? []} />
|
<TestimonialsSection testimonials={content.testimonials ?? []} />
|
||||||
|
<KofiSupportSection content={content} />
|
||||||
<CustomBlocksSection content={content} page="about" />
|
<CustomBlocksSection content={content} page="about" />
|
||||||
<SiteFooter content={content} />
|
<SiteFooter content={content} />
|
||||||
<AnalyticsConsentBanner content={content} />
|
<AnalyticsConsentBanner content={content} />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ChangeEvent } from 'react'
|
|||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
|
|
||||||
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]{1,64}@[a-zA-Z0-9.-]{1,253}\.[a-zA-Z]{2,}$/
|
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]{1,64}@[a-zA-Z0-9.-]{1,253}\.[a-zA-Z]{2,}$/
|
||||||
|
const MAX_MESSAGE = 3000
|
||||||
|
|
||||||
interface ContactFields {
|
interface ContactFields {
|
||||||
firstName: string
|
firstName: string
|
||||||
@@ -12,6 +13,14 @@ interface ContactFields {
|
|||||||
messageType: string
|
messageType: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TYPES: { value: string; label: string; hint: string; placeholder: string }[] = [
|
||||||
|
{ value: 'question', label: 'Bible question', hint: 'Ask anything about Scripture. Answers are posted on the Q&A page.', placeholder: 'What would you like to know?' },
|
||||||
|
{ value: 'community', label: 'Community question', hint: 'A question about a study discussion or the community.', placeholder: 'What’s on your mind?' },
|
||||||
|
{ value: 'testimony', label: 'Testimony', hint: 'Share what God is doing in your life. Every one is read.', placeholder: 'Tell your story…' },
|
||||||
|
{ value: 'topic', label: 'Topic request', hint: 'Suggest a passage or topic for a future episode.', placeholder: 'Which passage or topic?' },
|
||||||
|
{ value: 'general', label: 'General message', hint: 'Anything else you’d like to say.', placeholder: 'Write your message…' },
|
||||||
|
]
|
||||||
|
|
||||||
export default function ContactForm() {
|
export default function ContactForm() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
@@ -42,6 +51,8 @@ export default function ContactForm() {
|
|||||||
const [errorMsg, setErrorMsg] = useState('')
|
const [errorMsg, setErrorMsg] = useState('')
|
||||||
const [fieldErrors, setFieldErrors] = useState<{ email?: string }>({})
|
const [fieldErrors, setFieldErrors] = useState<{ email?: string }>({})
|
||||||
|
|
||||||
|
const activeType = TYPES.find(t => t.value === fields.messageType) ?? TYPES[0]
|
||||||
|
|
||||||
function handleChange(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
|
function handleChange(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
|
||||||
const { name, value } = e.target
|
const { name, value } = e.target
|
||||||
setFields(f => ({ ...f, [name]: value }))
|
setFields(f => ({ ...f, [name]: value }))
|
||||||
@@ -59,10 +70,6 @@ export default function ContactForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectChange(e: ChangeEvent<HTMLSelectElement>) {
|
|
||||||
setFields(f => ({ ...f, [e.target.name]: e.target.value }))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setStatus('submitting')
|
setStatus('submitting')
|
||||||
@@ -90,7 +97,7 @@ export default function ContactForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="contact-form" onSubmit={handleSubmit} noValidate>
|
<form className="cx-form" onSubmit={handleSubmit} noValidate>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="contact-honeypot"
|
className="contact-honeypot"
|
||||||
@@ -100,14 +107,37 @@ export default function ContactForm() {
|
|||||||
value={honey}
|
value={honey}
|
||||||
onChange={e => setHoney(e.target.value)}
|
onChange={e => setHoney(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<label>
|
|
||||||
First Name
|
<fieldset className="cx-types">
|
||||||
<input type="text" name="firstName" required autoComplete="given-name" value={fields.firstName} onChange={handleChange} />
|
<legend>What is this about?</legend>
|
||||||
</label>
|
<div className="cx-type-row" role="radiogroup">
|
||||||
<label>
|
{TYPES.map(t => (
|
||||||
Last Name
|
<label key={t.value} className="cx-type">
|
||||||
<input type="text" name="lastName" required autoComplete="family-name" value={fields.lastName} onChange={handleChange} />
|
<input
|
||||||
</label>
|
type="radio"
|
||||||
|
name="messageType"
|
||||||
|
value={t.value}
|
||||||
|
checked={fields.messageType === t.value}
|
||||||
|
onChange={() => setFields(f => ({ ...f, messageType: t.value }))}
|
||||||
|
/>
|
||||||
|
<span>{t.label}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="cx-hint" aria-live="polite">{activeType.hint}</p>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div className="cx-row">
|
||||||
|
<label>
|
||||||
|
First name
|
||||||
|
<input type="text" name="firstName" required autoComplete="given-name" value={fields.firstName} onChange={handleChange} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Last name
|
||||||
|
<input type="text" name="lastName" required autoComplete="family-name" value={fields.lastName} onChange={handleChange} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Email
|
Email
|
||||||
<input
|
<input
|
||||||
@@ -122,41 +152,37 @@ export default function ContactForm() {
|
|||||||
/>
|
/>
|
||||||
{fieldErrors.email && <span id="contact-email-error" className="study-signup-field-error">{fieldErrors.email}</span>}
|
{fieldErrors.email && <span id="contact-email-error" className="study-signup-field-error">{fieldErrors.email}</span>}
|
||||||
</label>
|
</label>
|
||||||
<label>
|
|
||||||
Message Type
|
|
||||||
<select name="messageType" value={fields.messageType} onChange={handleSelectChange}>
|
|
||||||
<option value="question">Bible Question</option>
|
|
||||||
<option value="community">Community Question</option>
|
|
||||||
<option value="testimony">Testimony</option>
|
|
||||||
<option value="topic">Topic Request</option>
|
|
||||||
<option value="general">General Message</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
<label>
|
||||||
Message
|
Message
|
||||||
<textarea name="message" rows={6} required value={fields.message} onChange={handleChange} />
|
<textarea
|
||||||
</label>
|
name="message"
|
||||||
{fields.messageType === 'question' && (
|
rows={6}
|
||||||
<label className="contact-consent">
|
required
|
||||||
<input
|
maxLength={MAX_MESSAGE}
|
||||||
type="checkbox"
|
placeholder={activeType.placeholder}
|
||||||
checked={notifyOnAnswer}
|
value={fields.message}
|
||||||
onChange={e => setNotifyOnAnswer(e.target.checked)}
|
onChange={handleChange}
|
||||||
/>
|
|
||||||
<span>Email me when this question is answered.</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<label className="contact-consent">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={subscribe}
|
|
||||||
onChange={e => setSubscribe(e.target.checked)}
|
|
||||||
/>
|
/>
|
||||||
<span>Send me updates from Verse by Verse with Nate. I can unsubscribe anytime.</span>
|
<span className="cx-count">{fields.message.length} / {MAX_MESSAGE}</span>
|
||||||
</label>
|
</label>
|
||||||
{status === 'error' && <p className="contact-error">{errorMsg}</p>}
|
|
||||||
<button type="submit" className="btn-primary" disabled={status === 'submitting'}>
|
<div className="cx-checks">
|
||||||
{status === 'submitting' ? 'Sending…' : 'Send Message'}
|
{fields.messageType === 'question' && (
|
||||||
|
<label className="cx-check">
|
||||||
|
<input type="checkbox" checked={notifyOnAnswer} onChange={e => setNotifyOnAnswer(e.target.checked)} />
|
||||||
|
<span>Email me when this question is answered.</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<label className="cx-check">
|
||||||
|
<input type="checkbox" checked={subscribe} onChange={e => setSubscribe(e.target.checked)} />
|
||||||
|
<span>Send me updates from Verse by Verse with Nate. I can unsubscribe anytime.</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status === 'error' && <p className="contact-error" role="alert">{errorMsg}</p>}
|
||||||
|
<button type="submit" className="btn-primary cx-submit" disabled={status === 'submitting'}>
|
||||||
|
{status === 'submitting' ? 'Sending…' : 'Send message'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import type { SiteContent } from '../content'
|
||||||
|
|
||||||
|
type TabKey = 'downloads' | 'books' | 'recommended'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
content: SiteContent
|
||||||
|
downloadPath: (resourceId: string) => string
|
||||||
|
// Shown when there are no downloads, books or recommended resources at all
|
||||||
|
fallback: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// A description that stays short in the grid; long ones get a Read more toggle
|
||||||
|
function ClampedText({ text, className, lines = 3, threshold = 150 }: { text: string; className: string; lines?: number; threshold?: number }) {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const long = text.length > threshold
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p
|
||||||
|
className={`${className}${long && !open ? ' res-clamped' : ''}`}
|
||||||
|
style={long && !open ? { WebkitLineClamp: lines } : undefined}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
{long && (
|
||||||
|
<button type="button" className="res-more" onClick={() => setOpen(o => !o)} aria-expanded={open}>
|
||||||
|
{open ? 'Show less' : 'Read more'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResourcesHub({ content, downloadPath, fallback }: Props) {
|
||||||
|
const links = content.customLinks ?? []
|
||||||
|
const resources = links.filter(l => l.placement === 'resources')
|
||||||
|
const books = links.filter(l => l.placement === 'books')
|
||||||
|
const recommended = links.filter(l => l.placement === 'recommended')
|
||||||
|
|
||||||
|
const featured: { key: string; to: string; title: string; description: string; image: string; alt: string }[] = []
|
||||||
|
if (content.studyGuideTitle) {
|
||||||
|
featured.push({
|
||||||
|
key: 'primary',
|
||||||
|
to: '/downloads/primary-guide',
|
||||||
|
title: content.studyGuideTitle,
|
||||||
|
description: content.studyGuideDescription ?? '',
|
||||||
|
image: content.seriesImageUrl || '/images/titus-cover.png',
|
||||||
|
alt: content.seriesTitle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (content.secondSeries?.guide?.title) {
|
||||||
|
featured.push({
|
||||||
|
key: 'second',
|
||||||
|
to: '/downloads/second-guide',
|
||||||
|
title: content.secondSeries.guide.title,
|
||||||
|
description: content.secondSeries.guide.description ?? '',
|
||||||
|
image: content.secondSeries.imageUrl ?? '',
|
||||||
|
alt: content.secondSeries.title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const counts: Record<TabKey, number> = {
|
||||||
|
downloads: featured.length + resources.length,
|
||||||
|
books: books.length,
|
||||||
|
recommended: recommended.length,
|
||||||
|
}
|
||||||
|
const available = (Object.keys(counts) as TabKey[]).filter(k => counts[k] > 0)
|
||||||
|
|
||||||
|
const fromHash = (): TabKey | null => {
|
||||||
|
const h = typeof window !== 'undefined' ? window.location.hash.replace('#', '') : ''
|
||||||
|
return h === 'downloads' || h === 'books' || h === 'recommended' ? h : null
|
||||||
|
}
|
||||||
|
const [picked, setPicked] = useState<TabKey | null>(fromHash)
|
||||||
|
const [activeTag, setActiveTag] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Links like /resources#books, and the browser's back/forward buttons, switch the tab too
|
||||||
|
useEffect(() => {
|
||||||
|
const onHash = () => setPicked(fromHash())
|
||||||
|
window.addEventListener('hashchange', onHash)
|
||||||
|
return () => window.removeEventListener('hashchange', onHash)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (available.length === 0) return <>{fallback}</>
|
||||||
|
|
||||||
|
const tab: TabKey = picked && counts[picked] > 0 ? picked : available[0]
|
||||||
|
const choose = (k: TabKey) => {
|
||||||
|
setPicked(k)
|
||||||
|
try { window.history.replaceState(null, '', `#${k}`) } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = Array.from(new Set(resources.flatMap(r => r.tags ?? []))).filter(Boolean)
|
||||||
|
const shownResources = resources.filter(r => !activeTag || (r.tags ?? []).includes(activeTag))
|
||||||
|
const labels: Record<TabKey, string> = { downloads: 'Downloads', books: 'Reading List', recommended: 'Recommended' }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="res-hub" aria-label="Resources">
|
||||||
|
<div className="section-inner">
|
||||||
|
{available.length > 1 && (
|
||||||
|
<div className="res-tabs" role="tablist" aria-label="Resource types">
|
||||||
|
{available.map(k => (
|
||||||
|
<button
|
||||||
|
key={k}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
id={`res-tab-${k}`}
|
||||||
|
aria-selected={tab === k}
|
||||||
|
aria-controls="res-panel"
|
||||||
|
className={`res-tab${tab === k ? ' res-tab--on' : ''}`}
|
||||||
|
onClick={() => choose(k)}
|
||||||
|
>
|
||||||
|
{labels[k]} <span>{counts[k]}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div id="res-panel" role="tabpanel" aria-labelledby={`res-tab-${tab}`}>
|
||||||
|
{tab === 'downloads' && (
|
||||||
|
<>
|
||||||
|
{featured.length > 0 && (
|
||||||
|
<div className="res-featured">
|
||||||
|
{featured.map(f => (
|
||||||
|
<Link key={f.key} to={f.to} className="res-feature">
|
||||||
|
{f.image && <img src={f.image} alt={f.alt} className="res-feature-img" loading="lazy" />}
|
||||||
|
<div className="res-feature-body">
|
||||||
|
<span className="res-badge">Featured</span>
|
||||||
|
<span className="res-title">{f.title}</span>
|
||||||
|
{f.description && <p className="res-desc res-clamped" style={{ WebkitLineClamp: 2 }}>{f.description}</p>}
|
||||||
|
<span className="res-action">Download free guide →</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tags.length > 0 && (
|
||||||
|
<div className="res-tags" role="group" aria-label="Filter by topic">
|
||||||
|
<button type="button" className={`res-chip${activeTag === null ? ' res-chip--on' : ''}`} onClick={() => setActiveTag(null)}>All topics</button>
|
||||||
|
{tags.map(t => (
|
||||||
|
<button key={t} type="button" className={`res-chip${activeTag === t ? ' res-chip--on' : ''}`} onClick={() => setActiveTag(prev => (prev === t ? null : t))}>{t}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{shownResources.length > 0 ? (
|
||||||
|
<div className="res-grid">
|
||||||
|
{shownResources.map(r => (
|
||||||
|
<Link key={r.id} to={downloadPath(r.id)} className="res-card">
|
||||||
|
{r.imageUrl && <img src={r.imageUrl} alt={r.label} className="res-card-img" loading="lazy" />}
|
||||||
|
<div className="res-card-body">
|
||||||
|
<span className="res-title">{r.label}</span>
|
||||||
|
{r.description && <p className="res-desc res-clamped" style={{ WebkitLineClamp: 3 }}>{r.description}</p>}
|
||||||
|
{(r.tags ?? []).length > 0 && <span className="res-card-tags">{(r.tags ?? []).join(' · ')}</span>}
|
||||||
|
<span className="res-action">Open download →</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
resources.length > 0 && <p className="res-empty">No downloads tagged "{activeTag}".</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'books' && (
|
||||||
|
<div className="res-grid">
|
||||||
|
{books.map(book => (
|
||||||
|
<div key={book.id} className="res-book">
|
||||||
|
{book.imageUrl && <img src={book.imageUrl} alt={book.label} className="res-book-cover" loading="lazy" />}
|
||||||
|
<div className="res-book-body">
|
||||||
|
<span className="res-title">{book.label}</span>
|
||||||
|
{book.description && <ClampedText text={book.description} className="res-desc" lines={3} />}
|
||||||
|
<div className="res-book-actions">
|
||||||
|
{(book.amazonUrl || book.url) && (
|
||||||
|
<a href={book.amazonUrl || book.url} target="_blank" rel="noopener noreferrer" className="btn-primary res-btn">
|
||||||
|
{book.amazonUrl ? (book.amazonLabel || 'View on Amazon') : (book.amazonLabel || 'Learn More')}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{book.appleBooksUrl && (
|
||||||
|
<a href={book.appleBooksUrl} target="_blank" rel="noopener noreferrer" className="btn-secondary res-btn">
|
||||||
|
{book.appleBooksLabel || 'Apple Books'}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'recommended' && (
|
||||||
|
<div className="res-grid">
|
||||||
|
{recommended.map(item => (
|
||||||
|
<a key={item.id} href={item.url} target="_blank" rel="noopener noreferrer" className="res-card">
|
||||||
|
{item.imageUrl && <img src={item.imageUrl} alt={item.label} className="res-card-img" loading="lazy" />}
|
||||||
|
<div className="res-card-body">
|
||||||
|
<span className="res-title">{item.label}</span>
|
||||||
|
{item.description && <p className="res-desc res-clamped" style={{ WebkitLineClamp: 3 }}>{item.description}</p>}
|
||||||
|
<span className="res-action">Visit →</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user