Add scheduled series auto-launch for series card, Start Here, and study guide; v2.0.36
Publish Container / docker (push) Successful in 14m20s
Publish Container / docker (push) Successful in 14m20s
One scheduled date/time in the Current Series admin panel archives the active series to Finished Books and atomically swaps in new content for the series card, Start Here steps, and Companion Study Guide. Server checks every hour via setInterval; also runs on restart so the transition fires even if the server was down at the scheduled moment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
06f89d6436
commit
f254e3ec19
5 files changed
+252
-2
No files matched your search
@@ -4014,6 +4014,150 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Schedule Auto-Transition */}
|
||||
{(() => {
|
||||
const sched = form.scheduledSeriesTransition
|
||||
const patch = (p: Partial<NonNullable<typeof sched>>) =>
|
||||
setForm(f => ({ ...f, scheduledSeriesTransition: { ...(f.scheduledSeriesTransition ?? { triggersAt: '', newTitle: '', newSeason: 2 }), ...p } }))
|
||||
return (
|
||||
<div style={{ marginTop: '2rem' }}>
|
||||
<hr style={{ border: 'none', borderTop: '1px solid #2a2518', marginBottom: '1.5rem' }} />
|
||||
<h3 style={{ marginBottom: '0.35rem', fontSize: '1rem' }}>Schedule Auto Series Launch</h3>
|
||||
<p className="admin-stats-note" style={{ marginBottom: '1rem' }}>
|
||||
Set one date & time to automatically archive the current series and swap in new content for the Series card, Start Here section, and Companion Study Guide. The server checks every hour. Leave optional fields blank to keep the current values unchanged.
|
||||
</p>
|
||||
{sched?.triggersAt && (
|
||||
<div style={{ background: 'rgba(201,168,76,0.08)', border: '1px solid rgba(201,168,76,0.25)', borderRadius: '10px', padding: '0.8rem 1rem', marginBottom: '1rem' }}>
|
||||
<p style={{ margin: 0, fontSize: '0.88rem', color: '#c9a84c' }}>
|
||||
<strong>Scheduled:</strong> Launch “{sched.newTitle}” on{' '}
|
||||
{new Date(sched.triggersAt).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-triggers-at">Launch Date & Time</label>
|
||||
<input
|
||||
id="sched-triggers-at"
|
||||
type="datetime-local"
|
||||
value={sched?.triggersAt ? new Date(sched.triggersAt).toISOString().slice(0, 16) : ''}
|
||||
onChange={e => patch({ triggersAt: e.target.value ? new Date(e.target.value).toISOString() : '' })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: '0.78rem', fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#7a7060', margin: '1.25rem 0 0.6rem' }}>New Series Card</p>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-new-title">Series Title</label>
|
||||
<input id="sched-new-title" type="text" placeholder="e.g. Study of Romans" value={sched?.newTitle ?? ''} onChange={e => patch({ newTitle: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-new-desc">Series Description</label>
|
||||
<textarea id="sched-new-desc" rows={3} placeholder="Brief description of the new series…" value={sched?.newDescription ?? ''} onChange={e => patch({ newDescription: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-new-image">Series Image URL</label>
|
||||
<input id="sched-new-image" type="text" placeholder="/images/romans-cover.png" value={sched?.newImageUrl ?? ''} onChange={e => patch({ newImageUrl: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-new-season">New Season Number</label>
|
||||
<input id="sched-new-season" type="number" min="1" placeholder="2" value={sched?.newSeason ?? ''} onChange={e => patch({ newSeason: parseInt(e.target.value) || 2 })} />
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: '0.78rem', fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#7a7060', margin: '1.25rem 0 0.6rem' }}>Start Here Section <span style={{ fontWeight: 400, textTransform: 'none' }}>(optional)</span></p>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sh-heading">Heading</label>
|
||||
<input id="sched-sh-heading" type="text" placeholder="New Here? Start Here" value={sched?.newStartHereHeading ?? ''} onChange={e => patch({ newStartHereHeading: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sh-intro">Intro</label>
|
||||
<textarea id="sched-sh-intro" rows={2} placeholder="Intro paragraph…" value={sched?.newStartHereIntro ?? ''} onChange={e => patch({ newStartHereIntro: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sh-s1t">Step 1 Title</label>
|
||||
<input id="sched-sh-s1t" type="text" value={sched?.newStartHereStep1Title ?? ''} onChange={e => patch({ newStartHereStep1Title: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sh-s1b">Step 1 Body</label>
|
||||
<textarea id="sched-sh-s1b" rows={2} value={sched?.newStartHereStep1Body ?? ''} onChange={e => patch({ newStartHereStep1Body: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sh-s1c">Step 1 Button Text</label>
|
||||
<input id="sched-sh-s1c" type="text" value={sched?.newStartHereStep1Cta ?? ''} onChange={e => patch({ newStartHereStep1Cta: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sh-s2t">Step 2 Title</label>
|
||||
<input id="sched-sh-s2t" type="text" value={sched?.newStartHereStep2Title ?? ''} onChange={e => patch({ newStartHereStep2Title: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sh-s2b">Step 2 Body</label>
|
||||
<textarea id="sched-sh-s2b" rows={2} value={sched?.newStartHereStep2Body ?? ''} onChange={e => patch({ newStartHereStep2Body: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sh-s2c">Step 2 Button Text</label>
|
||||
<input id="sched-sh-s2c" type="text" value={sched?.newStartHereStep2Cta ?? ''} onChange={e => patch({ newStartHereStep2Cta: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sh-s3t">Step 3 Title</label>
|
||||
<input id="sched-sh-s3t" type="text" value={sched?.newStartHereStep3Title ?? ''} onChange={e => patch({ newStartHereStep3Title: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sh-s3b">Step 3 Body</label>
|
||||
<textarea id="sched-sh-s3b" rows={2} value={sched?.newStartHereStep3Body ?? ''} onChange={e => patch({ newStartHereStep3Body: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sh-s3c">Step 3 Button Text</label>
|
||||
<input id="sched-sh-s3c" type="text" value={sched?.newStartHereStep3Cta ?? ''} onChange={e => patch({ newStartHereStep3Cta: e.target.value })} />
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: '0.78rem', fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#7a7060', margin: '1.25rem 0 0.6rem' }}>Companion Study Guide <span style={{ fontWeight: 400, textTransform: 'none' }}>(optional)</span></p>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sg-title">Guide Title</label>
|
||||
<input id="sched-sg-title" type="text" placeholder="Companion Study Guide" value={sched?.newStudyGuideTitle ?? ''} onChange={e => patch({ newStudyGuideTitle: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sg-desc">Guide Description</label>
|
||||
<textarea id="sched-sg-desc" rows={3} value={sched?.newStudyGuideDescription ?? ''} onChange={e => patch({ newStudyGuideDescription: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sg-download">Download URL</label>
|
||||
<input id="sched-sg-download" type="url" placeholder="/uploads/guide.pdf" value={sched?.newStudyGuideDownloadUrl ?? ''} onChange={e => patch({ newStudyGuideDownloadUrl: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sg-amazon">Amazon URL</label>
|
||||
<input id="sched-sg-amazon" type="url" placeholder="https://amazon.com/…" value={sched?.newStudyGuideUrl ?? ''} onChange={e => patch({ newStudyGuideUrl: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sg-amazon-label">Amazon Button Label</label>
|
||||
<input id="sched-sg-amazon-label" type="text" placeholder="Get it on Amazon" value={sched?.newStudyGuideAmazonButtonLabel ?? ''} onChange={e => patch({ newStudyGuideAmazonButtonLabel: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sg-apple-url">Apple Books URL</label>
|
||||
<input id="sched-sg-apple-url" type="url" placeholder="https://books.apple.com/…" value={sched?.newStudyGuideAppleBooksUrl ?? ''} onChange={e => patch({ newStudyGuideAppleBooksUrl: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="sched-sg-apple-label">Apple Books Button Label</label>
|
||||
<input id="sched-sg-apple-label" type="text" placeholder="Get it on Apple Books" value={sched?.newStudyGuideAppleBooksLabel ?? ''} onChange={e => patch({ newStudyGuideAppleBooksLabel: e.target.value })} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.75rem', marginTop: '1rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary"
|
||||
disabled={!sched?.triggersAt || !sched?.newTitle?.trim()}
|
||||
onClick={() => handleSaveDraft()}
|
||||
>Save Schedule</button>
|
||||
{sched?.triggersAt && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-danger-outline"
|
||||
onClick={() => setForm(f => ({ ...f, scheduledSeriesTransition: undefined }))}
|
||||
>Clear Schedule</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user