Add email reminders to calendar episodes; v1.1.15

- Per-episode reminder select (1d/2d/3d/1wk/2wk before publish date)
- Server checks hourly; fires Resend email to admin on reminder day
- reminderSentAt persisted so reminders don't re-fire; cleared if date or setting changes
- Bell indicator on calendar chips; sent confirmation in edit popover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-07-28 11:12:17 -04:00
parent 0161a0dacd
commit de201ff356
5 changed files with 150 additions and 5 deletions
+14
View File
@@ -10575,6 +10575,20 @@
.cal-ep-chip-title { color: #4a8a58; }
.cal-ep-bell {
font-size: 0.6rem;
margin-left: 2px;
opacity: 0.75;
vertical-align: middle;
}
.cal-reminder-sent {
font-size: 0.72rem;
color: #4ade80;
margin-top: 0.25rem;
display: block;
}
.cal-sidebar {
width: 220px;
flex-shrink: 0;
+44 -4
View File
@@ -17,8 +17,19 @@ interface PodcastChecklistEpisode {
datePublished: string
expanded: boolean
tasks: Record<string, boolean>
reminderDays?: number // 0 = none; 1/2/3/7/14 = days before publish
reminderSentAt?: string // ISO timestamp set after reminder fires
}
const REMINDER_OPTIONS = [
{ value: '0', label: 'No reminder' },
{ value: '1', label: '1 day before' },
{ value: '2', label: '2 days before' },
{ value: '3', label: '3 days before' },
{ value: '7', label: '1 week before' },
{ value: '14', label: '2 weeks before' },
]
interface PodcastChecklistData {
tasks: PodcastChecklistTask[]
episodes: PodcastChecklistEpisode[]
@@ -149,9 +160,9 @@ function CalendarClient() {
const [viewMonth, setViewMonth] = useState(today.getMonth())
const [selectedEpisodeId, setSelectedEpisodeId] = useState<string | null>(null)
const [editEp, setEditEp] = useState<PodcastChecklistEpisode | null>(null)
const [editForm, setEditForm] = useState({ series: '', episodeNumber: '', title: '', datePublished: '' })
const [editForm, setEditForm] = useState({ series: '', episodeNumber: '', title: '', datePublished: '', reminderDays: '0' })
const [newEpOpen, setNewEpOpen] = useState(false)
const [newForm, setNewForm] = useState({ series: '', episodeNumber: '', title: '', datePublished: '' })
const [newForm, setNewForm] = useState({ series: '', episodeNumber: '', title: '', datePublished: '', reminderDays: '0' })
const [newBusy, setNewBusy] = useState(false)
const load = useCallback(async () => {
@@ -247,11 +258,16 @@ function CalendarClient() {
episodeNumber: ep.episodeNumber != null ? String(ep.episodeNumber) : '',
title: ep.title,
datePublished: ep.datePublished?.trim() ?? '',
reminderDays: String(ep.reminderDays ?? 0),
})
}
function saveEdit() {
if (!editEp || !checklist) return
const newDate = editForm.datePublished.trim()
const newReminder = Number(editForm.reminderDays)
const dateChanged = newDate !== (editEp.datePublished?.trim() ?? '')
const reminderChanged = newReminder !== (editEp.reminderDays ?? 0)
const updated = checklist.episodes.map(ep =>
ep.id === editEp.id
? {
@@ -259,7 +275,10 @@ function CalendarClient() {
series: editForm.series.trim(),
episodeNumber: editForm.episodeNumber.trim() ? Number(editForm.episodeNumber) : null,
title: editForm.title.trim(),
datePublished: editForm.datePublished.trim(),
datePublished: newDate,
reminderDays: newReminder,
// Re-arm if date or reminder setting changed
reminderSentAt: dateChanged || reminderChanged ? undefined : ep.reminderSentAt,
}
: ep
)
@@ -289,12 +308,13 @@ function CalendarClient() {
episodeNumber: newForm.episodeNumber.trim() ? Number(newForm.episodeNumber) : null,
title: newForm.title.trim(),
datePublished: newForm.datePublished.trim(),
reminderDays: Number(newForm.reminderDays),
expanded: false,
tasks: {},
}
const updated = [...checklist.episodes, ep]
await save(updated)
setNewForm({ series: '', episodeNumber: '', title: '', datePublished: '' })
setNewForm({ series: '', episodeNumber: '', title: '', datePublished: '', reminderDays: '0' })
setNewEpOpen(false)
setNewBusy(false)
}
@@ -355,6 +375,12 @@ function CalendarClient() {
Date
<input className="ct-input" type="date" value={newForm.datePublished} onChange={e => setNewForm(f => ({ ...f, datePublished: e.target.value }))} />
</label>
<label className="cal-new-ep-label">
Email Reminder
<select className="ct-input" value={newForm.reminderDays} onChange={e => setNewForm(f => ({ ...f, reminderDays: e.target.value }))}>
{REMINDER_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</label>
</div>
<div className="cal-new-ep-actions">
<button type="submit" className="em-btn em-btn--primary em-btn--sm" disabled={newBusy}>
@@ -405,6 +431,7 @@ function CalendarClient() {
>
{ep.episodeNumber ? `Ep ${ep.episodeNumber}` : ep.series?.slice(0, 6) ?? '—'}
{ep.title && <span className="cal-ep-chip-title"> {ep.title.slice(0, 18)}{ep.title.length > 18 ? '…' : ''}</span>}
{(ep.reminderDays ?? 0) > 0 && <span className="cal-ep-bell" title={`Reminder: ${ep.reminderDays}d before${ep.reminderSentAt ? ' (sent)' : ''}`}>{ep.reminderSentAt ? '✓' : '🔔'}</span>}
</button>
))}
</div>
@@ -506,6 +533,19 @@ function CalendarClient() {
onChange={e => setEditForm(f => ({ ...f, datePublished: e.target.value }))}
/>
</label>
<label className="cal-popover-label">
Email Reminder
<select
className="ct-input"
value={editForm.reminderDays}
onChange={e => setEditForm(f => ({ ...f, reminderDays: e.target.value }))}
>
{REMINDER_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
{editEp?.reminderSentAt && (
<span className="cal-reminder-sent">Reminder sent {new Date(editEp.reminderSentAt).toLocaleDateString()}</span>
)}
</label>
</div>
<div className="cal-popover-actions">
<button type="button" className="em-btn em-btn--primary em-btn--sm" onClick={saveEdit}>