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.
-
- bdayUpdate('month', e.target.value)} style={{ width: '130px' }}>
- Monthβ¦
- {MONTHS.map((name, i) => {name} )}
-
- bdayUpdate('day', e.target.value)} style={{ width: '80px' }}>
- Dayβ¦
- {Array.from({ length: 31 }, (_, i) => {i + 1} )}
-
- handleSaveBirthday(user)}>Save
- {(b.month && b.day) && (
- {
- setEditBirthday(prev => ({ ...prev, [user.id]: { month: '', day: '', altMonth: '', altDay: '' } }))
- patchUser(user.id, { birthdayMonth: '', birthdayDay: '', birthdayAlternateMonth: '', birthdayAlternateDay: '' })
- .then(() => { setUsers(prev => prev.map(u => u.id !== user.id ? u : { ...u, birthdayMonth: null, birthdayDay: null, birthdayAlternateMonth: null, birthdayAlternateDay: null })); flashMsg(user.id, 'Birthday cleared.') })
- .catch(() => flashMsg(user.id, 'Error clearing birthday.'))
- }}>Clear
- )}
-
- {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.
-
- bdayUpdate('altMonth', e.target.value)} style={{ width: '130px' }}>
- Celebrate inβ¦
- {MONTHS.map((name, i) => {name} )}
-
- bdayUpdate('altDay', e.target.value)} style={{ width: '80px' }}>
- Dayβ¦
- {Array.from({ length: 31 }, (_, i) => {i + 1} )}
-
-
- {(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. π
-
- { setBirthdayBannerDismissed(true); localStorage.setItem(`birthday-banner-dismissed-${new Date().getFullYear()}`, '1') }}
- aria-label="Dismiss birthday banner"
- style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#7a5800', fontSize: '1.1rem', lineHeight: 1, padding: '0.2rem 0.4rem', borderRadius: '3px' }}
- >β
-
- )}