Studies: add a Download Study Guide button to the study page, automatic from the linked series/book or chosen per study in admin; v2.35.0
Publish Container / build (push) Successful in 18m26s
Publish Container / build (push) Successful in 18m26s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b48a81a00c
commit
82ce312898
6 files changed
+60
-3
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "2.34.0",
|
"version": "2.35.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "2.34.0",
|
"version": "2.35.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.34.0",
|
"version": "2.35.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ function sanitizeStudies(value) {
|
|||||||
showOnHomepage: item.showOnHomepage === true,
|
showOnHomepage: item.showOnHomepage === true,
|
||||||
showNewTag: item.showNewTag === true,
|
showNewTag: item.showNewTag === true,
|
||||||
newTagLabel: typeof item.newTagLabel === 'string' ? item.newTagLabel.trim().slice(0, 24) : '',
|
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'),
|
status: item.status === 'planned' ? 'planned' : (item.status === 'complete' ? 'complete' : 'active'),
|
||||||
lane: item.lane === 'A' || item.lane === 'B' ? item.lane : '',
|
lane: item.lane === 'A' || item.lane === 'B' ? item.lane : '',
|
||||||
startsAt: sanitizeIsoDate(item.startsAt),
|
startsAt: sanitizeIsoDate(item.startsAt),
|
||||||
|
|||||||
@@ -6013,6 +6013,19 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
|||||||
<label htmlFor={`study-chapters-${activeStudy.id}`}>Number of Chapters</label>
|
<label htmlFor={`study-chapters-${activeStudy.id}`}>Number of Chapters</label>
|
||||||
<input id={`study-chapters-${activeStudy.id}`} type="number" min="1" max="999" value={activeStudy.numberOfChapters} placeholder="4" onChange={e => updateStudyProgram(activeStudy.id, 'numberOfChapters', Number(e.target.value) || 1)} />
|
<input id={`study-chapters-${activeStudy.id}`} type="number" min="1" max="999" value={activeStudy.numberOfChapters} placeholder="4" onChange={e => updateStudyProgram(activeStudy.id, 'numberOfChapters', Number(e.target.value) || 1)} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="admin-field">
|
||||||
|
<label htmlFor={`study-guide-source-${activeStudy.id}`}>Study Guide Download</label>
|
||||||
|
<select id={`study-guide-source-${activeStudy.id}`} value={activeStudy.guideSource ?? ''} onChange={e => updateStudyProgram(activeStudy.id, 'guideSource', e.target.value)}>
|
||||||
|
<option value="">Automatic (guide of the series or book linked to this study)</option>
|
||||||
|
<option value="none">None — don’t show a guide button</option>
|
||||||
|
{form.studyGuideTitle && <option value="primary">Current series guide: {form.studyGuideTitle}</option>}
|
||||||
|
{form.secondSeries?.guide?.title && <option value="second">Second series guide: {form.secondSeries.guide.title}</option>}
|
||||||
|
{(form.finishedBooks ?? []).filter(b => b.guide?.title).map(b => (
|
||||||
|
<option key={b.id} value={`book:${b.id}`}>{b.title}: {b.guide?.title}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<p className="admin-field-help">Adds a “Download Study Guide” button to this study’s page. Visitors enter their name and email to get the file.</p>
|
||||||
|
</div>
|
||||||
<div className="admin-field" style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
|
<div className="admin-field" style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
|
||||||
<input id={`study-home-feature-${activeStudy.id}`} type="checkbox" checked={activeStudy.showOnHomepage === true} onChange={e => updateStudyProgram(activeStudy.id, 'showOnHomepage', e.target.checked)} />
|
<input id={`study-home-feature-${activeStudy.id}`} type="checkbox" checked={activeStudy.showOnHomepage === true} onChange={e => updateStudyProgram(activeStudy.id, 'showOnHomepage', e.target.checked)} />
|
||||||
<label htmlFor={`study-home-feature-${activeStudy.id}`}>Feature this study card on homepage</label>
|
<label htmlFor={`study-home-feature-${activeStudy.id}`}>Feature this study card on homepage</label>
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ export interface StudyProgram {
|
|||||||
showOnHomepage?: boolean
|
showOnHomepage?: boolean
|
||||||
showNewTag?: boolean
|
showNewTag?: boolean
|
||||||
newTagLabel?: string
|
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:<finished book id>'
|
||||||
|
guideSource?: string
|
||||||
// 'complete' = finished and self-paced (still open for enrollment). A study whose lessons
|
// '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'.
|
// have all been released is treated as complete even if left as 'active'.
|
||||||
status: 'active' | 'planned' | 'complete'
|
status: 'active' | 'planned' | 'complete'
|
||||||
@@ -700,3 +703,39 @@ export function getAllHighlights(content: Pick<SiteContent, 'podcastFeaturedLink
|
|||||||
...(content.finishedBooks ?? []).flatMap(book => book.highlights ?? []),
|
...(content.finishedBooks ?? []).flatMap(book => book.highlights ?? []),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StudyGuideLink {
|
||||||
|
// Page id under /downloads/ (primary-guide, second-guide, book-guide--<id>)
|
||||||
|
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<SiteContent, 'seriesStudySlug' | 'studyGuideTitle' | 'studyGuideUrl' | 'studyGuideDownloadUrl' | 'studyGuideAppleBooksUrl' | 'secondSeries' | 'finishedBooks'>,
|
||||||
|
study: Pick<StudyProgram, 'slug' | 'guideSource'>,
|
||||||
|
): 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
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState, useCallback } from 'react'
|
|||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
|
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||||
import type { SiteContent, StudyProgram, StudySection } from './content'
|
import type { SiteContent, StudyProgram, StudySection } from './content'
|
||||||
|
import { getStudyGuideLink } from './content'
|
||||||
import { DEFAULT_COLOSSIANS_STUDY_SECTIONS } from './legacyColossiansData'
|
import { DEFAULT_COLOSSIANS_STUDY_SECTIONS } from './legacyColossiansData'
|
||||||
import { usePageMeta } from './hooks/usePageMeta'
|
import { usePageMeta } from './hooks/usePageMeta'
|
||||||
import { Breadcrumbs } from './components/Breadcrumbs'
|
import { Breadcrumbs } from './components/Breadcrumbs'
|
||||||
@@ -1704,6 +1705,8 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const studyGuide = getStudyGuideLink(content, study)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="study-index-page" aria-label={`${study.title} study`}>
|
<main className="study-index-page" aria-label={`${study.title} study`}>
|
||||||
<section className="section-study-course-hero">
|
<section className="section-study-course-hero">
|
||||||
@@ -1740,6 +1743,7 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
|||||||
{enrolling ? 'Enrolling...' : 'Enroll in This Study'}
|
{enrolling ? 'Enrolling...' : 'Enroll in This Study'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{studyGuide && <Link to={`/downloads/${studyGuide.pageId}`} className="btn-secondary">📄 Download Study Guide</Link>}
|
||||||
{allLessonsDone && <Link to="/study/account" className="btn-secondary">🏅 My Certificate</Link>}
|
{allLessonsDone && <Link to="/study/account" className="btn-secondary">🏅 My Certificate</Link>}
|
||||||
{enrolled && <Link to={`/study/${study.slug}/notes`} className="btn-secondary">My Notes</Link>}
|
{enrolled && <Link to={`/study/${study.slug}/notes`} className="btn-secondary">My Notes</Link>}
|
||||||
{enrolled && studyStatus !== 'planned' && <Link to={`/study/${study.slug}/community`} className="btn-secondary">Community</Link>}
|
{enrolled && studyStatus !== 'planned' && <Link to={`/study/${study.slug}/community`} className="btn-secondary">Community</Link>}
|
||||||
|
|||||||
Reference in new issue
Block a user