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:
+84
-3
@@ -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, 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>
|
||||
|
||||
Reference in New Issue
Block a user