diff --git a/package.json b/package.json index f82d549..30b4ac0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "1.1.31", + "version": "1.1.30", "type": "module", "scripts": { "dev": "vite", diff --git a/server/helpers.js b/server/helpers.js index 2042dcf..434ea2a 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -121,51 +121,6 @@ export function sanitizeRedirectRules(value) { return out.length > 0 ? out : DEFAULT_REDIRECT_RULES } -// Returns true if the user's celebration date falls within the current Mon–Sun week. -// Uses birthdayAlternateMonth/Day if set (for summer birthday workarounds), otherwise -// falls back to birthdayMonth/Day. Checks both the current year and next year so -// year-wrap birthdays (e.g. Dec 30 checked in late December) work correctly. -export function isBirthdayThisWeek(user) { - const month = user.birthdayMonth - const day = user.birthdayDay - if (!month || !day) return false - - const celebMonth = user.birthdayAlternateMonth ?? month - const celebDay = user.birthdayAlternateDay ?? day - - const today = new Date() - const monday = new Date(today) - monday.setUTCHours(0, 0, 0, 0) - const dow = monday.getUTCDay() - monday.setUTCDate(monday.getUTCDate() - (dow === 0 ? 6 : dow - 1)) - const sunday = new Date(monday) - sunday.setUTCDate(sunday.getUTCDate() + 6) - sunday.setUTCHours(23, 59, 59, 999) - - const yr = today.getUTCFullYear() - for (const year of [yr, yr + 1]) { - const bday = new Date(Date.UTC(year, celebMonth - 1, celebDay)) - if (bday >= monday && bday <= sunday) return true - } - return false -} - -// June, July, August are considered summer months. -export function isSummerBirthday(month) { - return Number.isInteger(month) && month >= 6 && month <= 8 -} - -// Returns "YYYY-MM-DD" for the celebration date (alternate if set, else birthday) -// in the given year. Returns null if no birthday is set. -export function celebrationDateForYear(user, year) { - const month = user.birthdayMonth - const day = user.birthdayDay - if (!month || !day) return null - const m = user.birthdayAlternateMonth ?? month - const d = user.birthdayAlternateDay ?? day - return `${year}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}` -} - export function sanitizeFeaturedLinks(value) { const source = Array.isArray(value) ? value : [] return source diff --git a/server/reminder.js b/server/reminder.js index cab55a9..373b8a4 100644 --- a/server/reminder.js +++ b/server/reminder.js @@ -1,8 +1,7 @@ import { Resend } from 'resend' import { state } from './state.js' -import { queuePodcastChecklistWrite, queueCalendarEventsWrite, queueStudyUsersWrite } from './data.js' +import { queuePodcastChecklistWrite, queueCalendarEventsWrite } 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) @@ -65,63 +64,6 @@ async function checkReminders() { } 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: 'βœ…' } diff --git a/server/routes/admin-assets.js b/server/routes/admin-assets.js index d60e566..0a48bca 100644 --- a/server/routes/admin-assets.js +++ b/server/routes/admin-assets.js @@ -1,6 +1,6 @@ import { mkdir, stat, unlink, writeFile } from 'node:fs/promises' import path from 'node:path' -import { inferImageExtensionFromDataUrl, normalizeAssetBaseName, isBirthdayThisWeek } from '../helpers.js' +import { inferImageExtensionFromDataUrl, normalizeAssetBaseName } from '../helpers.js' import { requireAdminAuth } from '../auth.js' import { UPLOADS_DIR } from '../config.js' import { state } from '../state.js' @@ -132,11 +132,6 @@ export function register(app) { noteCount, subscribeNewsletter: user.subscribeNewsletter !== false, studyRemindersEnabled: user.studyRemindersEnabled === true, - birthdayMonth: user.birthdayMonth ?? null, - birthdayDay: user.birthdayDay ?? null, - birthdayAlternateMonth: user.birthdayAlternateMonth ?? null, - birthdayAlternateDay: user.birthdayAlternateDay ?? null, - isBirthdayWeek: isBirthdayThisWeek(user), } })) @@ -147,7 +142,7 @@ export function register(app) { const user = state.studyUsers.find(u => u.id === req.params.id) if (!user) { res.status(404).json({ message: 'User not found.' }); return } - const { displayName, newPassword, addEnrollment, removeEnrollment, birthdayMonth, birthdayDay, birthdayAlternateMonth, birthdayAlternateDay } = req.body ?? {} + const { displayName, newPassword, addEnrollment, removeEnrollment } = req.body ?? {} if (typeof displayName === 'string') { user.displayName = displayName.trim().slice(0, 80) @@ -177,28 +172,6 @@ export function register(app) { user.enrolledStudySlugs = (user.enrolledStudySlugs ?? []).filter(s => s !== slug) } - if (birthdayMonth !== undefined) { - const m = parseInt(birthdayMonth, 10) - user.birthdayMonth = (Number.isInteger(m) && m >= 1 && m <= 12) ? m : null - } - if (birthdayDay !== undefined) { - const d = parseInt(birthdayDay, 10) - user.birthdayDay = (Number.isInteger(d) && d >= 1 && d <= 31) ? d : null - } - if (birthdayAlternateMonth !== undefined) { - const m = parseInt(birthdayAlternateMonth, 10) - user.birthdayAlternateMonth = (Number.isInteger(m) && m >= 1 && m <= 12) ? m : null - if (!user.birthdayAlternateMonth) user.birthdayAlternateDay = null - } - if (birthdayAlternateDay !== undefined) { - const d = parseInt(birthdayAlternateDay, 10) - user.birthdayAlternateDay = (Number.isInteger(d) && d >= 1 && d <= 31) ? d : null - } - // Clear the sent-year so a changed birthday can trigger a new notification - if (birthdayMonth !== undefined || birthdayDay !== undefined || birthdayAlternateMonth !== undefined || birthdayAlternateDay !== undefined) { - user.birthdayEmailSentYear = null - } - user.updatedAt = new Date().toISOString() queueStudyUsersWrite() res.json({ ok: true, displayName: user.displayName, enrolledStudySlugs: user.enrolledStudySlugs }) diff --git a/server/routes/study-auth.js b/server/routes/study-auth.js index fb0b3fa..2c2a12b 100644 --- a/server/routes/study-auth.js +++ b/server/routes/study-auth.js @@ -6,7 +6,7 @@ import { verifyTotpCode, generateRecoveryCodes, } from '../auth.js' -import { parseCookies, isBirthdayThisWeek } from '../helpers.js' +import { parseCookies } from '../helpers.js' import { STUDY_SESSION_COOKIE, MAX_STUDY_USERS, MAX_CONTACT_SUBMISSIONS } from '../config.js' import { state } from '../state.js' import { @@ -57,7 +57,6 @@ export function register(app) { totpEnabled: Boolean(user && (user.twoFaMethod === 'app' || user.twoFaMethod === 'email') && (user.twoFaMethod === 'email' || (user.totpSecret && user.totpVerified))), twoFaMethod: user?.twoFaMethod ?? null, totpRecoveryCodesRemaining: user?.twoFaMethod === 'app' ? (user.totpRecoveryCodes?.length ?? 0) : 0, - isBirthdayWeek: user ? isBirthdayThisWeek(user) : false, }) }) diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index 9312457..c3cb049 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -143,11 +143,6 @@ interface StudyUserRecord { noteCount: number subscribeNewsletter: boolean studyRemindersEnabled: boolean - birthdayMonth: number | null - birthdayDay: number | null - birthdayAlternateMonth: number | null - birthdayAlternateDay: number | null - isBirthdayWeek: boolean } function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) { @@ -159,8 +154,6 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) { const [newPassword, setNewPassword] = useState>({}) const [msg, setMsg] = useState>({}) const [confirmDeleteId, setConfirmDeleteId] = useState(null) - const [editBirthday, setEditBirthday] = useState>({}) - function flashMsg(id: string, text: string) { setMsg(prev => ({ ...prev, [id]: text })) @@ -218,27 +211,6 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) { } catch (e) { flashMsg(user.id, e instanceof Error ? e.message : 'Error') } } - async function handleSaveBirthday(user: StudyUserRecord) { - const b = editBirthday[user.id] ?? { month: String(user.birthdayMonth ?? ''), day: String(user.birthdayDay ?? ''), altMonth: String(user.birthdayAlternateMonth ?? ''), altDay: String(user.birthdayAlternateDay ?? '') } - const bMonth = b.month ? parseInt(b.month, 10) : null - const bDay = b.day ? parseInt(b.day, 10) : null - const altMonth = b.altMonth ? parseInt(b.altMonth, 10) : null - const altDay = b.altDay ? parseInt(b.altDay, 10) : null - if (bMonth !== null && (bMonth < 1 || bMonth > 12)) { flashMsg(user.id, 'Month must be 1–12.'); return } - if (bDay !== null && (bDay < 1 || bDay > 31)) { flashMsg(user.id, 'Day must be 1–31.'); return } - if ((altMonth == null) !== (altDay == null)) { flashMsg(user.id, 'Set both alternate month and day, or neither.'); return } - try { - await patchUser(user.id, { - birthdayMonth: bMonth !== null ? String(bMonth) : '', - birthdayDay: bDay !== null ? String(bDay) : '', - birthdayAlternateMonth: altMonth !== null ? String(altMonth) : '', - birthdayAlternateDay: altDay !== null ? String(altDay) : '', - }) - setUsers(prev => prev.map(u => u.id !== user.id ? u : { ...u, birthdayMonth: bMonth, birthdayDay: bDay, birthdayAlternateMonth: altMonth, birthdayAlternateDay: altDay })) - flashMsg(user.id, 'Birthday saved.') - } catch (e) { flashMsg(user.id, e instanceof Error ? e.message : 'Error') } - } - async function handleUnenroll(user: StudyUserRecord, slug: string) { try { await patchUser(user.id, { removeEnrollment: slug }) @@ -293,17 +265,6 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) { onClick={() => { setExpandedId(isExpanded ? null : user.id) setEditDisplayName(prev => ({ ...prev, [user.id]: user.displayName })) - if (!isExpanded) { - setEditBirthday(prev => ({ - ...prev, - [user.id]: { - month: user.birthdayMonth ? String(user.birthdayMonth) : '', - day: user.birthdayDay ? String(user.birthdayDay) : '', - altMonth: user.birthdayAlternateMonth ? String(user.birthdayAlternateMonth) : '', - altDay: user.birthdayAlternateDay ? String(user.birthdayAlternateDay) : '', - }, - })) - } }} >
    @@ -314,7 +275,6 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) { {user.enrolledStudies.length} enrolled {user.noteCount} notes {(user.currentStreak ?? 0) > 0 && {user.currentStreak} πŸ”₯} - {user.isBirthdayWeek && πŸŽ‚ Birthday Week!} {isExpanded ? 'β–²' : 'β–Ό'}
    @@ -345,60 +305,6 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) { - {/* Birthday */} - {(() => { - const b = editBirthday[user.id] ?? { month: String(user.birthdayMonth ?? ''), day: String(user.birthdayDay ?? ''), altMonth: String(user.birthdayAlternateMonth ?? ''), altDay: String(user.birthdayAlternateDay ?? '') } - const isSummer = (() => { const m = parseInt(b.month, 10); return Number.isInteger(m) && m >= 6 && m <= 8 })() - const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] - function bdayUpdate(field: string, val: string) { - setEditBirthday(prev => ({ ...prev, [user.id]: { ...(prev[user.id] ?? { month: '', day: '', altMonth: '', altDay: '' }), [field]: val } })) - } - return ( -
    -

    Birthday

    -

    Admin-only. Used for birthday celebration notifications and the student's birthday week banner.

    -
    - - - - {(b.month && b.day) && ( - - )} -
    - {isSummer && ( -
    -

    β˜€οΈ Summer birthday β€” this falls during summer break (June–August). Set an alternate school-year celebration date below so they still get recognized during the school year.

    -
    - - -
    - {(b.altMonth && b.altDay) &&

    Birthday banner and email will fire on {MONTHS[parseInt(b.altMonth, 10) - 1]} {b.altDay} instead of their summer birthday.

    } -
    - )} - {user.isBirthdayWeek && ( -

    πŸŽ‚ This student's celebration week is right now!

    - )} -
    - ) - })()} - {/* Password Reset */}

    Reset Password

    diff --git a/src/colossiansStudy.tsx b/src/colossiansStudy.tsx index 6fe5f15..c9dca17 100644 --- a/src/colossiansStudy.tsx +++ b/src/colossiansStudy.tsx @@ -23,7 +23,6 @@ type StudyAuthState = { totpEnabled?: boolean twoFaMethod?: 'app' | 'email' | null totpRecoveryCodesRemaining?: number - isBirthdayWeek?: boolean } type StudyAuthStatusResponse = { @@ -37,7 +36,6 @@ type StudyAuthStatusResponse = { totpEnabled?: boolean twoFaMethod?: 'app' | 'email' | null totpRecoveryCodesRemaining?: number - isBirthdayWeek?: boolean } type StudyAccountOverview = { @@ -1260,10 +1258,6 @@ export function ColossiansStudyIndexPage({ content }: Props) { const [enrollMessage, setEnrollMessage] = useState('') const [studyProgress, setStudyProgress] = useState({ completedSectionIds: [] }) const [progressLoading, setProgressLoading] = useState(false) - const [birthdayBannerDismissed, setBirthdayBannerDismissed] = useState(() => { - const yr = new Date().getFullYear() - return localStorage.getItem(`birthday-banner-dismissed-${yr}`) === '1' - }) useEffect(() => { let cancelled = false @@ -1274,9 +1268,7 @@ export function ColossiansStudyIndexPage({ content }: Props) { checked: true, authenticated: Boolean(data.authenticated), username: data.username ?? '', - displayName: data.displayName ?? '', enrolledStudySlugs: normalizeEnrolledStudySlugs(data.enrolledStudySlugs), - isBirthdayWeek: data.isBirthdayWeek === true, }) }) .catch(() => { @@ -1359,19 +1351,6 @@ export function ColossiansStudyIndexPage({ content }: Props) { return (
    - {auth.authenticated && auth.isBirthdayWeek && !birthdayBannerDismissed && ( -
    - - πŸŽ‚ Happy Birthday{auth.displayName ? `, ${auth.displayName}` : ''}! Wishing you a wonderful birthday week β€” we're so glad you're part of this study community. πŸŽ‰ - - -
    - )}