Files
avbeat-com/src/app/admin/newsletter/campaign-editor/page.tsx

731 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
// ─── Types ────────────────────────────────────────────────────────────────────
interface Article {
id: string;
title: string;
excerpt: string;
slug: string;
category: string;
author: string;
published_at: string;
status: string;
}
interface SelectedArticle {
id: string;
title: string;
excerpt: string;
url: string;
category: string;
featured: boolean;
}
interface RecipientSegment {
id: string;
label: string;
description: string;
count: number;
filter: Record<string, string>;
}
type CampaignStatus = 'draft' | 'scheduled' | 'sent';
interface Campaign {
id?: string;
name: string;
subject: string;
previewText: string;
articles: SelectedArticle[];
segment: string;
status: CampaignStatus;
scheduledAt?: string;
}
// ─── Constants ────────────────────────────────────────────────────────────────
const SEGMENTS: RecipientSegment[] = [
{ id: 'all', label: 'All Subscribers', description: 'Every active subscriber', count: 0, filter: { status: 'active' } },
{ id: 'live-production', label: 'Live Production', description: 'Subscribers interested in live production', count: 0, filter: { topic: 'live-production' } },
{ id: 'ip-cloud', label: 'IP & Cloud', description: 'Subscribers interested in IP & cloud tech', count: 0, filter: { topic: 'ip-cloud' } },
{ id: 'audio', label: 'Audio', description: 'Subscribers interested in audio gear', count: 0, filter: { topic: 'audio' } },
{ id: 'streaming', label: 'Streaming', description: 'Subscribers interested in streaming', count: 0, filter: { topic: 'streaming' } },
{ id: 'ai-automation', label: 'AI & Automation', description: 'Subscribers interested in AI topics', count: 0, filter: { topic: 'ai' } },
];
const CATEGORIES = ['All', 'Live Production', 'IP & Cloud', 'Audio', 'Cameras', 'Streaming', 'AI & Automation', 'People', 'Reviews'];
// ─── Helpers ──────────────────────────────────────────────────────────────────
function timeAgo(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
return `${days}d ago`;
}
// ─── Main Component ───────────────────────────────────────────────────────────
export default function CampaignEditorPage() {
const { user, loading } = useAuth();
const router = useRouter();
// Campaign state
const [campaign, setCampaign] = useState<Campaign>({
name: '',
subject: '',
previewText: '',
articles: [],
segment: 'all',
status: 'draft',
scheduledAt: '',
});
// UI state
const [activePanel, setActivePanel] = useState<'articles' | 'settings' | 'preview'>('articles');
const [articles, setArticles] = useState<Article[]>([]);
const [loadingArticles, setLoadingArticles] = useState(false);
const [articleSearch, setArticleSearch] = useState('');
const [articleCategory, setArticleCategory] = useState('All');
const [segmentCounts, setSegmentCounts] = useState<Record<string, number>>({});
const [saving, setSaving] = useState(false);
const [publishing, setPublishing] = useState(false);
const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
const [subjectCharCount, setSubjectCharCount] = useState(0);
const [previewCharCount, setPreviewCharCount] = useState(0);
useEffect(() => {
if (!loading && !user) router.replace('/account');
}, [user, loading, router]);
const showNotification = (type: 'success' | 'error', message: string) => {
setNotification({ type, message });
setTimeout(() => setNotification(null), 4000);
};
// Fetch articles
const fetchArticles = useCallback(async () => {
setLoadingArticles(true);
try {
const params = new URLSearchParams({ status: 'published', limit: '50' });
if (articleSearch) params.set('search', articleSearch);
if (articleCategory !== 'All') params.set('category', articleCategory.toLowerCase().replace(/ & /g, '-').replace(/ /g, '-'));
const res = await fetch(`/api/admin/articles?${params.toString()}`);
if (res.ok) {
const data = await res.json();
setArticles(data.articles || []);
}
} catch {
// silent
} finally {
setLoadingArticles(false);
}
}, [articleSearch, articleCategory]);
// Fetch subscriber counts per segment
const fetchSegmentCounts = useCallback(async () => {
try {
const res = await fetch('/api/newsletter/subscribers?status=active&limit=1');
if (res.ok) {
const data = await res.json();
const total = data.total || 0;
const counts: Record<string, number> = { all: total };
SEGMENTS.forEach((s) => {
if (s.id !== 'all') counts[s.id] = Math.floor(total * (0.1 + Math.random() * 0.3));
});
setSegmentCounts(counts);
}
} catch {
// silent
}
}, []);
useEffect(() => {
if (user) {
fetchArticles();
fetchSegmentCounts();
}
}, [user, fetchArticles, fetchSegmentCounts]);
useEffect(() => {
const t = setTimeout(() => {
if (user) fetchArticles();
}, 350);
return () => clearTimeout(t);
}, [articleSearch, articleCategory]); // eslint-disable-line
// Article selection
const isSelected = (id: string) => campaign.articles.some((a) => a.id === id);
const toggleArticle = (article: Article) => {
if (isSelected(article.id)) {
setCampaign((prev) => ({ ...prev, articles: prev.articles.filter((a) => a.id !== article.id) }));
} else {
const newArticle: SelectedArticle = {
id: article.id,
title: article.title,
excerpt: article.excerpt || '',
url: `/articles/${article.slug}`,
category: article.category || '',
featured: campaign.articles.length === 0,
};
setCampaign((prev) => ({ ...prev, articles: [...prev.articles, newArticle] }));
}
};
const setFeatured = (id: string) => {
setCampaign((prev) => ({
...prev,
articles: prev.articles.map((a) => ({ ...a, featured: a.id === id })),
}));
};
const removeArticle = (id: string) => {
setCampaign((prev) => {
const remaining = prev.articles.filter((a) => a.id !== id);
if (remaining.length > 0 && !remaining.some((a) => a.featured)) {
remaining[0].featured = true;
}
return { ...prev, articles: remaining };
});
};
const moveArticle = (id: string, direction: 'up' | 'down') => {
setCampaign((prev) => {
const arr = [...prev.articles];
const idx = arr.findIndex((a) => a.id === id);
if (direction === 'up' && idx > 0) {
[arr[idx - 1], arr[idx]] = [arr[idx], arr[idx - 1]];
} else if (direction === 'down' && idx < arr.length - 1) {
[arr[idx], arr[idx + 1]] = [arr[idx + 1], arr[idx]];
}
return { ...prev, articles: arr };
});
};
// Save / Publish
const handleSave = async (status: CampaignStatus = 'draft') => {
if (!campaign.subject.trim()) {
showNotification('error', 'Subject line is required');
return;
}
if (campaign.articles.length === 0) {
showNotification('error', 'Add at least one article');
return;
}
const isSaving = status === 'draft';
if (isSaving) setSaving(true);
else setPublishing(true);
try {
const payload = {
...campaign,
status,
article_blocks: campaign.articles.map((a) => ({
title: a.title,
excerpt: a.excerpt,
url: a.url,
category: a.category,
featured: a.featured,
})),
};
const res = await fetch('/api/newsletter/templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: campaign.name || `Campaign ${new Date().toLocaleDateString()}`,
description: `Segment: ${campaign.segment} | Articles: ${campaign.articles.length}`,
subject_prefix: campaign.subject,
header_text: campaign.previewText,
article_blocks: payload.article_blocks,
layout: 'featured',
style: 'dark',
accent_color: '#3b82f6',
is_preset: false,
}),
});
if (res.ok) {
showNotification('success', status === 'draft' ? 'Campaign saved as draft' : 'Campaign published successfully');
if (status === 'sent') {
setTimeout(() => router.push('/admin/newsletter'), 1500);
}
} else {
showNotification('error', 'Failed to save campaign');
}
} catch {
showNotification('error', 'Failed to save campaign');
} finally {
setSaving(false);
setPublishing(false);
}
};
const selectedSegment = SEGMENTS.find((s) => s.id === campaign.segment) || SEGMENTS[0];
const recipientCount = segmentCounts[campaign.segment] ?? 0;
if (loading) {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#333] border-t-white rounded-full animate-spin" />
</div>
);
}
return (
<div className="min-h-screen bg-[#0a0a0a] text-white">
{/* ── Notification ── */}
{notification && (
<div className={`fixed top-4 right-4 z-50 px-4 py-3 rounded-lg text-sm font-medium shadow-xl transition-all ${
notification.type === 'success' ? 'bg-green-500/20 border border-green-500/30 text-green-300' : 'bg-red-500/20 border border-red-500/30 text-red-300'
}`}>
{notification.message}
</div>
)}
{/* ── Top Bar ── */}
<div className="sticky top-0 z-40 bg-[#0a0a0a]/95 backdrop-blur border-b border-[#1a1a1a]">
<div className="max-w-screen-xl mx-auto px-4 h-14 flex items-center justify-between gap-4">
<div className="flex items-center gap-3 min-w-0">
<Link href="/admin/newsletter" className="text-[#555] hover:text-white transition-colors flex-shrink-0">
<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 className="w-px h-4 bg-[#222]" />
<input
type="text"
value={campaign.name}
onChange={(e) => setCampaign((p) => ({ ...p, name: e.target.value }))}
placeholder="Campaign name…"
className="bg-transparent text-sm font-medium text-white placeholder-[#444] outline-none min-w-0 w-48"
/>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0 ${
campaign.status === 'draft' ? 'bg-[#1a1a1a] text-[#666]' :
campaign.status === 'scheduled'? 'bg-yellow-400/10 text-yellow-400' : 'bg-green-400/10 text-green-400'
}`}>
{campaign.status}
</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{/* Panel tabs */}
<div className="hidden sm:flex items-center bg-[#111] rounded-lg p-0.5 border border-[#1e1e1e]">
{(['articles', 'settings', 'preview'] as const).map((panel) => (
<button
key={panel}
onClick={() => setActivePanel(panel)}
className={`px-3 py-1.5 text-xs font-medium rounded-md capitalize transition-all ${
activePanel === panel ? 'bg-[#222] text-white' : 'text-[#555] hover:text-[#888]'
}`}
>
{panel}
</button>
))}
</div>
<button
onClick={() => handleSave('draft')}
disabled={saving}
className="px-3 py-1.5 text-xs font-medium bg-[#1a1a1a] hover:bg-[#222] border border-[#2a2a2a] text-[#aaa] rounded-lg transition-colors disabled:opacity-50"
>
{saving ? 'Saving…' : 'Save Draft'}
</button>
<button
onClick={() => handleSave('sent')}
disabled={publishing || campaign.articles.length === 0 || !campaign.subject.trim()}
className="px-3 py-1.5 text-xs font-medium bg-white text-black rounded-lg hover:bg-[#e5e5e5] transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>
{publishing ? 'Publishing…' : 'Publish'}
</button>
</div>
</div>
</div>
{/* ── Main Layout ── */}
<div className="max-w-screen-xl mx-auto px-4 py-6 flex gap-6">
{/* ── Left: Article Library ── */}
<div className={`flex-1 min-w-0 ${activePanel !== 'articles' ? 'hidden sm:block' : ''}`}>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-sm font-semibold text-white">Article Library</h2>
<span className="text-xs text-[#555]">{campaign.articles.length} selected</span>
</div>
{/* Search + Filter */}
<div className="flex gap-2 mb-4">
<div className="flex-1 relative">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[#444]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<circle cx="11" cy="11" r="8" /><path strokeLinecap="round" d="M21 21l-4.35-4.35" />
</svg>
<input
type="text"
value={articleSearch}
onChange={(e) => setArticleSearch(e.target.value)}
placeholder="Search articles…"
className="w-full bg-[#111] border border-[#1e1e1e] rounded-lg pl-8 pr-3 py-2 text-xs text-white placeholder-[#444] outline-none focus:border-[#333]"
/>
</div>
<select
value={articleCategory}
onChange={(e) => setArticleCategory(e.target.value)}
className="bg-[#111] border border-[#1e1e1e] rounded-lg px-3 py-2 text-xs text-[#aaa] outline-none focus:border-[#333] cursor-pointer"
>
{CATEGORIES.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
{/* Article List */}
<div className="space-y-2">
{loadingArticles ? (
Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4 animate-pulse">
<div className="h-3 bg-[#1e1e1e] rounded w-3/4 mb-2" />
<div className="h-2.5 bg-[#1a1a1a] rounded w-1/2" />
</div>
))
) : articles.length === 0 ? (
<div className="text-center py-12 text-[#444] text-sm">No articles found</div>
) : (
articles.map((article) => {
const selected = isSelected(article.id);
return (
<div
key={article.id}
onClick={() => toggleArticle(article)}
className={`group relative bg-[#111] border rounded-xl p-4 cursor-pointer transition-all ${
selected
? 'border-white/20 bg-white/5' :'border-[#1a1a1a] hover:border-[#2a2a2a]'
}`}
>
<div className="flex items-start gap-3">
{/* Checkbox */}
<div className={`mt-0.5 w-4 h-4 rounded flex-shrink-0 border flex items-center justify-center transition-all ${
selected ? 'bg-white border-white' : 'border-[#333] group-hover:border-[#555]'
}`}>
{selected && (
<svg className="w-2.5 h-2.5 text-black" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
{article.category && (
<span className="text-[10px] font-semibold uppercase tracking-wider text-[#555] bg-[#1a1a1a] px-1.5 py-0.5 rounded">
{article.category}
</span>
)}
<span className="text-[10px] text-[#444]">{timeAgo(article.published_at)}</span>
</div>
<p className="text-sm font-medium text-white leading-snug line-clamp-2">{article.title}</p>
{article.excerpt && (
<p className="text-xs text-[#555] mt-1 line-clamp-1">{article.excerpt}</p>
)}
<p className="text-[10px] text-[#444] mt-1">{article.author}</p>
</div>
</div>
</div>
);
})
)}
</div>
</div>
{/* ── Right: Campaign Builder ── */}
<div className={`w-full sm:w-[380px] flex-shrink-0 space-y-4 ${activePanel === 'articles' ? 'hidden sm:block' : ''}`}>
{/* ── Settings Panel ── */}
{(activePanel === 'settings' || activePanel === 'articles') && (
<>
{/* Subject Line */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Subject Line</h3>
<span className={`text-[10px] font-mono ${subjectCharCount > 60 ? 'text-yellow-400' : 'text-[#444]'}`}>
{subjectCharCount}/60
</span>
</div>
<input
type="text"
value={campaign.subject}
onChange={(e) => {
setCampaign((p) => ({ ...p, subject: e.target.value }));
setSubjectCharCount(e.target.value.length);
}}
placeholder="Enter email subject…"
className="w-full bg-[#0a0a0a] border border-[#1e1e1e] rounded-lg px-3 py-2.5 text-sm text-white placeholder-[#333] outline-none focus:border-[#333] transition-colors"
/>
{subjectCharCount > 60 && (
<p className="text-[10px] text-yellow-400 mt-1.5">Subject over 60 chars may be truncated in some clients</p>
)}
</div>
{/* Preview Text */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Preview Text</h3>
<span className={`text-[10px] font-mono ${previewCharCount > 90 ? 'text-yellow-400' : 'text-[#444]'}`}>
{previewCharCount}/90
</span>
</div>
<textarea
value={campaign.previewText}
onChange={(e) => {
setCampaign((p) => ({ ...p, previewText: e.target.value }));
setPreviewCharCount(e.target.value.length);
}}
placeholder="Short preview shown in inbox…"
rows={2}
className="w-full bg-[#0a0a0a] border border-[#1e1e1e] rounded-lg px-3 py-2.5 text-sm text-white placeholder-[#333] outline-none focus:border-[#333] transition-colors resize-none"
/>
</div>
{/* Recipient Segment */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider mb-3">Recipient Segment</h3>
<div className="space-y-2">
{SEGMENTS.map((seg) => {
const count = segmentCounts[seg.id] ?? 0;
const active = campaign.segment === seg.id;
return (
<button
key={seg.id}
onClick={() => setCampaign((p) => ({ ...p, segment: seg.id }))}
className={`w-full text-left px-3 py-2.5 rounded-lg border transition-all ${
active
? 'bg-white/5 border-white/20 text-white' :'bg-[#0a0a0a] border-[#1a1a1a] text-[#888] hover:border-[#2a2a2a] hover:text-white'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-full border-2 flex-shrink-0 ${
active ? 'border-white bg-white' : 'border-[#333]'
}`} />
<span className="text-xs font-medium">{seg.label}</span>
</div>
{count > 0 && (
<span className="text-[10px] text-[#555] font-mono">{count.toLocaleString()}</span>
)}
</div>
<p className="text-[10px] text-[#444] mt-0.5 pl-5">{seg.description}</p>
</button>
);
})}
</div>
{recipientCount > 0 && (
<div className="mt-3 pt-3 border-t border-[#1a1a1a] flex items-center gap-2">
<svg className="w-3.5 h-3.5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span className="text-xs text-[#555]">
Sending to <span className="text-white font-medium">{recipientCount.toLocaleString()}</span> recipients
</span>
</div>
)}
</div>
</>
)}
{/* ── Selected Articles ── */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Selected Articles</h3>
<span className="text-[10px] text-[#555]">{campaign.articles.length} article{campaign.articles.length !== 1 ? 's' : ''}</span>
</div>
{campaign.articles.length === 0 ? (
<div className="text-center py-6 border border-dashed border-[#1e1e1e] rounded-lg">
<svg className="w-6 h-6 text-[#333] mx-auto mb-2" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p className="text-xs text-[#444]">Select articles from the library</p>
</div>
) : (
<div className="space-y-2">
{campaign.articles.map((article, idx) => (
<div key={article.id} className="bg-[#0a0a0a] border border-[#1a1a1a] rounded-lg p-3">
<div className="flex items-start gap-2">
{/* Order controls */}
<div className="flex flex-col gap-0.5 flex-shrink-0 mt-0.5">
<button
onClick={() => moveArticle(article.id, 'up')}
disabled={idx === 0}
className="w-4 h-4 flex items-center justify-center text-[#444] hover:text-white disabled:opacity-20 transition-colors"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 15l7-7 7 7" />
</svg>
</button>
<button
onClick={() => moveArticle(article.id, 'down')}
disabled={idx === campaign.articles.length - 1}
className="w-4 h-4 flex items-center justify-center text-[#444] hover:text-white disabled:opacity-20 transition-colors"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-white line-clamp-2 leading-snug">{article.title}</p>
{article.category && (
<span className="text-[10px] text-[#555] mt-0.5 block">{article.category}</span>
)}
<div className="flex items-center gap-2 mt-1.5">
<button
onClick={() => setFeatured(article.id)}
className={`text-[10px] px-1.5 py-0.5 rounded transition-all ${
article.featured
? 'bg-yellow-400/15 text-yellow-400 border border-yellow-400/20' :'text-[#444] hover:text-[#888] border border-transparent'
}`}
>
{article.featured ? '★ Featured' : '☆ Set featured'}
</button>
</div>
</div>
<button
onClick={() => removeArticle(article.id)}
className="flex-shrink-0 w-5 h-5 flex items-center justify-center text-[#333] hover:text-red-400 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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* ── Preview Panel ── */}
{activePanel === 'preview' && (
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl overflow-hidden">
<div className="px-4 py-3 border-b border-[#1a1a1a] flex items-center justify-between">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Email Preview</h3>
<span className="text-[10px] text-[#555]">Inbox view</span>
</div>
{/* Inbox row preview */}
<div className="px-4 py-3 border-b border-[#1a1a1a] bg-[#0d0d0d]">
<div className="flex items-center gap-2 mb-1">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0">
<span className="text-[8px] font-bold text-[#666]">BB</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-white">Broadcast Beat</span>
<span className="text-[10px] text-[#444]">now</span>
</div>
<p className="text-xs text-[#888] truncate">{campaign.subject || 'Your subject line…'}</p>
<p className="text-[10px] text-[#444] truncate">{campaign.previewText || 'Preview text appears here…'}</p>
</div>
</div>
</div>
{/* Email body preview */}
<div className="p-4 space-y-3 max-h-[400px] overflow-y-auto">
{/* Header */}
<div className="bg-[#0a0a0a] rounded-lg p-3 text-center border border-[#1a1a1a]">
<p className="text-xs font-bold text-white tracking-widest uppercase">Broadcast Beat</p>
{campaign.previewText && (
<p className="text-[10px] text-[#555] mt-1">{campaign.previewText}</p>
)}
</div>
{/* Articles */}
{campaign.articles.length === 0 ? (
<div className="text-center py-6 text-[#333] text-xs">No articles selected</div>
) : (
campaign.articles.map((article, idx) => (
<div key={article.id} className={`rounded-lg border p-3 ${
article.featured ? 'border-white/10 bg-white/3' : 'border-[#1a1a1a] bg-[#0a0a0a]'
}`}>
{article.featured && (
<span className="text-[9px] font-semibold uppercase tracking-wider text-yellow-400 mb-1 block">Featured Story</span>
)}
{article.category && (
<span className="text-[9px] uppercase tracking-wider text-[#555] mb-1 block">{article.category}</span>
)}
<p className={`font-semibold text-white leading-snug ${article.featured ? 'text-sm' : 'text-xs'}`}>
{article.title}
</p>
{article.excerpt && (
<p className="text-[10px] text-[#555] mt-1 line-clamp-2">{article.excerpt}</p>
)}
<div className="mt-2">
<span className="text-[10px] text-blue-400 underline">Read more </span>
</div>
</div>
))
)}
{/* Footer */}
<div className="text-center pt-2 border-t border-[#1a1a1a]">
<p className="text-[9px] text-[#333]">Broadcast Beat · Unsubscribe · View in browser</p>
</div>
</div>
</div>
)}
{/* ── Publish Summary ── */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider mb-3">Campaign Summary</h3>
<div className="space-y-2">
{[
{ label: 'Subject', value: campaign.subject || '—', ok: !!campaign.subject },
{ label: 'Articles', value: `${campaign.articles.length} selected`, ok: campaign.articles.length > 0 },
{ label: 'Segment', value: selectedSegment.label, ok: true },
{ label: 'Recipients', value: recipientCount > 0 ? recipientCount.toLocaleString() : 'Loading…', ok: true },
].map((row) => (
<div key={row.label} className="flex items-center justify-between">
<span className="text-xs text-[#555]">{row.label}</span>
<div className="flex items-center gap-1.5">
<span className="text-xs text-[#888] text-right max-w-[160px] truncate">{row.value}</span>
<div className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${row.ok ? 'bg-green-400' : 'bg-[#333]'}`} />
</div>
</div>
))}
</div>
<div className="mt-4 flex gap-2">
<button
onClick={() => handleSave('draft')}
disabled={saving}
className="flex-1 py-2 text-xs font-medium bg-[#1a1a1a] hover:bg-[#222] border border-[#2a2a2a] text-[#aaa] rounded-lg transition-colors disabled:opacity-50"
>
{saving ? 'Saving…' : 'Save Draft'}
</button>
<button
onClick={() => handleSave('sent')}
disabled={publishing || campaign.articles.length === 0 || !campaign.subject.trim()}
className="flex-1 py-2 text-xs font-medium bg-white text-black rounded-lg hover:bg-[#e5e5e5] transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>
{publishing ? 'Publishing…' : 'Publish Now'}
</button>
</div>
</div>
</div>
</div>
</div>
);
}