Add /contacts and /calendar pages; email signature settings; v1.1.12

- /contacts: standalone page with hybrid contact list (submissions + manual entry), inline edit, search, archive
- /calendar: monthly release scheduling calendar reading/writing podcast checklist episode dates
- /email settings: editable signature panel; signature persisted server-side and injected into outgoing emails
- Move contacts out of /admin panel (now links to /contacts route)
- Partial PATCH for contact submissions (name, notes, archived independently)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-07-28 10:12:52 -04:00
parent 7670b44d27
commit ce235ba9ef
13 changed files with 1952 additions and 145 deletions
+84 -3
View File
@@ -60,6 +60,10 @@ interface ReplyConfig {
note: string
}
interface EmailSettings {
signature: string
}
// ── Helpers ──────────────────────────────────────────────────────────────────
function extractSubject(message: string): string {
@@ -120,6 +124,11 @@ function EmailClient({ onLogout }: { onLogout: () => void }) {
const [templateStatus, setTemplateStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
const [showTemplatesMgr, setShowTemplatesMgr] = useState(false)
const [showHistory, setShowHistory] = useState(false)
const [showSettings, setShowSettings] = useState(false)
const [emailSettings, setEmailSettings] = useState<EmailSettings>({ signature: 'Grace and peace,\nVerse by Verse with Nate' })
const [settingsSig, setSettingsSig] = useState('')
const [settingsSaving, setSettingsSaving] = useState(false)
const [settingsSaved, setSettingsSaved] = useState(false)
const [actionMsg, setActionMsg] = useState('')
const composeRef = useRef<HTMLTextAreaElement>(null)
const listRef = useRef<HTMLElement>(null)
@@ -128,11 +137,12 @@ function EmailClient({ onLogout }: { onLogout: () => void }) {
const loadAll = useCallback(async () => {
try {
const [subRes, tplRes, histRes, cfgRes] = await Promise.all([
const [subRes, tplRes, histRes, cfgRes, settingsRes] = await Promise.all([
fetch('/api/admin-contact-submissions'),
fetch('/api/admin-contact-reply-templates'),
fetch('/api/admin-contact-reply-history'),
fetch('/api/admin-reply-config'),
fetch('/api/admin-email-settings'),
])
if (!subRes.ok) throw new Error('submissions failed')
const subData = await subRes.json() as { submissions?: ContactSubmission[] }
@@ -147,6 +157,10 @@ function EmailClient({ onLogout }: { onLogout: () => void }) {
setHistory(d.items ?? [])
}
if (cfgRes.ok) setConfig(await cfgRes.json() as ReplyConfig)
if (settingsRes.ok) {
const s = await settingsRes.json() as EmailSettings
setEmailSettings(s)
}
} catch {
setLoadStatus('error')
}
@@ -543,8 +557,8 @@ function EmailClient({ onLogout }: { onLogout: () => void }) {
onChange={e => setReplyDraft({ ...replyDraft, message: e.target.value })}
/>
<div className="em-compose-signature">
Grace and peace, · Verse by Verse with Nate
<span className="em-compose-signature-note">auto-appended</span>
<span className="em-compose-signature-text">{emailSettings.signature}</span>
<span className="em-compose-signature-note">auto-appended · <button type="button" className="em-sig-edit-link" onClick={() => { setSettingsSig(emailSettings.signature); setShowSettings(true) }}>edit signature</button></span>
</div>
<div className="em-compose-actions">
<button type="button" className="em-btn em-btn--primary" onClick={handleSend} disabled={replySending}>
@@ -788,6 +802,70 @@ function EmailClient({ onLogout }: { onLogout: () => void }) {
</div>
)}
{/* Settings drawer */}
{showSettings && (
<div className="em-drawer-overlay" onClick={e => { if (e.target === e.currentTarget) setShowSettings(false) }}>
<div className="em-drawer">
<div className="em-drawer-header">
<h3>Email Settings</h3>
<button type="button" className="em-compose-close" onClick={() => setShowSettings(false)}>×</button>
</div>
<div className="em-drawer-body">
<div className="em-settings-section">
<label className="em-settings-label">
Outgoing Signature
<p className="em-settings-note">Appended to every reply and compose email you send.</p>
<textarea
className="em-compose-body em-settings-sig-textarea"
rows={4}
value={settingsSig}
onChange={e => { setSettingsSig(e.target.value); setSettingsSaved(false) }}
placeholder="Grace and peace,&#10;Verse by Verse with Nate"
/>
</label>
</div>
{config && (
<div className="em-settings-section">
<p className="em-settings-label">
Email Configuration
<span className={`em-settings-status${config.canSendReplies ? ' em-settings-status--ok' : ' em-settings-status--warn'}`}>
{config.canSendReplies ? 'Configured' : 'Not configured'}
</span>
</p>
<p className="em-settings-note">{config.note}</p>
{config.fromEmail && <p className="em-settings-note">Reply-to: {config.fromEmail}</p>}
</div>
)}
</div>
<div className="em-drawer-actions">
<button
type="button"
className="em-btn em-btn--primary"
disabled={settingsSaving}
onClick={async () => {
setSettingsSaving(true)
try {
const res = await fetch('/api/admin-email-settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ signature: settingsSig }),
})
if (res.ok) {
const data = await res.json() as { settings?: EmailSettings }
if (data.settings) setEmailSettings(data.settings)
setSettingsSaved(true)
}
} catch { /* silent */ }
setSettingsSaving(false)
}}
>
{settingsSaving ? 'Saving…' : settingsSaved ? 'Saved ✓' : 'Save Settings'}
</button>
</div>
</div>
</div>
)}
{/* Footer toolbar */}
<div className="em-footer-toolbar">
{config && !config.canSendReplies && (
@@ -799,6 +877,9 @@ function EmailClient({ onLogout }: { onLogout: () => void }) {
<button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={() => setShowHistory(true)}>
Sent History
</button>
<button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={() => { setSettingsSig(emailSettings.signature); setShowSettings(true) }}>
Settings
</button>
<span className="em-footer-hint"> navigate · r reply · e archive · Esc close</span>
</div>
</div>