diff --git a/package-lock.json b/package-lock.json
index 344e022..ca753a4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "siteforge",
- "version": "2.34.0",
+ "version": "2.35.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "siteforge",
- "version": "2.34.0",
+ "version": "2.35.0",
"dependencies": {
"@aws-sdk/client-s3": "^3.1135.0",
"@dnd-kit/core": "^6.3.1",
diff --git a/package.json b/package.json
index 92c49ed..9506112 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "siteforge",
"private": true,
- "version": "2.34.0",
+ "version": "2.35.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/server/helpers.js b/server/helpers.js
index 0b66321..8e04c5c 100644
--- a/server/helpers.js
+++ b/server/helpers.js
@@ -344,6 +344,7 @@ function sanitizeStudies(value) {
showOnHomepage: item.showOnHomepage === true,
showNewTag: item.showNewTag === true,
newTagLabel: typeof item.newTagLabel === 'string' ? item.newTagLabel.trim().slice(0, 24) : '',
+ guideSource: typeof item.guideSource === 'string' && /^(none|primary|second|book:[A-Za-z0-9_-]{1,80})$/.test(item.guideSource.trim()) ? item.guideSource.trim() : '',
status: item.status === 'planned' ? 'planned' : (item.status === 'complete' ? 'complete' : 'active'),
lane: item.lane === 'A' || item.lane === 'B' ? item.lane : '',
startsAt: sanitizeIsoDate(item.startsAt),
diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx
index ad2a768..0fd115c 100644
--- a/src/AdminPage.tsx
+++ b/src/AdminPage.tsx
@@ -6013,6 +6013,19 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
updateStudyProgram(activeStudy.id, 'numberOfChapters', Number(e.target.value) || 1)} />
+
+
+
+
Adds a “Download Study Guide” button to this study’s page. Visitors enter their name and email to get the file.
+
updateStudyProgram(activeStudy.id, 'showOnHomepage', e.target.checked)} />
diff --git a/src/content.ts b/src/content.ts
index 5eeae40..815a5fb 100644
--- a/src/content.ts
+++ b/src/content.ts
@@ -14,6 +14,9 @@ export interface StudyProgram {
showOnHomepage?: boolean
showNewTag?: boolean
newTagLabel?: string
+ // Which companion guide the study offers for download: '' = automatic (the guide of the series or
+ // finished book linked to this study), 'none', 'primary', 'second', or 'book:'
+ guideSource?: string
// 'complete' = finished and self-paced (still open for enrollment). A study whose lessons
// have all been released is treated as complete even if left as 'active'.
status: 'active' | 'planned' | 'complete'
@@ -700,3 +703,39 @@ export function getAllHighlights(content: Pick book.highlights ?? []),
]
}
+
+export interface StudyGuideLink {
+ // Page id under /downloads/ (primary-guide, second-guide, book-guide--)
+ pageId: string
+ title: string
+}
+
+// The companion guide a study offers, either chosen explicitly in admin or found through the
+// series / finished book that is linked to the study.
+export function getStudyGuideLink(
+ content: Pick,
+ study: Pick,
+): StudyGuideLink | null {
+ const source = study.guideSource ?? ''
+ if (source === 'none') return null
+
+ const primary = (): StudyGuideLink | null =>
+ content.studyGuideTitle && (content.studyGuideUrl || content.studyGuideDownloadUrl || content.studyGuideAppleBooksUrl)
+ ? { pageId: 'primary-guide', title: content.studyGuideTitle }
+ : null
+ const second = (): StudyGuideLink | null =>
+ content.secondSeries?.guide?.title ? { pageId: 'second-guide', title: content.secondSeries.guide.title } : null
+ const book = (id: string): StudyGuideLink | null => {
+ const found = (content.finishedBooks ?? []).find(b => b.id === id)
+ return found?.guide?.title ? { pageId: `book-guide--${found.id}`, title: found.guide.title } : null
+ }
+
+ if (source === 'primary') return primary()
+ if (source === 'second') return second()
+ if (source.startsWith('book:')) return book(source.slice('book:'.length))
+
+ if (content.seriesStudySlug && content.seriesStudySlug === study.slug) return primary()
+ if (content.secondSeries?.studySlug && content.secondSeries.studySlug === study.slug) return second()
+ const linkedBook = (content.finishedBooks ?? []).find(b => b.studySlug === study.slug && b.guide?.title)
+ return linkedBook ? book(linkedBook.id) : null
+}
diff --git a/src/studyPages.tsx b/src/studyPages.tsx
index a935a83..8f987c0 100644
--- a/src/studyPages.tsx
+++ b/src/studyPages.tsx
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState, useCallback } from 'react'
import DOMPurify from 'dompurify'
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
import type { SiteContent, StudyProgram, StudySection } from './content'
+import { getStudyGuideLink } from './content'
import { DEFAULT_COLOSSIANS_STUDY_SECTIONS } from './legacyColossiansData'
import { usePageMeta } from './hooks/usePageMeta'
import { Breadcrumbs } from './components/Breadcrumbs'
@@ -1704,6 +1705,8 @@ export function ColossiansStudyIndexPage({ content }: Props) {
}
}
+ const studyGuide = getStudyGuideLink(content, study)
+
return (
@@ -1740,6 +1743,7 @@ export function ColossiansStudyIndexPage({ content }: Props) {
{enrolling ? 'Enrolling...' : 'Enroll in This Study'}
)}
+ {studyGuide && 📄 Download Study Guide}
{allLessonsDone && 🏅 My Certificate}
{enrolled && My Notes}
{enrolled && studyStatus !== 'planned' && Community}