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}