'use client'; import React, { useState, useEffect, useCallback } from 'react'; import Link from 'next/link'; import { useAuth } from '@/contexts/AuthContext'; import { useRouter } from 'next/navigation'; interface ArticleBlock { title: string; excerpt: string; url: string; category: string; featured?: boolean; } interface NewsletterTemplate { id: string; name: string; description: string; layout: 'standard' | 'featured' | 'minimal' | 'two-column'; style: 'dark' | 'light' | 'branded'; subject_prefix: string; header_text: string; footer_text: string; accent_color: string; article_blocks: ArticleBlock[]; is_preset: boolean; created_at: string; } const LAYOUT_OPTIONS = [ { value: 'standard', label: 'Standard', desc: 'Single column, articles stacked vertically' }, { value: 'featured', label: 'Featured', desc: 'Lead story hero + supporting articles below' }, { value: 'minimal', label: 'Minimal', desc: 'Clean single-story alert format' }, { value: 'two-column', label: 'Two Column', desc: 'Side-by-side article grid layout' }, ]; const STYLE_OPTIONS = [ { value: 'dark', label: 'Dark', desc: 'Dark background, light text' }, { value: 'light', label: 'Light', desc: 'White background, dark text' }, { value: 'branded', label: 'Branded', desc: 'Accent color header with dark body' }, ]; const ACCENT_COLORS = [ '#F0A623', '#ef4444', '#10b981', '#8b5cf6', '#f59e0b', '#ec4899', '#06b6d4', '#f97316', ]; const LAYOUT_ICONS: Record = { standard: ( ), featured: ( ), minimal: ( ), 'two-column': ( ), }; const emptyForm = (): Omit => ({ name: '', description: '', layout: 'standard', style: 'dark', subject_prefix: '', header_text: '', footer_text: '', accent_color: '#F0A623', article_blocks: [{ title: '', excerpt: '', url: '', category: '' }], }); export default function NewsletterTemplatesPage() { const { user, loading } = useAuth(); const router = useRouter(); const [templates, setTemplates] = useState([]); const [loadingData, setLoadingData] = useState(true); const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null); const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState(emptyForm()); const [saving, setSaving] = useState(false); const [deletingId, setDeletingId] = useState(null); const [previewTemplate, setPreviewTemplate] = useState(null); useEffect(() => { if (!loading && !user) router.replace('/login?next=' + encodeURIComponent(window.location.pathname)); }, [user, loading, router]); const showNotification = (type: 'success' | 'error', message: string) => { setNotification({ type, message }); setTimeout(() => setNotification(null), 4000); }; const fetchTemplates = useCallback(async () => { setLoadingData(true); try { const res = await fetch('/api/newsletter/templates'); if (res.ok) { const data = await res.json(); setTemplates(data.templates || []); } } catch { // silent } finally { setLoadingData(false); } }, []); useEffect(() => { if (user) fetchTemplates(); }, [user, fetchTemplates]); const openNew = () => { setForm(emptyForm()); setEditingId(null); setShowForm(true); }; const openEdit = (tpl: NewsletterTemplate) => { setForm({ name: tpl.name, description: tpl.description, layout: tpl.layout, style: tpl.style, subject_prefix: tpl.subject_prefix, header_text: tpl.header_text, footer_text: tpl.footer_text, accent_color: tpl.accent_color, article_blocks: tpl.article_blocks?.length ? tpl.article_blocks : [{ title: '', excerpt: '', url: '', category: '' }], }); setEditingId(tpl.id); setShowForm(true); }; const handleSave = async () => { if (!form.name.trim()) { showNotification('error', 'Template name is required'); return; } setSaving(true); try { const method = editingId ? 'PUT' : 'POST'; const body = editingId ? { id: editingId, ...form } : form; const res = await fetch('/api/newsletter/templates', { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (res.ok) { showNotification('success', editingId ? 'Template updated' : 'Template created'); setShowForm(false); fetchTemplates(); } else { const d = await res.json(); showNotification('error', d.error || 'Failed to save'); } } catch { showNotification('error', 'Failed to save template'); } finally { setSaving(false); } }; const handleDelete = async (id: string) => { if (!confirm('Delete this template?')) return; setDeletingId(id); try { const res = await fetch('/api/newsletter/templates', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }); if (res.ok) { setTemplates((prev) => prev.filter((t) => t.id !== id)); showNotification('success', 'Template deleted'); } else { const d = await res.json(); showNotification('error', d.error || 'Failed to delete'); } } catch { showNotification('error', 'Failed to delete template'); } finally { setDeletingId(null); } }; const addBlock = () => { setForm((f) => ({ ...f, article_blocks: [...f.article_blocks, { title: '', excerpt: '', url: '', category: '' }] })); }; const removeBlock = (i: number) => { setForm((f) => ({ ...f, article_blocks: f.article_blocks.filter((_, idx) => idx !== i) })); }; const updateBlock = (i: number, field: keyof ArticleBlock, value: string | boolean) => { setForm((f) => ({ ...f, article_blocks: f.article_blocks.map((b, idx) => idx === i ? { ...b, [field]: value } : b), })); }; const presets = templates.filter((t) => t.is_preset); const custom = templates.filter((t) => !t.is_preset); if (loading) { return (
); } if (!user) return null; return (
{/* Notification */} {notification && (
{notification.message}
)} {/* Header */}

Digest Templates

Reusable layouts, styles & article block arrangements

{/* Preset Templates */}

Preset Templates

{presets.length}
{loadingData ? (
) : (
{presets.map((tpl) => ( ))}
)}
{/* Custom Templates */}

Custom Templates

{custom.length}
{!loadingData && custom.length === 0 ? (

No custom templates yet

) : (
{custom.map((tpl) => ( ))}
)}
{/* Template Form Modal */} {showForm && (

{editingId ? 'Edit Template' : 'New Template'}

{/* Name & Description */}
setForm((f) => ({ ...f, name: e.target.value }))} placeholder="e.g. Weekly Tech Roundup" className="search-input w-full py-2.5 px-3 text-sm" />
setForm((f) => ({ ...f, description: e.target.value }))} placeholder="Brief description of this template's purpose" className="search-input w-full py-2.5 px-3 text-sm" />
{/* Layout */}
{LAYOUT_OPTIONS.map((opt) => ( ))}
{/* Style */}
{STYLE_OPTIONS.map((opt) => ( ))}
{/* Accent Color */}
{ACCENT_COLORS.map((color) => (
{/* Subject Prefix */}
setForm((f) => ({ ...f, subject_prefix: e.target.value }))} placeholder="e.g. AV Beat Weekly —" className="search-input w-full py-2.5 px-3 text-sm" />

Prepended to the digest subject line when using this template

{/* Header & Footer Text */}