diff --git a/package.json b/package.json index 53cb37b..a6b9967 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.8.1", + "version": "2.8.2", "type": "module", "scripts": { "dev": "vite", diff --git a/server/helpers.js b/server/helpers.js index 9245900..d0d704b 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -268,6 +268,7 @@ function sanitizeColossiansStudySections(value) { chapter: Number.isInteger(item.chapter) && item.chapter >= 1 && item.chapter <= 999 ? item.chapter : 1, reference: typeof item.reference === 'string' ? item.reference.trim().slice(0, 40) : '', title: typeof item.title === 'string' ? item.title.trim().slice(0, 160) : '', + episodeNumber: typeof item.episodeNumber === 'string' || typeof item.episodeNumber === 'number' ? String(item.episodeNumber).trim().slice(0, 20) : '', audioEmbedUrl: sanitizeUrl(item.audioEmbedUrl), passageText: typeof item.passageText === 'string' ? item.passageText.trim().slice(0, 12000) : '', summary: typeof item.summary === 'string' ? item.summary.trim().slice(0, 600) : '', diff --git a/server/study-helpers.js b/server/study-helpers.js index 5465537..d88218c 100644 --- a/server/study-helpers.js +++ b/server/study-helpers.js @@ -312,9 +312,11 @@ export function isSectionReleased(section) { export function redactUnreleasedSection(section) { if (!section || typeof section !== 'object') return section - if (isSectionReleased(section)) return section + // episodeNumber is an admin-only reference and is never sent to the public site + const { episodeNumber: _episodeNumber, ...publicSection } = section + if (isSectionReleased(section)) return publicSection return { - ...section, + ...publicSection, passageText: '', commentary: '', greekNotes: [], diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index 2032ad6..d2d4253 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -14,22 +14,43 @@ import InboxPanel, { type InboxSubmission } from './InboxPanel' import AssetManager, { type AdminAsset } from './AssetManager' +function toLocalInputValue(iso?: string): string { + if (!iso) return '' + const d = new Date(iso) + if (isNaN(d.getTime())) return '' + const pad = (n: number) => String(n).padStart(2, '0') + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}` +} + +function fromLocalInputValue(val: string): string { + return val ? new Date(val).toISOString() : '' +} + interface SortableLessonRowProps { section: ColossiansStudySection study: StudyProgram onEdit: () => void removeStudySection: (studyId: string, sectionId: string) => void + onReleaseChange: (value: string) => void } -function SortableLessonRow({ section, study, onEdit, removeStudySection }: SortableLessonRowProps) { +function SortableLessonRow({ section, study, onEdit, removeStudySection, onReleaseChange }: SortableLessonRowProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: section.id }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.4 : 1 } const isLive = section.releasedAt ? new Date(section.releasedAt) <= new Date() : false return (
⠿ + {section.episodeNumber || '—'} {section.reference || '—'} {section.title || Untitled} + onReleaseChange(fromLocalInputValue(e.target.value))} + /> {isLive ? 'Live' : 'Draft'}
@@ -5043,8 +5064,10 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
+ Ep # Reference Title + Release Status
@@ -5075,6 +5098,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date()) study={activeStudy} onEdit={() => setSelectedLessonId(section.id)} removeStudySection={removeStudySection} + onReleaseChange={value => updateStudySection(activeStudy.id, section.id, 'releasedAt', value)} /> ))} @@ -5109,6 +5133,10 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
+
+ + updateStudySection(activeStudy.id, activeLesson.id, 'episodeNumber', e.target.value)} /> +
{ - const val = e.target.value - updateStudySection(activeStudy.id, activeLesson.id, 'releasedAt', val ? new Date(val + ':00Z').toISOString() : '') - }} /> + updateStudySection(activeStudy.id, activeLesson.id, 'releasedAt', fromLocalInputValue(e.target.value))} />
diff --git a/src/App.css b/src/App.css index 4cfe085..e009d46 100644 --- a/src/App.css +++ b/src/App.css @@ -6428,6 +6428,14 @@ } .study-lesson-drag:active { cursor: grabbing; } +.study-lesson-ep { + width: 44px; + flex-shrink: 0; + font-size: 0.82rem; + color: var(--admin-muted); + font-variant-numeric: tabular-nums; +} + .study-lesson-ref { width: 80px; flex-shrink: 0; @@ -6436,6 +6444,19 @@ font-variant-numeric: tabular-nums; } +.study-lesson-release { + width: 190px; + flex-shrink: 0; + font-size: 0.8rem; + font-family: inherit; + color: var(--admin-text); + background: transparent; + border: 1px solid var(--admin-card-border); + border-radius: 4px; + padding: 0.2rem 0.4rem; + color-scheme: dark; +} + .study-lesson-title { flex: 1; font-size: 0.88rem; diff --git a/src/legacyColossiansData.ts b/src/legacyColossiansData.ts index 71b9f8e..4578d4c 100644 --- a/src/legacyColossiansData.ts +++ b/src/legacyColossiansData.ts @@ -14,6 +14,7 @@ export type ColossiansStudySection = { checkpointPrompt?: string checkpointQuestions?: string[] releasedAt?: string + episodeNumber?: string } function section(