'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; } 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({ name: '', subject: '', previewText: '', articles: [], segment: 'all', status: 'draft', scheduledAt: '', }); // UI state const [activePanel, setActivePanel] = useState<'articles' | 'settings' | 'preview'>('articles'); const [articles, setArticles] = useState([]); const [loadingArticles, setLoadingArticles] = useState(false); const [articleSearch, setArticleSearch] = useState(''); const [articleCategory, setArticleCategory] = useState('All'); const [segmentCounts, setSegmentCounts] = useState>({}); 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('/login?next=' + encodeURIComponent(window.location.pathname)); }, [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 = { 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: '#1D4ED8', 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 (
); } return (
{/* ── Notification ── */} {notification && (
{notification.message}
)} {/* ── Top Bar ── */}
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" /> {campaign.status}
{/* Panel tabs */}
{(['articles', 'settings', 'preview'] as const).map((panel) => ( ))}
{/* ── Main Layout ── */}
{/* ── Left: Article Library ── */}

Article Library

{campaign.articles.length} selected
{/* Search + Filter */}
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]" />
{/* Article List */}
{loadingArticles ? ( Array.from({ length: 5 }).map((_, i) => (
)) ) : articles.length === 0 ? (
No articles found
) : ( articles.map((article) => { const selected = isSelected(article.id); return (
toggleArticle(article)} className={`group relative bg-[#111] border rounded-xl p-4 cursor-pointer transition-all ${ selected ? 'border-white/20 bg-white/5' :'border-[#FFFFFF] hover:border-[#DCE6F2]' }`} >
{/* Checkbox */}
{selected && ( )}
{article.category && ( {article.category} )} {timeAgo(article.published_at)}

{article.title}

{article.excerpt && (

{article.excerpt}

)}

{article.author}

); }) )}
{/* ── Right: Campaign Builder ── */}
{/* ── Settings Panel ── */} {(activePanel === 'settings' || activePanel === 'articles') && ( <> {/* Subject Line */}

Subject Line

60 ? 'text-yellow-400' : 'text-[#444]'}`}> {subjectCharCount}/60
{ 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 && (

Subject over 60 chars may be truncated in some clients

)}
{/* Preview Text */}

Preview Text

90 ? 'text-yellow-400' : 'text-[#444]'}`}> {previewCharCount}/90