refactor(admin): split email-templates page.tsx into colocated components
Extract tabs nav, templates grid, editor split view, settings form, logs table, and data-loading/actions hook into _components/ and _hooks/. page.tsx reduced from 816 to 88 LOC. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
API_BASE,
|
||||
EmailTemplate,
|
||||
SendLog,
|
||||
Settings,
|
||||
TabId,
|
||||
TemplateType,
|
||||
TemplateVersion,
|
||||
getHeaders,
|
||||
} from '../_types'
|
||||
|
||||
export function useEmailTemplates(activeTab: TabId) {
|
||||
const [templates, setTemplates] = useState<EmailTemplate[]>([])
|
||||
const [templateTypes, setTemplateTypes] = useState<TemplateType[]>([])
|
||||
const [settings, setSettings] = useState<Settings | null>(null)
|
||||
const [logs, setLogs] = useState<SendLog[]>([])
|
||||
const [logsTotal, setLogsTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
|
||||
|
||||
// Editor state
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<EmailTemplate | null>(null)
|
||||
const [editorSubject, setEditorSubject] = useState('')
|
||||
const [editorHtml, setEditorHtml] = useState('')
|
||||
const [editorVersion, setEditorVersion] = useState<TemplateVersion | null>(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [previewHtml, setPreviewHtml] = useState<string | null>(null)
|
||||
|
||||
// Settings form
|
||||
const [settingsForm, setSettingsForm] = useState<Settings | null>(null)
|
||||
const [savingSettings, setSavingSettings] = useState(false)
|
||||
|
||||
const loadTemplates = useCallback(async () => {
|
||||
try {
|
||||
const url = selectedCategory ? `${API_BASE}?category=${selectedCategory}` : API_BASE
|
||||
const res = await fetch(url, { headers: getHeaders() })
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const data = await res.json()
|
||||
setTemplates(Array.isArray(data) ? data : [])
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
}
|
||||
}, [selectedCategory])
|
||||
|
||||
const loadTypes = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/types`, { headers: getHeaders() })
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setTemplateTypes(Array.isArray(data) ? data : [])
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, [])
|
||||
|
||||
const loadSettings = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/settings`, { headers: getHeaders() })
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setSettings(data)
|
||||
setSettingsForm(data)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, [])
|
||||
|
||||
const loadLogs = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/logs?limit=50`, { headers: getHeaders() })
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setLogs(data.logs || [])
|
||||
setLogsTotal(data.total || 0)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
Promise.all([loadTemplates(), loadTypes(), loadSettings()])
|
||||
.finally(() => setLoading(false))
|
||||
}, [loadTemplates, loadTypes, loadSettings])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'logs') loadLogs()
|
||||
}, [activeTab, loadLogs])
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates()
|
||||
}, [selectedCategory, loadTemplates])
|
||||
|
||||
const openEditor = useCallback(async (template: EmailTemplate) => {
|
||||
setSelectedTemplate(template)
|
||||
setPreviewHtml(null)
|
||||
|
||||
if (template.latest_version) {
|
||||
setEditorSubject(template.latest_version.subject)
|
||||
setEditorHtml(template.latest_version.body_html)
|
||||
setEditorVersion(template.latest_version)
|
||||
} else {
|
||||
// Load default content
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/default/${template.template_type}`, { headers: getHeaders() })
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setEditorSubject(data.default_subject || '')
|
||||
setEditorHtml(data.default_body_html || '')
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
setEditorVersion(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const saveVersion = useCallback(async () => {
|
||||
if (!selectedTemplate) return
|
||||
setSaving(true)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/${selectedTemplate.id}/versions`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({
|
||||
version: editorVersion ? `${parseFloat(editorVersion.version) + 0.1}` : '1.0',
|
||||
language: 'de',
|
||||
subject: editorSubject,
|
||||
body_html: editorHtml,
|
||||
}),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const version = await res.json()
|
||||
setEditorVersion(version)
|
||||
await loadTemplates()
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}, [selectedTemplate, editorSubject, editorHtml, editorVersion, loadTemplates])
|
||||
|
||||
const publishVersion = useCallback(async () => {
|
||||
if (!editorVersion) return
|
||||
setSaving(true)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/versions/${editorVersion.id}/publish`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const updated = await res.json()
|
||||
setEditorVersion(updated)
|
||||
await loadTemplates()
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}, [editorVersion, loadTemplates])
|
||||
|
||||
const loadPreview = useCallback(async () => {
|
||||
if (!editorVersion) return
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/versions/${editorVersion.id}/preview`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ variables: {} }),
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setPreviewHtml(data.body_html)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, [editorVersion])
|
||||
|
||||
const saveSettings2 = useCallback(async () => {
|
||||
if (!settingsForm) return
|
||||
setSavingSettings(true)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/settings`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(settingsForm),
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setSettings(data)
|
||||
setSettingsForm(data)
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setSavingSettings(false)
|
||||
}
|
||||
}, [settingsForm])
|
||||
|
||||
const initializeDefaults = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/initialize`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
})
|
||||
if (res.ok) {
|
||||
await loadTemplates()
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
}
|
||||
}, [loadTemplates])
|
||||
|
||||
return {
|
||||
// Data
|
||||
templates, templateTypes, settings, logs, logsTotal, loading, error,
|
||||
selectedCategory,
|
||||
// Editor
|
||||
selectedTemplate, editorSubject, editorHtml, editorVersion, saving, previewHtml,
|
||||
// Settings
|
||||
settingsForm, savingSettings,
|
||||
// Setters
|
||||
setError, setSelectedCategory,
|
||||
setEditorSubject, setEditorHtml,
|
||||
setSettingsForm,
|
||||
// Actions
|
||||
openEditor, saveVersion, publishVersion, loadPreview,
|
||||
saveSettings2, initializeDefaults,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user