diff --git a/package.json b/package.json
index b72145c..e984f9b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "siteforge",
"private": true,
- "version": "1.1.15",
+ "version": "1.1.16",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/server.js b/server.js
index 957fd68..4f33161 100644
--- a/server.js
+++ b/server.js
@@ -24,6 +24,7 @@ import {
loadPodcastChecklistFromDisk,
loadAnalyticsEventsFromDisk,
loadEmailSettingsFromDisk,
+ loadCalendarEventsFromDisk,
createBackupSnapshot,
refreshContentCaches,
queueHitStatsWrite,
@@ -50,6 +51,7 @@ import { register as registerStudyComments } from './server/routes/study-comment
import { register as registerStudyCertificate } from './server/routes/study-certificate.js'
import { register as registerEpisodeScripts } from './server/routes/episode-scripts.js'
import { register as registerContact } from './server/routes/contact.js'
+import { register as registerCalendarEvents } from './server/routes/calendar-events.js'
import { startReminderScheduler } from './server/reminder.js'
import { register as registerInboundEmail } from './server/routes/inbound-email.js'
import { register as registerQuestions } from './server/routes/questions.js'
@@ -97,6 +99,7 @@ registerStudyComments(app)
registerStudyCertificate(app)
registerEpisodeScripts(app)
registerContact(app)
+registerCalendarEvents(app)
registerInboundEmail(app)
registerQuestions(app)
registerAnalytics(app)
@@ -146,6 +149,7 @@ Promise.all([
loadPodcastChecklistFromDisk(),
loadAnalyticsEventsFromDisk(),
loadEmailSettingsFromDisk(),
+ loadCalendarEventsFromDisk(),
refreshContentCaches(),
])
.catch(err => {
diff --git a/server/config.js b/server/config.js
index d2dd1d3..efbc431 100644
--- a/server/config.js
+++ b/server/config.js
@@ -59,6 +59,7 @@ export const QR_CODES_FILE = path.join(DATA_DIR, 'qr-codes.json')
export const EPISODE_PLAYS_FILE = path.join(DATA_DIR, 'episode-plays.json')
export const ANALYTICS_EVENTS_FILE = path.join(DATA_DIR, 'analytics-events.json')
export const EMAIL_SETTINGS_FILE = path.join(DATA_DIR, 'email-settings.json')
+export const CALENDAR_EVENTS_FILE = path.join(DATA_DIR, 'calendar-events.json')
export const MAX_EPISODE_SCRIPT_LENGTH = 200_000 // ~150k words, well beyond any sermon
export const DIST_DIR = path.join(ROOT_DIR, 'dist')
diff --git a/server/data.js b/server/data.js
index 8acd857..dd17b49 100644
--- a/server/data.js
+++ b/server/data.js
@@ -31,6 +31,7 @@ import {
EPISODE_PLAYS_FILE,
ANALYTICS_EVENTS_FILE,
EMAIL_SETTINGS_FILE,
+ CALENDAR_EVENTS_FILE,
EMPTY_HIT_STATS,
EMPTY_VISITOR_STATS,
DEFAULT_REPLY_TEMPLATES,
@@ -416,6 +417,36 @@ export function loadEmailSettingsFromDisk() {
})
}
+// ── Calendar events ────────────────────────────────────────────────────────
+
+export function queueCalendarEventsWrite() {
+ state.calendarEventsWritePromise = state.calendarEventsWritePromise
+ .then(async () => {
+ await mkdir(DATA_DIR, { recursive: true })
+ await writeFile(
+ CALENDAR_EVENTS_FILE,
+ JSON.stringify({ events: state.calendarEvents, updatedAt: new Date().toISOString() }, null, 2),
+ 'utf8',
+ )
+ })
+ .catch(err => {
+ console.error('[calendar-events] failed to write:', err)
+ })
+}
+
+export function loadCalendarEventsFromDisk() {
+ return readFile(CALENDAR_EVENTS_FILE, 'utf8')
+ .then(raw => {
+ const parsed = JSON.parse(raw)
+ if (Array.isArray(parsed?.events)) {
+ state.calendarEvents = parsed.events
+ }
+ })
+ .catch(() => {
+ // No file yet — start with empty array
+ })
+}
+
// ── Podcast checklist ──────────────────────────────────────────────────────
export function queuePodcastChecklistWrite() {
diff --git a/server/reminder.js b/server/reminder.js
index 8ed93b0..16a3c2e 100644
--- a/server/reminder.js
+++ b/server/reminder.js
@@ -1,6 +1,6 @@
import { Resend } from 'resend'
import { state } from './state.js'
-import { queuePodcastChecklistWrite } from './data.js'
+import { queuePodcastChecklistWrite, queueCalendarEventsWrite } from './data.js'
import { DEFAULT_RESEND_FROM, DEFAULT_RESEND_TO } from './config.js'
function toDateKey(date) {
@@ -13,35 +13,62 @@ export function startReminderScheduler() {
}
async function checkReminders() {
- const episodes = state.podcastChecklist?.episodes
- if (!Array.isArray(episodes)) return
-
const todayKey = toDateKey(new Date())
- 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)
- const reminderKey = toDateKey(reminderDate)
-
- if (todayKey >= reminderKey && todayKey <= toDateKey(publish)) {
- const sent = await sendReminderEmail(ep)
- if (sent) {
- ep.reminderSentAt = new Date().toISOString()
- changed = true
+ // 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) && todayKey <= toDateKey(publish)) {
+ 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()
}
- 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) && todayKey <= toDateKey(publish)) {
+ 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()
+ }
}
-async function sendReminderEmail(ep) {
+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 {
@@ -49,37 +76,33 @@ async function sendReminderEmail(ep) {
const from = process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM
const to = process.env.RESEND_TO ?? DEFAULT_RESEND_TO
- const publishDate = new Date(ep.datePublished + 'T12:00:00Z')
- const msLeft = publishDate.getTime() - Date.now()
+ 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 label = ep.episodeNumber ? `Episode ${ep.episodeNumber}` : 'Episode'
- const title = ep.title || 'Untitled'
- const series = ep.series ? ` (${ep.series})` : ''
+ const seriesStr = series ? ` (${series})` : ''
const daysText = daysLeft === 0 ? 'today' : daysLeft === 1 ? 'in 1 day' : `in ${daysLeft} days`
- const subject = `Reminder: "${label}: ${title}" publishes ${daysText}`
+ const icon = TYPE_ICONS[type] ?? '📅'
+ const subject = `Reminder: ${label}: ${title} — ${daysText}`
const html = `
-
📅 Release Reminder
+
${icon} Calendar Reminder
- ${label}: ${title} ${series}
- Publishes ${ep.datePublished} — ${daysText}
+ ${label}: ${title} ${seriesStr}
+ Scheduled for ${date} — ${daysText}
- This reminder was set ${ep.reminderDays} day${ep.reminderDays === 1 ? '' : 's'} before the publish date.
+ 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 episode "${title}" (${ep.datePublished})`)
+ 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)
diff --git a/server/routes/calendar-events.js b/server/routes/calendar-events.js
new file mode 100644
index 0000000..7013904
--- /dev/null
+++ b/server/routes/calendar-events.js
@@ -0,0 +1,80 @@
+import { randomUUID } from 'node:crypto'
+import { state } from '../state.js'
+import { queueCalendarEventsWrite } from '../data.js'
+import { requireAdminAuth } from '../auth.js'
+
+const VALID_TYPES = ['general', 'recording', 'social', 'task']
+const DATE_RE = /^\d{4}-\d{2}-\d{2}$/
+
+function sanitize(ev) {
+ return {
+ id: ev.id,
+ type: VALID_TYPES.includes(ev.type) ? ev.type : 'general',
+ title: typeof ev.title === 'string' ? ev.title.slice(0, 300) : '',
+ date: typeof ev.date === 'string' && DATE_RE.test(ev.date) ? ev.date : '',
+ notes: typeof ev.notes === 'string' ? ev.notes.slice(0, 2000) : '',
+ completed: Boolean(ev.completed),
+ reminderDays: Number.isFinite(ev.reminderDays) && ev.reminderDays > 0 ? ev.reminderDays : 0,
+ reminderSentAt: typeof ev.reminderSentAt === 'string' ? ev.reminderSentAt : undefined,
+ createdAt: typeof ev.createdAt === 'string' ? ev.createdAt : new Date().toISOString(),
+ }
+}
+
+export function register(app) {
+ app.get('/api/admin-calendar-events', requireAdminAuth, (_req, res) => {
+ res.json({ events: state.calendarEvents })
+ })
+
+ app.post('/api/admin-calendar-events', requireAdminAuth, (req, res) => {
+ const { type, title, date, notes, reminderDays } = req.body ?? {}
+ if (!title?.trim()) { res.status(400).json({ message: 'Title is required.' }); return }
+ if (!date || !DATE_RE.test(date)) { res.status(400).json({ message: 'Valid date is required.' }); return }
+ if (type && !VALID_TYPES.includes(type)) { res.status(400).json({ message: 'Invalid type.' }); return }
+
+ const ev = sanitize({
+ id: randomUUID(),
+ type: type ?? 'general',
+ title: String(title).trim(),
+ date,
+ notes: notes ?? '',
+ completed: false,
+ reminderDays: reminderDays ?? 0,
+ createdAt: new Date().toISOString(),
+ })
+
+ state.calendarEvents.unshift(ev)
+ queueCalendarEventsWrite()
+ res.json({ ok: true, event: ev })
+ })
+
+ app.patch('/api/admin-calendar-events/:id', requireAdminAuth, (req, res) => {
+ const { id } = req.params
+ let found = false
+ state.calendarEvents = state.calendarEvents.map(ev => {
+ if (ev.id !== id) return ev
+ found = true
+ const patch = {}
+ if (typeof req.body?.title === 'string') patch.title = req.body.title.trim().slice(0, 300)
+ if (typeof req.body?.date === 'string' && DATE_RE.test(req.body.date)) patch.date = req.body.date
+ if (typeof req.body?.notes === 'string') patch.notes = req.body.notes.trim().slice(0, 2000)
+ if (typeof req.body?.completed === 'boolean') patch.completed = req.body.completed
+ if (VALID_TYPES.includes(req.body?.type)) patch.type = req.body.type
+ if (Number.isFinite(req.body?.reminderDays)) patch.reminderDays = req.body.reminderDays
+ const dateChanged = patch.date && patch.date !== ev.date
+ const reminderChanged = 'reminderDays' in patch && patch.reminderDays !== ev.reminderDays
+ if (dateChanged || reminderChanged) patch.reminderSentAt = undefined
+ return { ...ev, ...patch }
+ })
+ if (!found) { res.status(404).json({ message: 'Event not found.' }); return }
+ queueCalendarEventsWrite()
+ res.json({ ok: true })
+ })
+
+ app.delete('/api/admin-calendar-events/:id', requireAdminAuth, (req, res) => {
+ const before = state.calendarEvents.length
+ state.calendarEvents = state.calendarEvents.filter(ev => ev.id !== req.params.id)
+ if (state.calendarEvents.length === before) { res.status(404).json({ message: 'Event not found.' }); return }
+ queueCalendarEventsWrite()
+ res.json({ ok: true })
+ })
+}
diff --git a/server/state.js b/server/state.js
index 22ad8d2..264ea71 100644
--- a/server/state.js
+++ b/server/state.js
@@ -93,6 +93,9 @@ export const state = {
emailSettings: { signature: 'Grace and peace,\nVerse by Verse with Nate' },
emailSettingsWritePromise: Promise.resolve(),
+ calendarEvents: [],
+ calendarEventsWritePromise: Promise.resolve(),
+
lastBackupStatus: { ok: true, at: null, error: null, file: null },
lastCachePurgeStatus: { ok: true, at: null, error: null },
lastDeployHookStatus: { ok: true, at: null, error: null },
diff --git a/src/App.css b/src/App.css
index 9499936..dfaff62 100644
--- a/src/App.css
+++ b/src/App.css
@@ -10575,6 +10575,33 @@
.cal-ep-chip-title { color: #4a8a58; }
+/* Calendar event chips (general / recording / social / task) */
+.cal-ev-chip {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ font-size: 0.68rem;
+ padding: 0.2rem 0.4rem;
+ border-radius: 4px;
+ cursor: pointer;
+ text-align: left;
+ width: 100%;
+ border: 1px solid transparent;
+ transition: opacity 0.12s;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.cal-ev-chip--general { background: #1a2340; border-color: #243060; color: #93c5fd; }
+.cal-ev-chip--recording { background: #28183a; border-color: #3a205a; color: #c084fc; }
+.cal-ev-chip--social { background: #3a1a28; border-color: #5a2038; color: #f9a8d4; }
+.cal-ev-chip--task { background: #2a2010; border-color: #3a3010; color: #fbbf24; }
+.cal-ev-chip--done { opacity: 0.45; text-decoration: line-through; }
+.cal-ev-chip:hover { filter: brightness(1.15); }
+
+.cal-ev-icon { flex-shrink: 0; }
+.cal-ev-title { flex: 1; overflow: hidden; text-overflow: ellipsis; }
+
.cal-ep-bell {
font-size: 0.6rem;
margin-left: 2px;
diff --git a/src/CalendarPage.tsx b/src/CalendarPage.tsx
index 474ed16..35364b1 100644
--- a/src/CalendarPage.tsx
+++ b/src/CalendarPage.tsx
@@ -30,6 +30,27 @@ const REMINDER_OPTIONS = [
{ value: '14', label: '2 weeks before' },
]
+type CalendarEventType = 'general' | 'recording' | 'social' | 'task'
+
+interface CalendarEvent {
+ id: string
+ type: CalendarEventType
+ title: string
+ date: string
+ notes: string
+ completed: boolean
+ reminderDays: number
+ reminderSentAt?: string
+ createdAt: string
+}
+
+const EVENT_TYPE_OPTIONS: { value: CalendarEventType; label: string; icon: string }[] = [
+ { value: 'general', label: 'General', icon: '📌' },
+ { value: 'recording', label: 'Recording', icon: '🎙️' },
+ { value: 'social', label: 'Social', icon: '📱' },
+ { value: 'task', label: 'Task', icon: '✅' },
+]
+
interface PodcastChecklistData {
tasks: PodcastChecklistTask[]
episodes: PodcastChecklistEpisode[]
@@ -165,13 +186,28 @@ function CalendarClient() {
const [newForm, setNewForm] = useState({ series: '', episodeNumber: '', title: '', datePublished: '', reminderDays: '0' })
const [newBusy, setNewBusy] = useState(false)
+ const [events, setEvents] = useState([])
+ const [newEvOpen, setNewEvOpen] = useState(false)
+ const [newEvForm, setNewEvForm] = useState({ type: 'general' as CalendarEventType, title: '', date: '', notes: '', reminderDays: '0' })
+ const [newEvBusy, setNewEvBusy] = useState(false)
+ const [editEv, setEditEv] = useState(null)
+ const [editEvForm, setEditEvForm] = useState({ type: 'general' as CalendarEventType, title: '', date: '', notes: '', reminderDays: '0' })
+ const [evSaving, setEvSaving] = useState(false)
+
const load = useCallback(async () => {
try {
- const res = await fetch('/api/admin-podcast-checklist', { credentials: 'include' })
- if (res.ok) {
- const data = await res.json() as { checklist: PodcastChecklistData }
+ const [clRes, evRes] = await Promise.all([
+ fetch('/api/admin-podcast-checklist', { credentials: 'include' }),
+ fetch('/api/admin-calendar-events', { credentials: 'include' }),
+ ])
+ if (clRes.ok) {
+ const data = await clRes.json() as { checklist: PodcastChecklistData }
setChecklist(data.checklist)
}
+ if (evRes.ok) {
+ const data = await evRes.json() as { events: CalendarEvent[] }
+ setEvents(Array.isArray(data.events) ? data.events : [])
+ }
} catch { /* silent */ }
setLoading(false)
}, [])
@@ -229,6 +265,17 @@ function CalendarClient() {
[checklist]
)
+ const eventsByDate = useMemo(() => {
+ const map = new Map()
+ for (const ev of events) {
+ if (!ev.date) continue
+ const arr = map.get(ev.date) ?? []
+ arr.push(ev)
+ map.set(ev.date, arr)
+ }
+ return map
+ }, [events])
+
const todayKey = toDateKey(today)
function prevMonth() {
@@ -295,6 +342,86 @@ function CalendarClient() {
setEditEp(null)
}
+ async function handleNewEvent(e: React.FormEvent) {
+ e.preventDefault()
+ if (!newEvForm.title.trim() || !newEvForm.date) return
+ setNewEvBusy(true)
+ try {
+ const res = await fetch('/api/admin-calendar-events', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({
+ type: newEvForm.type,
+ title: newEvForm.title.trim(),
+ date: newEvForm.date,
+ notes: newEvForm.notes.trim(),
+ reminderDays: Number(newEvForm.reminderDays),
+ }),
+ })
+ if (res.ok) {
+ const data = await res.json() as { event: CalendarEvent }
+ setEvents(prev => [data.event, ...prev])
+ setNewEvForm({ type: 'general', title: '', date: '', notes: '', reminderDays: '0' })
+ setNewEvOpen(false)
+ }
+ } catch { /* silent */ }
+ setNewEvBusy(false)
+ }
+
+ function openEditEvent(ev: CalendarEvent) {
+ setEditEv(ev)
+ setEditEvForm({ type: ev.type, title: ev.title, date: ev.date, notes: ev.notes, reminderDays: String(ev.reminderDays) })
+ }
+
+ async function saveEditEvent() {
+ if (!editEv) return
+ setEvSaving(true)
+ try {
+ const res = await fetch(`/api/admin-calendar-events/${editEv.id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({
+ type: editEvForm.type,
+ title: editEvForm.title.trim(),
+ date: editEvForm.date,
+ notes: editEvForm.notes.trim(),
+ reminderDays: Number(editEvForm.reminderDays),
+ }),
+ })
+ if (res.ok) {
+ setEvents(prev => prev.map(ev => ev.id === editEv.id
+ ? { ...ev, ...editEvForm, reminderDays: Number(editEvForm.reminderDays),
+ reminderSentAt: (editEvForm.date !== editEv.date || Number(editEvForm.reminderDays) !== editEv.reminderDays) ? undefined : ev.reminderSentAt }
+ : ev
+ ))
+ setEditEv(null)
+ }
+ } catch { /* silent */ }
+ setEvSaving(false)
+ }
+
+ async function deleteEvent(ev: CalendarEvent) {
+ try {
+ const res = await fetch(`/api/admin-calendar-events/${ev.id}`, { method: 'DELETE', credentials: 'include' })
+ if (res.ok) { setEvents(prev => prev.filter(e => e.id !== ev.id)); setEditEv(null) }
+ } catch { /* silent */ }
+ }
+
+ async function toggleComplete(ev: CalendarEvent) {
+ const next = !ev.completed
+ try {
+ const res = await fetch(`/api/admin-calendar-events/${ev.id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ completed: next }),
+ })
+ if (res.ok) setEvents(prev => prev.map(e => e.id === ev.id ? { ...e, completed: next } : e))
+ } catch { /* silent */ }
+ }
+
async function handleNewEpisode(e: React.FormEvent) {
e.preventDefault()
if (!checklist) return
@@ -346,10 +473,18 @@ function CalendarClient() {
setNewEpOpen(o => !o)}
+ onClick={() => { setNewEpOpen(o => !o); setNewEvOpen(false) }}
>
- {newEpOpen ? 'Cancel' : '+ New Episode'}
+ {newEpOpen ? 'Cancel' : '+ Episode'}
+ { setNewEvOpen(o => !o); setNewEpOpen(false) }}
+ >
+ {newEvOpen ? 'Cancel' : '+ Event'}
+
+ ✉ Email
← Admin
@@ -391,6 +526,47 @@ function CalendarClient() {
)}
+ {newEvOpen && (
+
+ )}
+
{loading ? (
@@ -434,6 +610,22 @@ function CalendarClient() {
{(ep.reminderDays ?? 0) > 0 && {ep.reminderSentAt ? '✓' : '🔔'} }
))}
+ {(eventsByDate.get(dk) ?? []).map(ev => {
+ const opt = EVENT_TYPE_OPTIONS.find(o => o.value === ev.type)
+ return (
+ { e.stopPropagation(); openEditEvent(ev) }}
+ >
+ {opt?.icon}
+ {ev.title.slice(0, 20)}{ev.title.length > 20 ? '…' : ''}
+ {(ev.reminderDays ?? 0) > 0 && {ev.reminderSentAt ? '✓' : '🔔'} }
+
+ )
+ })}
)
@@ -561,6 +753,69 @@ function CalendarClient() {
)}
+
+ {/* Event edit popover */}
+ {editEv && (
+
+ )}
)
}
diff --git a/src/ContactsPage.tsx b/src/ContactsPage.tsx
index 08ee17b..722b574 100644
--- a/src/ContactsPage.tsx
+++ b/src/ContactsPage.tsx
@@ -303,7 +303,8 @@ function ContactsClient() {
>
{addOpen ? 'Cancel' : '+ Add Contact'}
- Email
+ ✉ Email
+ Calendar
← Admin