initial commit: rocket.new export of broadcastbeat

This commit is contained in:
Ryan Salazar
2026-05-07 16:39:17 +00:00
commit 70aa1ad46e
302 changed files with 60710 additions and 0 deletions

View File

@@ -0,0 +1,702 @@
'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 = [
'#3b82f6', '#ef4444', '#10b981', '#8b5cf6', '#f59e0b', '#ec4899', '#06b6d4', '#f97316',
];
const LAYOUT_ICONS: Record<string, React.ReactNode> = {
standard: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<rect x="3" y="4" width="18" height="3" rx="1" strokeLinecap="round" />
<rect x="3" y="10" width="18" height="3" rx="1" strokeLinecap="round" />
<rect x="3" y="16" width="18" height="3" rx="1" strokeLinecap="round" />
</svg>
),
featured: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="8" rx="1" strokeLinecap="round" />
<rect x="3" y="14" width="8" height="6" rx="1" strokeLinecap="round" />
<rect x="13" y="14" width="8" height="6" rx="1" strokeLinecap="round" />
</svg>
),
minimal: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<rect x="3" y="8" width="18" height="8" rx="1" strokeLinecap="round" />
</svg>
),
'two-column': (
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<rect x="3" y="3" width="8" height="8" rx="1" strokeLinecap="round" />
<rect x="13" y="3" width="8" height="8" rx="1" strokeLinecap="round" />
<rect x="3" y="13" width="8" height="8" rx="1" strokeLinecap="round" />
<rect x="13" y="13" width="8" height="8" rx="1" strokeLinecap="round" />
</svg>
),
};
const emptyForm = (): Omit<NewsletterTemplate, 'id' | 'is_preset' | 'created_at'> => ({
name: '',
description: '',
layout: 'standard',
style: 'dark',
subject_prefix: '',
header_text: '',
footer_text: '',
accent_color: '#3b82f6',
article_blocks: [{ title: '', excerpt: '', url: '', category: '' }],
});
export default function NewsletterTemplatesPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [templates, setTemplates] = useState<NewsletterTemplate[]>([]);
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<string | null>(null);
const [form, setForm] = useState(emptyForm());
const [saving, setSaving] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [previewTemplate, setPreviewTemplate] = useState<NewsletterTemplate | null>(null);
useEffect(() => {
if (!loading && !user) router.replace('/account');
}, [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 (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
</div>
);
}
if (!user) return null;
return (
<div className="min-h-screen bg-[#0a0a0a]">
{/* Notification */}
{notification && (
<div className={`fixed top-4 right-4 z-50 px-4 py-3 rounded-lg text-sm font-body font-semibold shadow-lg transition-all ${notification.type === 'success' ? 'bg-green-500/20 border border-green-500/40 text-green-400' : 'bg-red-500/20 border border-red-500/40 text-red-400'}`}>
{notification.message}
</div>
)}
{/* Header */}
<div className="bg-[#111] border-b border-[#252525] px-6 py-4">
<div className="max-w-6xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/admin/newsletter" className="text-[#555] hover:text-[#888] transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div>
<h1 className="font-display font-bold text-white text-lg">Digest Templates</h1>
<p className="text-[#555] text-xs font-body">Reusable layouts, styles & article block arrangements</p>
</div>
</div>
<button
onClick={openNew}
className="btn-subscribe py-2 px-4 text-sm font-semibold flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg>
New Template
</button>
</div>
</div>
<div className="max-w-6xl mx-auto px-6 py-6">
{/* Preset Templates */}
<div className="mb-8">
<div className="flex items-center gap-2 mb-4">
<h2 className="font-display font-bold text-white text-base">Preset Templates</h2>
<span className="text-xs bg-[#1a2535] text-[#3b82f6] px-2 py-0.5 rounded-full font-body">{presets.length}</span>
</div>
{loadingData ? (
<div className="flex items-center justify-center py-12">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{presets.map((tpl) => (
<TemplateCard
key={tpl.id}
template={tpl}
onEdit={openEdit}
onDelete={handleDelete}
onPreview={setPreviewTemplate}
deletingId={deletingId}
/>
))}
</div>
)}
</div>
{/* Custom Templates */}
<div>
<div className="flex items-center gap-2 mb-4">
<h2 className="font-display font-bold text-white text-base">Custom Templates</h2>
<span className="text-xs bg-[#1a2535] text-[#3b82f6] px-2 py-0.5 rounded-full font-body">{custom.length}</span>
</div>
{!loadingData && custom.length === 0 ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-10 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
<p className="text-[#555] font-body text-sm mb-3">No custom templates yet</p>
<button onClick={openNew} className="text-[#3b82f6] text-sm font-body font-semibold hover:text-blue-400 transition-colors">
Create your first template
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{custom.map((tpl) => (
<TemplateCard
key={tpl.id}
template={tpl}
onEdit={openEdit}
onDelete={handleDelete}
onPreview={setPreviewTemplate}
deletingId={deletingId}
/>
))}
</div>
)}
</div>
</div>
{/* Template Form Modal */}
{showForm && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-start justify-center overflow-y-auto py-8 px-4">
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-2xl">
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525]">
<h2 className="font-display font-bold text-white text-base">
{editingId ? 'Edit Template' : 'New Template'}
</h2>
<button onClick={() => setShowForm(false)} className="text-[#555] hover:text-white transition-colors">
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="px-6 py-5 space-y-5">
{/* Name & Description */}
<div className="grid grid-cols-1 gap-4">
<div>
<label className="block text-xs font-body font-bold text-[#888] uppercase tracking-wider mb-1.5">Template Name *</label>
<input
type="text"
value={form.name}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs font-body font-bold text-[#888] uppercase tracking-wider mb-1.5">Description</label>
<input
type="text"
value={form.description}
onChange={(e) => 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"
/>
</div>
</div>
{/* Layout */}
<div>
<label className="block text-xs font-body font-bold text-[#888] uppercase tracking-wider mb-2">Layout</label>
<div className="grid grid-cols-2 gap-2">
{LAYOUT_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setForm((f) => ({ ...f, layout: opt.value as NewsletterTemplate['layout'] }))}
className={`flex items-start gap-3 p-3 rounded-lg border text-left transition-colors ${form.layout === opt.value ? 'border-[#3b82f6] bg-[#1a2535]' : 'border-[#252525] bg-[#0d0d0d] hover:border-[#333]'}`}>
<span className={`mt-0.5 ${form.layout === opt.value ? 'text-[#3b82f6]' : 'text-[#555]'}`}>
{LAYOUT_ICONS[opt.value]}
</span>
<div>
<p className={`text-sm font-body font-semibold ${form.layout === opt.value ? 'text-white' : 'text-[#888]'}`}>{opt.label}</p>
<p className="text-xs font-body text-[#555] mt-0.5">{opt.desc}</p>
</div>
</button>
))}
</div>
</div>
{/* Style */}
<div>
<label className="block text-xs font-body font-bold text-[#888] uppercase tracking-wider mb-2">Style</label>
<div className="flex gap-2">
{STYLE_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setForm((f) => ({ ...f, style: opt.value as NewsletterTemplate['style'] }))}
className={`flex-1 py-2.5 px-3 rounded-lg border text-sm font-body font-semibold transition-colors ${form.style === opt.value ? 'border-[#3b82f6] bg-[#1a2535] text-white' : 'border-[#252525] bg-[#0d0d0d] text-[#888] hover:border-[#333]'}`}>
{opt.label}
</button>
))}
</div>
</div>
{/* Accent Color */}
<div>
<label className="block text-xs font-body font-bold text-[#888] uppercase tracking-wider mb-2">Accent Color</label>
<div className="flex items-center gap-2 flex-wrap">
{ACCENT_COLORS.map((color) => (
<button
key={color}
type="button"
onClick={() => setForm((f) => ({ ...f, accent_color: color }))}
style={{ backgroundColor: color }}
className={`w-7 h-7 rounded-full transition-transform ${form.accent_color === color ? 'ring-2 ring-white ring-offset-2 ring-offset-[#111] scale-110' : 'hover:scale-110'}`}
/>
))}
<input
type="color"
value={form.accent_color}
onChange={(e) => setForm((f) => ({ ...f, accent_color: e.target.value }))}
className="w-7 h-7 rounded-full cursor-pointer border-0 bg-transparent"
title="Custom color"
/>
</div>
</div>
{/* Subject Prefix */}
<div>
<label className="block text-xs font-body font-bold text-[#888] uppercase tracking-wider mb-1.5">Subject Prefix</label>
<input
type="text"
value={form.subject_prefix}
onChange={(e) => setForm((f) => ({ ...f, subject_prefix: e.target.value }))}
placeholder="e.g. BroadcastBeat Weekly —"
className="search-input w-full py-2.5 px-3 text-sm"
/>
<p className="text-xs text-[#555] font-body mt-1">Prepended to the digest subject line when using this template</p>
</div>
{/* Header & Footer Text */}
<div className="grid grid-cols-1 gap-4">
<div>
<label className="block text-xs font-body font-bold text-[#888] uppercase tracking-wider mb-1.5">Header Text</label>
<textarea
value={form.header_text}
onChange={(e) => setForm((f) => ({ ...f, header_text: e.target.value }))}
placeholder="Intro text shown at the top of the email body"
rows={2}
className="search-input w-full py-2.5 px-3 text-sm resize-none"
/>
</div>
<div>
<label className="block text-xs font-body font-bold text-[#888] uppercase tracking-wider mb-1.5">Footer Text</label>
<textarea
value={form.footer_text}
onChange={(e) => setForm((f) => ({ ...f, footer_text: e.target.value }))}
placeholder="Closing message shown at the bottom of the email"
rows={2}
className="search-input w-full py-2.5 px-3 text-sm resize-none"
/>
</div>
</div>
{/* Article Blocks */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs font-body font-bold text-[#888] uppercase tracking-wider">Article Block Slots</label>
<button type="button" onClick={addBlock} className="text-xs text-[#3b82f6] hover:text-blue-400 font-body font-semibold transition-colors">
+ Add Slot
</button>
</div>
<p className="text-xs text-[#555] font-body mb-3">Define placeholder slots for articles. These will be pre-filled when using the template in Send Digest.</p>
<div className="space-y-2">
{form.article_blocks.map((block, i) => (
<div key={i} className="bg-[#0d0d0d] border border-[#252525] rounded-lg p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-body text-[#555]">Slot {i + 1}</span>
<div className="flex items-center gap-3">
{form.layout === 'featured' && (
<label className="flex items-center gap-1.5 cursor-pointer">
<input
type="checkbox"
checked={!!block.featured}
onChange={(e) => updateBlock(i, 'featured', e.target.checked)}
className="w-3 h-3 accent-[#3b82f6]"
/>
<span className="text-xs font-body text-[#888]">Featured</span>
</label>
)}
{form.article_blocks.length > 1 && (
<button type="button" onClick={() => removeBlock(i)} className="text-xs text-[#555] hover:text-red-400 font-body transition-colors">
Remove
</button>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<input
type="text"
value={block.title}
onChange={(e) => updateBlock(i, 'title', e.target.value)}
placeholder="Default title (optional)"
className="search-input py-1.5 px-2 text-xs col-span-2"
/>
<input
type="text"
value={block.category}
onChange={(e) => updateBlock(i, 'category', e.target.value)}
placeholder="Category"
className="search-input py-1.5 px-2 text-xs"
/>
<input
type="text"
value={block.url}
onChange={(e) => updateBlock(i, 'url', e.target.value)}
placeholder="/articles/slug"
className="search-input py-1.5 px-2 text-xs"
/>
</div>
</div>
))}
</div>
</div>
</div>
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-[#252525]">
<button onClick={() => setShowForm(false)} className="px-4 py-2 text-sm font-body text-[#888] hover:text-white transition-colors">
Cancel
</button>
<button
onClick={handleSave}
disabled={saving}
className="btn-subscribe py-2 px-5 text-sm font-semibold disabled:opacity-60">
{saving ? 'Saving…' : editingId ? 'Update Template' : 'Create Template'}
</button>
</div>
</div>
</div>
)}
{/* Preview Modal */}
{previewTemplate && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4" onClick={() => setPreviewTemplate(null)}>
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-lg" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525]">
<h2 className="font-display font-bold text-white text-base">Template Preview</h2>
<button onClick={() => setPreviewTemplate(null)} className="text-[#555] hover:text-white transition-colors">
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="px-6 py-5 space-y-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ backgroundColor: previewTemplate.accent_color + '20', border: `1px solid ${previewTemplate.accent_color}40` }}>
<span style={{ color: previewTemplate.accent_color }}>{LAYOUT_ICONS[previewTemplate.layout]}</span>
</div>
<div>
<p className="font-display font-bold text-white">{previewTemplate.name}</p>
<p className="text-xs text-[#555] font-body">{previewTemplate.description}</p>
</div>
</div>
<div className="grid grid-cols-3 gap-3 text-center">
<div className="bg-[#0d0d0d] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Layout</p>
<p className="text-sm font-body font-semibold text-white capitalize">{previewTemplate.layout}</p>
</div>
<div className="bg-[#0d0d0d] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Style</p>
<p className="text-sm font-body font-semibold text-white capitalize">{previewTemplate.style}</p>
</div>
<div className="bg-[#0d0d0d] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Slots</p>
<p className="text-sm font-body font-semibold text-white">{previewTemplate.article_blocks?.length || 0}</p>
</div>
</div>
{previewTemplate.subject_prefix && (
<div className="bg-[#0d0d0d] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Subject Prefix</p>
<p className="text-sm font-body text-[#e0e0e0]">{previewTemplate.subject_prefix}</p>
</div>
)}
{previewTemplate.header_text && (
<div className="bg-[#0d0d0d] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Header Text</p>
<p className="text-sm font-body text-[#e0e0e0]">{previewTemplate.header_text}</p>
</div>
)}
{previewTemplate.article_blocks?.length > 0 && (
<div>
<p className="text-xs text-[#555] font-body mb-2">Article Slots ({previewTemplate.article_blocks.length})</p>
<div className="space-y-1.5">
{previewTemplate.article_blocks.map((b, i) => (
<div key={i} className="flex items-center gap-2 bg-[#0d0d0d] rounded px-3 py-2">
<span className="text-xs text-[#555] font-body w-12 flex-shrink-0">Slot {i + 1}</span>
{b.featured && <span className="text-xs text-[#3b82f6] font-body bg-[#1a2535] px-1.5 py-0.5 rounded">Featured</span>}
{b.category && <span className="text-xs text-[#888] font-body">{b.category}</span>}
{b.title && <span className="text-xs text-[#e0e0e0] font-body truncate">{b.title}</span>}
</div>
))}
</div>
</div>
)}
<div className="flex gap-2 pt-2">
<Link
href={`/admin/newsletter?template=${previewTemplate.id}`}
className="flex-1 btn-subscribe py-2 text-sm font-semibold text-center">
Use in Digest
</Link>
{!previewTemplate.is_preset && (
<button
onClick={() => { setPreviewTemplate(null); openEdit(previewTemplate); }}
className="px-4 py-2 text-sm font-body text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors">
Edit
</button>
)}
</div>
</div>
</div>
</div>
)}
</div>
);
}
interface TemplateCardProps {
template: NewsletterTemplate;
onEdit: (t: NewsletterTemplate) => void;
onDelete: (id: string) => void;
onPreview: (t: NewsletterTemplate) => void;
deletingId: string | null;
}
function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: TemplateCardProps) {
return (
<div className="bg-[#111] border border-[#252525] rounded-xl overflow-hidden hover:border-[#333] transition-colors group">
{/* Color bar */}
<div className="h-1" style={{ backgroundColor: template.accent_color }} />
<div className="p-4">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0" style={{ backgroundColor: template.accent_color + '20' }}>
<span style={{ color: template.accent_color }}>{LAYOUT_ICONS[template.layout]}</span>
</div>
<div>
<p className="font-display font-bold text-white text-sm leading-tight">{template.name}</p>
{template.is_preset && (
<span className="text-xs text-[#555] font-body">Preset</span>
)}
</div>
</div>
</div>
{template.description && (
<p className="text-xs text-[#777] font-body mb-3 line-clamp-2">{template.description}</p>
)}
<div className="flex flex-wrap gap-1.5 mb-4">
<span className="text-xs bg-[#1a1a1a] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.layout}</span>
<span className="text-xs bg-[#1a1a1a] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.style}</span>
<span className="text-xs bg-[#1a1a1a] text-[#888] px-2 py-0.5 rounded-full font-body">{template.article_blocks?.length || 0} slots</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => onPreview(template)}
className="flex-1 py-1.5 text-xs font-body font-semibold text-[#3b82f6] border border-[#1e3a5f] rounded-lg hover:bg-[#1a2535] transition-colors">
Preview
</button>
<Link
href={`/admin/newsletter?template=${template.id}`}
className="flex-1 py-1.5 text-xs font-body font-semibold text-center text-white bg-[#1a2535] border border-[#1e3a5f] rounded-lg hover:bg-[#1e3a5f] transition-colors">
Use
</Link>
{!template.is_preset && (
<>
<button
onClick={() => onEdit(template)}
className="p-1.5 text-[#555] hover:text-white border border-[#252525] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125" />
</svg>
</button>
<button
onClick={() => onDelete(template.id)}
disabled={deletingId === template.id}
className="p-1.5 text-[#555] hover:text-red-400 border border-[#252525] rounded-lg transition-colors disabled:opacity-50">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</button>
</>
)}
</div>
</div>
</div>
);
}