import { Resend } from 'resend' import { state } from './state.js' import { queuePodcastChecklistWrite, queueCalendarEventsWrite, queueStudyUsersWrite } from './data.js' import { DEFAULT_RESEND_FROM, DEFAULT_RESEND_TO } from './config.js' import { celebrationDateForYear } from './helpers.js' function toDateKey(date) { return date.toISOString().slice(0, 10) } export function startReminderScheduler() { checkReminders() setInterval(checkReminders, 60 * 60 * 1000) } async function checkReminders() { const todayKey = toDateKey(new Date()) // Episodes const episodes = state.podcastChecklist?.episodes if (Array.isArray(episodes)) { let changed = false for (const ep of episodes) { if (!ep.datePublished || !(ep.reminderDays > 0) || ep.reminderSentAt) continue const publish = new Date(ep.datePublished + 'T12:00:00Z') if (isNaN(publish.getTime())) continue const reminderDate = new Date(publish) reminderDate.setUTCDate(reminderDate.getUTCDate() - ep.reminderDays) if (todayKey === toDateKey(reminderDate)) { const sent = await sendReminderEmail({ label: ep.episodeNumber ? `Episode ${ep.episodeNumber}` : 'Episode', title: ep.title || 'Untitled', series: ep.series, date: ep.datePublished, reminderDays: ep.reminderDays, type: 'episode', }) if (sent) { ep.reminderSentAt = new Date().toISOString(); changed = true } } } if (changed) queuePodcastChecklistWrite() } // Calendar events const events = state.calendarEvents if (Array.isArray(events)) { let changed = false for (const ev of events) { if (!ev.date || !(ev.reminderDays > 0) || ev.reminderSentAt) continue const publish = new Date(ev.date + 'T12:00:00Z') if (isNaN(publish.getTime())) continue const reminderDate = new Date(publish) reminderDate.setUTCDate(reminderDate.getUTCDate() - ev.reminderDays) if (todayKey === toDateKey(reminderDate)) { const sent = await sendReminderEmail({ label: ev.type.charAt(0).toUpperCase() + ev.type.slice(1), title: ev.title, series: null, date: ev.date, reminderDays: ev.reminderDays, type: ev.type, }) if (sent) { ev.reminderSentAt = new Date().toISOString(); changed = true } } } if (changed) queueCalendarEventsWrite() } // Birthdays — fire on the student's celebration day, once per calendar year const users = state.studyUsers ?? [] let birthdayUsersChanged = false const todayYear = new Date().getUTCFullYear() const birthdayKids = [] for (const user of users) { if (!user.birthdayMonth || !user.birthdayDay) continue const celebDate = celebrationDateForYear(user, todayYear) if (!celebDate) continue if (todayKey !== celebDate) continue if (user.birthdayEmailSentYear === todayYear) continue birthdayKids.push({ name: user.displayName || user.username, birthdayMonth: user.birthdayMonth, birthdayDay: user.birthdayDay, hasAlternate: Boolean(user.birthdayAlternateMonth) }) user.birthdayEmailSentYear = todayYear birthdayUsersChanged = true } if (birthdayKids.length > 0) { await sendBirthdayNotificationEmail(birthdayKids) if (birthdayUsersChanged) queueStudyUsersWrite() } } async function sendBirthdayNotificationEmail(kids) { if (!process.env.RESEND_API_KEY) return try { const resend = new Resend(process.env.RESEND_API_KEY) const from = process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM const to = process.env.RESEND_TO ?? DEFAULT_RESEND_TO const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'] const lines = kids.map(k => { const bdayStr = `${MONTHS[k.birthdayMonth - 1]} ${k.birthdayDay}` const alt = k.hasAlternate ? ' (celebrating on alternate date)' : '' return `
  • ${k.name} — birthday: ${bdayStr}${alt}
  • ` }).join('') const subject = kids.length === 1 ? `🎂 Birthday today: ${kids[0].name}` : `🎂 ${kids.length} student birthdays today` const html = `

    🎂 Student Birthday Celebration

    The following student${kids.length > 1 ? 's have' : ' has'} a birthday celebration today — consider sending a special message or acknowledging them in your study community!


    To edit birthday info, open the Study Users panel in your admin settings.

    ` const { error } = await resend.emails.send({ from, to, subject, html }) if (error) console.error('[birthday] send error:', error) else console.log(`[birthday] notification sent for ${kids.map(k => k.name).join(', ')}`) } catch (err) { console.error('[birthday] send exception:', err) } } const TYPE_ICONS = { episode: '📅', general: '📌', recording: '🎙️', social: '📱', task: '✅' } async function sendReminderEmail({ label, title, series, date, reminderDays, type }) { if (!process.env.RESEND_API_KEY) return false try { const resend = new Resend(process.env.RESEND_API_KEY) const from = process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM const to = process.env.RESEND_TO ?? DEFAULT_RESEND_TO const eventDate = new Date(date + 'T12:00:00Z') const msLeft = eventDate.getTime() - Date.now() const daysLeft = Math.max(0, Math.ceil(msLeft / (1000 * 60 * 60 * 24))) const seriesStr = series ? ` (${series})` : '' const daysText = daysLeft === 0 ? 'today' : daysLeft === 1 ? 'in 1 day' : `in ${daysLeft} days` const icon = TYPE_ICONS[type] ?? '📅' const subject = `Reminder: ${label}: ${title} — ${daysText}` const html = `

    ${icon} Calendar Reminder

    ${label}: ${title}${seriesStr}
    Scheduled for ${date} — ${daysText}


    This reminder was set ${reminderDays} day${reminderDays === 1 ? '' : 's'} before the date. To change or remove it, open the Calendar in your admin panel.

    ` const { error } = await resend.emails.send({ from, to, subject, html }) if (error) { console.error('[reminder] send error:', error); return false } console.log(`[reminder] sent for "${title}" (${date})`) return true } catch (err) { console.error('[reminder] send exception:', err) return false } }