Files
ALwrity/frontend/src/components/BacklinkOutreach/BacklinkOutreachDashboard.tsx

1130 lines
64 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.
import React, { useCallback, useEffect, useState } from 'react';
import { useAuth } from '@clerk/clerk-react';
import { useBacklinkOutreachStore } from '../../stores/backlinkOutreachStore';
import {
listEmailTemplates,
generateEmailTemplate,
generateSubjectLines,
generateFollowUp,
personalizeEmail,
createEmailTemplate,
EmailTemplateRecord,
GenerateEmailRequest,
bulkUpdateLeadStatus,
updateLeadStatus,
fetchCampaignAnalyticsVolume,
fetchCampaignAnalyticsFunnel,
CampaignVolumePoint,
FunnelStage,
exportCampaignLeadsCsv,
exportCampaignAttemptsCsv,
exportCampaignRepliesCsv,
} from '../../api/backlinkOutreachApi';
import { showToastNotification } from '../../utils/toastNotifications';
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer } from 'recharts';
type Tab = 'campaigns' | 'discover' | 'leads' | 'composer' | 'analytics';
const STATUS_OPTIONS = ['discovered', 'contacted', 'replied', 'placed', 'bounced', 'unsubscribed'];
const STATUS_EXPLANATIONS: Record<string, string> = {
discovered: 'Lead found but not yet contacted',
contacted: 'Outreach email has been sent',
replied: 'Lead has responded to outreach',
placed: 'Guest post successfully published',
bounced: 'Email bounced — invalid or inactive',
unsubscribed: 'Lead opted out of future emails',
};
const GRADIENT_BG = 'linear-gradient(135deg, #0f0c29, #302b63, #24243e)';
const GRADIENT_CARD = 'linear-gradient(135deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03))';
const GRADIENT_PRIMARY = 'linear-gradient(135deg, #667eea, #764ba2)';
const GRADIENT_SECONDARY = 'linear-gradient(135deg, #f093fb, #f5576c)';
const GRADIENT_SUCCESS = 'linear-gradient(135deg, #43e97b, #38f9d7)';
const GRADIENT_WARNING = 'linear-gradient(135deg, #fa709a, #fee140)';
const TooltipWrap: React.FC<{ text: string; children: React.ReactNode }> = ({ text, children }) => {
const [show, setShow] = useState(false);
return (
<span style={{ position: 'relative', display: 'inline-flex' }}
onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>
{children}
{show && (
<span style={{
position: 'absolute', bottom: 'calc(100% + 8px)', left: '50%', transform: 'translateX(-50%)',
padding: '8px 12px', background: '#1a1a2e', color: '#fff', borderRadius: '8px',
fontSize: '12px', lineHeight: 1.4, whiteSpace: 'normal', zIndex: 1000,
boxShadow: '0 8px 24px rgba(0,0,0,0.3)', pointerEvents: 'none',
maxWidth: '280px',
}}>
{text}
<span style={{
position: 'absolute', top: '100%', left: '50%', transform: 'translateX(-50%)',
border: '6px solid transparent', borderTopColor: '#1a1a2e',
}} />
</span>
)}
</span>
);
};
const cardSx: React.CSSProperties = {
background: GRADIENT_CARD, backdropFilter: 'blur(20px)',
border: '1px solid rgba(255,255,255,0.1)', borderRadius: '12px',
boxShadow: '0 8px 32px rgba(0,0,0,0.15)',
};
const inputSx: React.CSSProperties = {
width: '100%', padding: '12px 16px',
background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)',
borderRadius: '8px', color: '#fff', fontSize: '14px', outline: 'none',
};
const selectSx: React.CSSProperties = {
...inputSx, cursor: 'pointer',
};
const btnBase: React.CSSProperties = {
border: 'none', borderRadius: '8px', cursor: 'pointer', fontWeight: 600,
fontSize: '14px', padding: '10px 24px', transition: 'all 0.2s',
};
const BacklinkOutreachDashboard: React.FC = () => {
const { userId } = useAuth();
const workspaceId = userId || 'default';
const {
campaigns, selectedCampaign, discoveredOpportunities,
isLoading, isDiscovering, error,
fetchCampaigns, createCampaign, selectCampaign,
deepDiscover, clearDiscoveries,
attempts, replies, followups, analytics,
fetchAttempts, fetchReplies, fetchFollowUps, fetchAnalytics,
} = useBacklinkOutreachStore();
const [activeTab, setActiveTab] = useState<Tab>('campaigns');
const [newCampaignName, setNewCampaignName] = useState('');
const [keyword, setKeyword] = useState('');
const [discoverCampaignId, setDiscoverCampaignId] = useState('');
const [templates, setTemplates] = useState<EmailTemplateRecord[]>([]);
const [selectedTemplateId, setSelectedTemplateId] = useState('');
const [topic, setTopic] = useState('');
const [targetSite, setTargetSite] = useState('');
const [tone, setTone] = useState<'professional' | 'friendly' | 'casual' | 'formal'>('professional');
const [subject, setSubject] = useState('');
const [body, setBody] = useState('');
const [subjectSuggestions, setSubjectSuggestions] = useState<string[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
const [leadName, setLeadName] = useState('');
const [leadSite, setLeadSite] = useState('');
const [leadContentTopic, setLeadContentTopic] = useState('');
const [followUpDays, setFollowUpDays] = useState(7);
const [replyContext, setReplyContext] = useState('');
const [templateName, setTemplateName] = useState('');
const [selectedLeadIds, setSelectedLeadIds] = useState<Set<string>>(new Set());
const [bulkStatus, setBulkStatus] = useState('contacted');
const [volumeData, setVolumeData] = useState<CampaignVolumePoint[]>([]);
const [funnelData, setFunnelData] = useState<FunnelStage[]>([]);
const [analyticsDays, setAnalyticsDays] = useState(30);
const [isAnalyticsLoading, setIsAnalyticsLoading] = useState(false);
const [isStatusUpdating, setIsStatusUpdating] = useState(false);
const [isExporting, setIsExporting] = useState<string | null>(null);
useEffect(() => {
fetchCampaigns(workspaceId);
}, [fetchCampaigns, workspaceId]);
useEffect(() => {
listEmailTemplates().then(r => setTemplates(r.templates)).catch(() => showToastNotification('Failed to load email templates', 'error'));
}, []);
useEffect(() => {
if (selectedCampaign) {
const cid = selectedCampaign.campaign_id;
fetchAttempts(cid);
fetchReplies(cid);
fetchFollowUps(cid);
fetchAnalytics(cid);
}
}, [selectedCampaign, fetchAttempts, fetchReplies, fetchFollowUps, fetchAnalytics]);
useEffect(() => {
if (!selectedCampaign) return;
let cancelled = false;
setIsAnalyticsLoading(true);
Promise.all([
fetchCampaignAnalyticsVolume(selectedCampaign.campaign_id, analyticsDays),
fetchCampaignAnalyticsFunnel(selectedCampaign.campaign_id),
]).then(([vol, funnel]) => {
if (!cancelled) {
setVolumeData(vol.volume);
setFunnelData(funnel.stages);
setIsAnalyticsLoading(false);
}
}).catch(() => {
if (!cancelled) {
showToastNotification('Failed to load analytics data', 'error');
setIsAnalyticsLoading(false);
}
});
return () => { cancelled = true; };
}, [analyticsDays, selectedCampaign?.campaign_id]);
const handleCreateCampaign = useCallback(async () => {
if (!newCampaignName.trim()) return;
const id = await createCampaign(workspaceId, newCampaignName.trim());
if (id) {
setNewCampaignName('');
setActiveTab('discover');
}
}, [newCampaignName, createCampaign]);
const handleDiscover = useCallback(async () => {
if (!keyword.trim()) return;
await deepDiscover(keyword.trim(), 15);
}, [keyword, deepDiscover]);
const handleDiscoverAndSave = useCallback(async () => {
if (!keyword.trim() || !discoverCampaignId) return;
await deepDiscover(keyword.trim(), 15, discoverCampaignId);
}, [keyword, discoverCampaignId, deepDiscover]);
const handleSelectCampaign = useCallback(async (campaignId: string) => {
await selectCampaign(campaignId);
setActiveTab('leads');
}, [selectCampaign]);
const handleGenerate = useCallback(async () => {
if (!topic.trim()) return;
setIsGenerating(true);
try {
const payload: GenerateEmailRequest = {
topic: topic.trim(),
target_site: targetSite.trim() || undefined,
tone,
existing_template_id: selectedTemplateId || undefined,
};
const result = await generateEmailTemplate(payload);
setSubject(result.subject);
setBody(result.body);
setSubjectSuggestions([]);
} catch (e) {
showToastNotification('Email generation failed', 'error');
} finally {
setIsGenerating(false);
}
}, [topic, targetSite, tone, selectedTemplateId]);
const handleSuggestSubjects = useCallback(async () => {
if (!body.trim()) return;
setIsGenerating(true);
try {
const result = await generateSubjectLines({ body: body.trim() });
setSubjectSuggestions(result.subjects);
} catch (e) {
showToastNotification('Failed to generate subject lines', 'error');
} finally {
setIsGenerating(false);
}
}, [body]);
const handlePersonalize = useCallback(async () => {
if (!leadName.trim() || !leadSite.trim() || !leadContentTopic.trim() || !topic.trim()) return;
setIsGenerating(true);
try {
const result = await personalizeEmail({
lead_name: leadName.trim(),
lead_site: leadSite.trim(),
lead_content_topic: leadContentTopic.trim(),
pitch_topic: topic.trim(),
existing_body: body,
});
setSubject(result.subject);
setBody(result.body);
} catch (e) {
showToastNotification('Personalization failed', 'error');
} finally {
setIsGenerating(false);
}
}, [leadName, leadSite, leadContentTopic, topic, body]);
const handleFollowUp = useCallback(async () => {
if (!subject.trim() || !body.trim()) return;
setIsGenerating(true);
try {
const result = await generateFollowUp({
original_subject: subject.trim(),
original_body: body.trim(),
days_elapsed: followUpDays,
reply_context: replyContext.trim() || undefined,
});
setSubject(result.subject);
setBody(result.body);
} catch (e) {
showToastNotification('Follow-up generation failed', 'error');
} finally {
setIsGenerating(false);
}
}, [subject, body, followUpDays, replyContext]);
const handleSaveTemplate = useCallback(async () => {
if (!templateName.trim() || !subject.trim() || !body.trim()) return;
try {
await createEmailTemplate({
name: templateName.trim(),
subject_template: subject,
body_template: body,
variables: ['lead_name', 'lead_site', 'pitch_topic'],
});
setTemplateName('');
const updated = await listEmailTemplates();
setTemplates(updated.templates);
} catch (e) {
showToastNotification('Failed to save template', 'error');
}
}, [templateName, subject, body]);
const applySuggestion = (s: string) => {
setSubject(s);
setSubjectSuggestions([]);
};
const toggleLeadSelection = (leadId: string) => {
setSelectedLeadIds(prev => {
const next = new Set(prev);
if (next.has(leadId)) next.delete(leadId);
else next.add(leadId);
return next;
});
};
const toggleAllLeads = () => {
if (!selectedCampaign) return;
const all = selectedCampaign.leads;
setSelectedLeadIds(prev =>
prev.size === all.length ? new Set() : new Set(all.map(l => l.lead_id))
);
};
const handleSingleStatusUpdate = async (leadId: string, status: string) => {
setIsStatusUpdating(true);
try {
await updateLeadStatus(leadId, { status });
showToastNotification(`Status updated to "${status}"`, 'success');
await selectCampaign(selectedCampaign!.campaign_id);
} catch (e) {
showToastNotification('Status update failed', 'error');
} finally {
setIsStatusUpdating(false);
}
};
const handleBulkStatusUpdate = async () => {
if (selectedLeadIds.size === 0) return;
setIsStatusUpdating(true);
try {
const result = await bulkUpdateLeadStatus({ lead_ids: Array.from(selectedLeadIds), status: bulkStatus });
if (result.failed.length > 0) {
showToastNotification(`Updated ${result.updated} leads; ${result.failed.length} failed`, 'warning');
} else {
showToastNotification(`Updated ${result.updated} leads to "${bulkStatus}"`, 'success');
}
setSelectedLeadIds(new Set());
await selectCampaign(selectedCampaign!.campaign_id);
} catch (e) {
showToastNotification('Bulk status update failed', 'error');
} finally {
setIsStatusUpdating(false);
}
};
const handleExportCsv = useCallback(async (type: 'leads' | 'attempts' | 'replies') => {
if (!selectedCampaign || isExporting) return;
setIsExporting(type);
try {
const fn = type === 'leads' ? exportCampaignLeadsCsv : type === 'attempts' ? exportCampaignAttemptsCsv : exportCampaignRepliesCsv;
const blob = await fn(selectedCampaign.campaign_id);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${type}_${selectedCampaign.campaign_id}.csv`;
a.click();
window.URL.revokeObjectURL(url);
showToastNotification(`${type.charAt(0).toUpperCase() + type.slice(1)} exported`, 'success');
} catch (e: any) {
showToastNotification(e?.message || 'Export failed', 'error');
} finally {
setIsExporting(null);
}
}, [selectedCampaign, isExporting]);
const handleTabChange = useCallback((tab: Tab) => {
setActiveTab(tab);
}, []);
const renderStatusBadge = (status: string) => {
const styles: Record<string, { bg: string; fg: string }> = {
discovered: { bg: 'rgba(102,126,234,0.2)', fg: '#8b9cf7' },
contacted: { bg: 'rgba(240,147,251,0.2)', fg: '#f093fb' },
replied: { bg: 'rgba(67,233,123,0.2)', fg: '#43e97b' },
placed: { bg: 'rgba(67,233,123,0.3)', fg: '#38f9d7' },
bounced: { bg: 'rgba(245,87,108,0.2)', fg: '#f5576c' },
unsubscribed: { bg: 'rgba(254,225,64,0.15)', fg: '#fee140' },
};
const s = styles[status] || { bg: 'rgba(255,255,255,0.1)', fg: '#aaa' };
return (
<TooltipWrap text={STATUS_EXPLANATIONS[status] || ''}>
<span style={{
padding: '3px 10px', borderRadius: '12px', fontSize: '11px', fontWeight: 600,
background: s.bg, color: s.fg, border: `1px solid ${s.fg}33`,
}}>{status}</span>
</TooltipWrap>
);
};
const tabMeta: { key: Tab; label: string; desc: string }[] = [
{ key: 'campaigns', label: 'Campaigns', desc: 'Create and manage outreach campaigns' },
{ key: 'discover', label: 'Discover', desc: 'AI-powered search for guest post opportunities' },
{ key: 'leads', label: 'Leads', desc: 'Track leads, send outreach, and manage replies' },
{ key: 'composer', label: 'Composer', desc: 'AI email composer with smart suggestions' },
{ key: 'analytics', label: 'Analytics', desc: 'Campaign performance metrics and exports' },
];
const SectionHeader: React.FC<{ title: string; subtitle: string }> = ({ title, subtitle }) => (
<div style={{ marginBottom: '16px' }}>
<h3 style={{ margin: 0, background: GRADIENT_PRIMARY, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', fontSize: '18px' }}>{title}</h3>
<p style={{ margin: '4px 0 0', fontSize: '13px', color: 'rgba(255,255,255,0.5)' }}>{subtitle}</p>
</div>
);
return (
<div style={{
minHeight: '100vh',
background: GRADIENT_BG,
padding: '32px',
}}>
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
{/* Header */}
<div style={{ marginBottom: '28px' }}>
<h1 style={{
margin: 0, fontSize: '28px', fontWeight: 700,
background: GRADIENT_PRIMARY, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
}}>Backlink Outreach</h1>
<p style={{ margin: '6px 0 0', color: 'rgba(255,255,255,0.45)', fontSize: '14px' }}>
AI-powered guest post outreach platform discover opportunities, manage campaigns, compose emails, and track results.
</p>
</div>
{/* Tab bar */}
<div style={{ display: 'flex', gap: '4px', marginBottom: '24px', flexWrap: 'wrap', padding: '4px', background: 'rgba(255,255,255,0.04)', borderRadius: '12px' }}>
{tabMeta.map(({ key, label, desc }) => (
<TooltipWrap key={key} text={desc}>
<button onClick={() => handleTabChange(key)}
style={{
...btnBase, padding: '10px 20px', fontSize: '13px',
background: activeTab === key ? GRADIENT_PRIMARY : 'transparent',
color: activeTab === key ? '#fff' : 'rgba(255,255,255,0.5)',
boxShadow: activeTab === key ? '0 4px 15px rgba(102,126,234,0.4)' : 'none',
}}>
{label}
</button>
</TooltipWrap>
))}
</div>
{error && (
<div style={{ padding: '14px 18px', background: 'rgba(245,87,108,0.15)', border: '1px solid rgba(245,87,108,0.3)', borderRadius: '10px', color: '#f5576c', marginBottom: '16px', fontSize: '13px' }}>
{error}
</div>
)}
{/* === CAMPAIGNS TAB === */}
{activeTab === 'campaigns' && (
<div style={{ ...cardSx, padding: '24px' }}>
<SectionHeader title="Campaigns" subtitle="Organize your outreach efforts into campaigns. Each campaign groups leads, emails, and analytics together." />
<div style={{ display: 'flex', gap: '12px', marginBottom: '20px' }}>
<input type="text" value={newCampaignName} onChange={(e) => setNewCampaignName(e.target.value)}
placeholder="Enter campaign name (e.g. 'Tech Bloggers Q3')"
style={{ ...inputSx, flex: 1 }} />
<TooltipWrap text="Creates a new campaign and takes you to the Discover tab to find leads">
<button onClick={handleCreateCampaign} disabled={!newCampaignName.trim() || isLoading}
style={{ ...btnBase, background: GRADIENT_PRIMARY, color: '#fff', opacity: !newCampaignName.trim() || isLoading ? 0.5 : 1 }}>
{isLoading ? 'Creating...' : 'Create Campaign'}
</button>
</TooltipWrap>
</div>
{campaigns.length === 0 && !isLoading && (
<p style={{ color: 'rgba(255,255,255,0.3)', textAlign: 'center', padding: '40px 0' }}>
No campaigns yet. Create one above to get started.
</p>
)}
{campaigns.map((c) => (
<TooltipWrap key={c.campaign_id} text="Click to view leads and manage this campaign">
<div onClick={() => handleSelectCampaign(c.campaign_id)}
style={{
padding: '16px', marginBottom: '8px', borderRadius: '10px', cursor: 'pointer',
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)',
transition: 'all 0.2s',
}}>
<div style={{ fontWeight: 600, color: '#fff' }}>{c.name}</div>
<div style={{ fontSize: '13px', color: 'rgba(255,255,255,0.4)', marginTop: '4px' }}>
Status: {c.status} {c.created_at && <> &middot; Created {new Date(c.created_at).toLocaleDateString()}</>}
</div>
</div>
</TooltipWrap>
))}
{isLoading && <p style={{ color: 'rgba(255,255,255,0.4)' }}>Loading...</p>}
</div>
)}
{/* === DISCOVER TAB === */}
{activeTab === 'discover' && (
<div style={{ ...cardSx, padding: '24px' }}>
<SectionHeader title="Discover Opportunities" subtitle="AI searches the web using Exa neural search + DuckDuckGo to find websites accepting guest posts in your niche." />
<div style={{ display: 'flex', gap: '12px', marginBottom: '20px', flexWrap: 'wrap' }}>
<input type="text" value={keyword} onChange={(e) => setKeyword(e.target.value)}
placeholder="e.g. 'AI marketing', 'SaaS growth', 'digital nomad'"
style={{ ...inputSx, flex: 1, minWidth: '220px' }} />
<TooltipWrap text="Searches multiple guest-post query patterns (e.g. 'write for us', 'guest contributor') across search engines">
<button onClick={handleDiscover} disabled={!keyword.trim() || isDiscovering}
style={{ ...btnBase, background: GRADIENT_SUCCESS, color: '#1a1a2e', opacity: !keyword.trim() || isDiscovering ? 0.5 : 1 }}>
{isDiscovering ? 'Searching...' : 'Discover'}
</button>
</TooltipWrap>
</div>
{isDiscovering && (
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: '13px' }}>
Searching across Exa (neural) + DuckDuckGo... This may take 1020 seconds.
</p>
)}
{discoveredOpportunities.length > 0 && (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px', gap: '12px', flexWrap: 'wrap' }}>
<span style={{ fontWeight: 600, color: '#fff' }}>{discoveredOpportunities.length} opportunities found</span>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<TooltipWrap text="Save discovered leads directly to a campaign for tracking">
<select value={discoverCampaignId} onChange={(e) => setDiscoverCampaignId(e.target.value)}
style={{ ...selectSx, padding: '8px 12px', fontSize: '13px', minWidth: '160px' }}>
<option value="">-- Select campaign --</option>
{campaigns.map((c) => <option key={c.campaign_id} value={c.campaign_id}>{c.name}</option>)}
</select>
</TooltipWrap>
<TooltipWrap text="Saves all discovered leads to your selected campaign">
<button onClick={handleDiscoverAndSave}
disabled={!keyword.trim() || !discoverCampaignId}
style={{ ...btnBase, padding: '8px 16px', fontSize: '13px', background: GRADIENT_PRIMARY, color: '#fff', opacity: discoverCampaignId ? 1 : 0.4 }}>
Save to Campaign
</button>
</TooltipWrap>
<TooltipWrap text="Clears current search results">
<button onClick={clearDiscoveries}
style={{ ...btnBase, padding: '8px 16px', fontSize: '13px', background: 'rgba(255,255,255,0.06)', color: 'rgba(255,255,255,0.6)' }}>
Clear
</button>
</TooltipWrap>
</div>
</div>
{discoveredOpportunities.map((opp, i) => (
<div key={`${opp.url}-${i}`} style={{
padding: '16px', marginBottom: '8px', borderRadius: '10px',
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)',
}}>
<div style={{ fontWeight: 600, marginBottom: '4px' }}>
<a href={opp.url} target="_blank" rel="noopener noreferrer"
style={{ color: '#8b9cf7', textDecoration: 'none' }}>
{opp.page_title || opp.domain}
</a>
</div>
<div style={{ fontSize: '13px', color: 'rgba(255,255,255,0.4)', marginBottom: '4px' }}>{opp.domain}</div>
{opp.snippet && <div style={{ fontSize: '13px', color: 'rgba(255,255,255,0.6)', marginBottom: '8px' }}>{opp.snippet.slice(0, 200)}...</div>}
<div style={{ display: 'flex', gap: '16px', fontSize: '12px', flexWrap: 'wrap' }}>
<TooltipWrap text="How relevant this site is to your keyword based on content analysis">
<span style={{ color: 'rgba(255,255,255,0.5)' }}>Quality: <strong style={{ color: '#43e97b' }}>{(opp.quality_score * 100).toFixed(0)}%</strong></span>
</TooltipWrap>
<TooltipWrap text="Confidence that this site accepts guest posts, based on page signals">
<span style={{ color: 'rgba(255,255,255,0.5)' }}>Confidence: <strong style={{ color: '#8b9cf7' }}>{(opp.confidence_score * 100).toFixed(0)}%</strong></span>
</TooltipWrap>
{opp.has_guest_post_guidelines && (
<TooltipWrap text="This site has a dedicated guest post guidelines page">
<span style={{ color: '#43e97b' }}>Has guidelines</span>
</TooltipWrap>
)}
{opp.email && (
<TooltipWrap text="Contact email found on the site">
<span style={{ color: '#8b9cf7' }}>Email found</span>
</TooltipWrap>
)}
</div>
</div>
))}
</div>
)}
{!isDiscovering && discoveredOpportunities.length === 0 && (
<p style={{ color: 'rgba(255,255,255,0.3)', textAlign: 'center', padding: '40px 0', fontSize: '13px' }}>
Enter a keyword above and click Discover to find guest post opportunities.
</p>
)}
</div>
)}
{/* === LEADS TAB === */}
{activeTab === 'leads' && (
<div style={{ ...cardSx, padding: '24px' }}>
{selectedCampaign ? (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px', flexWrap: 'wrap', gap: '8px' }}>
<div>
<h3 style={{ margin: 0, color: '#fff' }}>{selectedCampaign.name}</h3>
<p style={{ fontSize: '13px', color: 'rgba(255,255,255,0.4)', margin: '4px 0 0' }}>
{selectedCampaign.lead_count} leads &middot; Status: {selectedCampaign.status}
</p>
</div>
<TooltipWrap text="Switch to a different campaign">
<select onChange={(e) => { const c = campaigns.find(x => x.campaign_id === e.target.value); if (c) handleSelectCampaign(c.campaign_id); }}
value={selectedCampaign.campaign_id} style={{ ...selectSx, padding: '8px 12px', fontSize: '13px', minWidth: '180px' }}>
{campaigns.map((c) => <option key={c.campaign_id} value={c.campaign_id}>{c.name}</option>)}
</select>
</TooltipWrap>
</div>
{/* Analytics cards */}
{analytics && (
<div style={{ display: 'flex', gap: '12px', marginBottom: '20px', flexWrap: 'wrap' }}>
{[{ label: 'Sent', value: analytics.send_volume, grad: GRADIENT_PRIMARY },
{ label: 'Response Rate', value: `${(analytics.response_rate * 100).toFixed(1)}%`, grad: GRADIENT_SUCCESS },
{ label: 'Replies', value: analytics.reply_count, grad: GRADIENT_WARNING },
{ label: 'Placement', value: `${(analytics.placement_rate * 100).toFixed(1)}%`, grad: 'linear-gradient(135deg, #a18cd1, #fbc2eb)' },
{ label: 'Blocked', value: analytics.blocked_count, grad: GRADIENT_SECONDARY },
].map(({ label, value, grad }) => (
<TooltipWrap key={label} text={`${label}: ${value}`}>
<div style={{
flex: 1, minWidth: '100px', padding: '14px', borderRadius: '10px', textAlign: 'center',
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)',
}}>
<div style={{ fontSize: '22px', fontWeight: 700, background: grad, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>{value}</div>
<div style={{ fontSize: '11px', color: 'rgba(255,255,255,0.4)', marginTop: '2px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>{label}</div>
</div>
</TooltipWrap>
))}
</div>
)}
{/* Reply classification */}
{analytics && Object.keys(analytics.reply_classification).length > 0 && (
<div style={{ marginBottom: '16px', padding: '12px 16px', background: 'rgba(255,255,255,0.03)', borderRadius: '10px' }}>
<div style={{ fontSize: '12px', fontWeight: 600, color: 'rgba(255,255,255,0.5)', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '8px' }}>Reply Classification</div>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
{Object.entries(analytics.reply_classification).map(([cls, count]) => (
<TooltipWrap key={cls} text={`${count} replies classified as "${cls}"`}>
<span style={{ padding: '4px 12px', background: 'rgba(255,255,255,0.06)', borderRadius: '8px', fontSize: '12px', color: 'rgba(255,255,255,0.7)' }}>
<strong>{cls}</strong>: {count}
</span>
</TooltipWrap>
))}
</div>
</div>
)}
{/* Bulk actions */}
{selectedCampaign.leads.length > 0 && (
<div style={{
display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '12px',
padding: '10px 16px', background: 'rgba(255,255,255,0.03)', borderRadius: '10px', flexWrap: 'wrap',
}}>
<label style={{ fontSize: '13px', display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer', color: 'rgba(255,255,255,0.7)' }}>
<input type="checkbox" checked={selectedLeadIds.size === selectedCampaign.leads.length && selectedCampaign.leads.length > 0}
onChange={toggleAllLeads} style={{ accentColor: '#667eea' }} />
{selectedLeadIds.size > 0 ? `${selectedLeadIds.size} selected` : 'Select all'}
</label>
{selectedLeadIds.size > 0 && (
<>
<TooltipWrap text="Choose the new status for all selected leads">
<select value={bulkStatus} onChange={(e) => setBulkStatus(e.target.value)}
style={{ ...selectSx, padding: '6px 10px', fontSize: '12px', minWidth: '130px' }}>
{STATUS_OPTIONS.map((s) => <option key={s} value={s}>{s}</option>)}
</select>
</TooltipWrap>
<TooltipWrap text="Updates the status of all selected leads in one click">
<button onClick={handleBulkStatusUpdate} disabled={isStatusUpdating}
style={{ ...btnBase, padding: '6px 16px', fontSize: '12px', background: GRADIENT_PRIMARY, color: '#fff', opacity: isStatusUpdating ? 0.5 : 1 }}>
{isStatusUpdating ? 'Updating...' : 'Update Status'}
</button>
</TooltipWrap>
</>
)}
</div>
)}
{selectedCampaign.leads.length === 0 && (
<p style={{ color: 'rgba(255,255,255,0.3)', textAlign: 'center', padding: '40px 0', fontSize: '13px' }}>
No leads yet. Go to the <strong>Discover</strong> tab to find and save opportunities.
</p>
)}
{/* Lead cards */}
{selectedCampaign.leads.map((lead) => (
<div key={lead.lead_id} style={{
padding: '16px', marginBottom: '8px', borderRadius: '10px',
background: selectedLeadIds.has(lead.lead_id) ? 'rgba(102,126,234,0.1)' : 'rgba(255,255,255,0.03)',
border: selectedLeadIds.has(lead.lead_id) ? '1px solid rgba(102,126,234,0.3)' : '1px solid rgba(255,255,255,0.06)',
}}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '10px' }}>
<input type="checkbox" checked={selectedLeadIds.has(lead.lead_id)}
onChange={() => toggleLeadSelection(lead.lead_id)} style={{ marginTop: '4px', accentColor: '#667eea' }} />
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, color: '#fff', marginBottom: '2px' }}>{lead.page_title || lead.domain}</div>
<div style={{ fontSize: '13px', color: 'rgba(255,255,255,0.4)', marginBottom: '4px' }}>
{lead.url && <a href={lead.url} target="_blank" rel="noopener noreferrer" style={{ color: '#8b9cf7' }}>{lead.url}</a>}
</div>
<div style={{ display: 'flex', gap: '10px', fontSize: '12px', color: 'rgba(255,255,255,0.4)', alignItems: 'center', flexWrap: 'wrap' }}>
{renderStatusBadge(lead.status)}
{lead.email && <span>Email: {lead.email}</span>}
<span>Source: {lead.discovery_source}</span>
</div>
<div style={{ display: 'flex', gap: '4px', marginTop: '10px', flexWrap: 'wrap' }}>
{STATUS_OPTIONS.map((s) => (
<TooltipWrap key={s} text={STATUS_EXPLANATIONS[s] || ''}>
<button onClick={() => handleSingleStatusUpdate(lead.lead_id, s)}
disabled={lead.status === s || isStatusUpdating}
style={{
padding: '4px 12px', fontSize: '11px', borderRadius: '20px', border: '1px solid',
borderColor: lead.status === s ? '#667eea' : 'rgba(255,255,255,0.15)',
background: lead.status === s ? GRADIENT_PRIMARY : 'transparent',
color: lead.status === s ? '#fff' : 'rgba(255,255,255,0.5)',
cursor: lead.status === s ? 'default' : 'pointer', fontWeight: lead.status === s ? 600 : 400,
transition: 'all 0.2s',
}}>
{s}
</button>
</TooltipWrap>
))}
</div>
{attempts.filter(a => a.lead_id === lead.lead_id).slice(0, 1).map(a => (
<div key={a.attempt_id} style={{ marginTop: '8px', padding: '8px 12px', background: 'rgba(255,255,255,0.04)', borderRadius: '8px', fontSize: '12px' }}>
<span style={{ color: 'rgba(255,255,255,0.5)' }}>Latest: {a.subject} </span>
{renderStatusBadge(a.status)}
{a.sent_at && <span style={{ color: 'rgba(255,255,255,0.3)', marginLeft: '8px' }}>{new Date(a.sent_at).toLocaleString()}</span>}
</div>
))}
</div>
</div>
</div>
))}
{/* Attempt history */}
{attempts.length > 0 && (
<div style={{ marginTop: '24px' }}>
<SectionHeader title="Attempt History" subtitle="Record of all outreach emails sent and their delivery statuses." />
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead>
<tr style={{ background: 'rgba(255,255,255,0.04)' }}>
{['Subject', 'Status', 'Sender', 'Sent At'].map(h => (
<th key={h} style={{ padding: '10px 12px', borderBottom: '1px solid rgba(255,255,255,0.08)', textAlign: 'left', color: 'rgba(255,255,255,0.4)', fontWeight: 500, fontSize: '12px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{attempts.map((a) => (
<tr key={a.attempt_id}>
<td style={{ padding: '10px 12px', borderBottom: '1px solid rgba(255,255,255,0.04)', color: '#fff', maxWidth: '250px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{a.subject}</td>
<td style={{ padding: '10px 12px', borderBottom: '1px solid rgba(255,255,255,0.04)' }}>{renderStatusBadge(a.status)}</td>
<td style={{ padding: '10px 12px', borderBottom: '1px solid rgba(255,255,255,0.04)', color: 'rgba(255,255,255,0.4)', fontSize: '12px' }}>{a.sender_email}</td>
<td style={{ padding: '10px 12px', borderBottom: '1px solid rgba(255,255,255,0.04)', color: 'rgba(255,255,255,0.4)', fontSize: '12px' }}>{a.sent_at ? new Date(a.sent_at).toLocaleDateString() : '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Reply inbox */}
{replies.length > 0 && (
<div style={{ marginTop: '24px' }}>
<SectionHeader title="Reply Inbox" subtitle={`${replies.length} replies received. Each reply is auto-classified by sentiment for quick triage.`} />
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{replies.map((r) => (
<div key={r.reply_id} style={{ padding: '14px', borderRadius: '10px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
<span style={{ fontWeight: 600, color: '#fff', fontSize: '13px' }}>{r.subject}</span>
<TooltipWrap text={`Auto-classified as "${r.classification}"`}>
<span style={{
padding: '3px 10px', borderRadius: '12px', fontSize: '11px', fontWeight: 600,
background: r.classification === 'positive' ? 'rgba(67,233,123,0.15)' : r.classification === 'negative' ? 'rgba(245,87,108,0.15)' : 'rgba(254,225,64,0.1)',
color: r.classification === 'positive' ? '#43e97b' : r.classification === 'negative' ? '#f5576c' : '#fee140',
border: `1px solid ${r.classification === 'positive' ? '#43e97b33' : r.classification === 'negative' ? '#f5576c33' : '#fee14033'}`,
}}>
{r.classification}
</span>
</TooltipWrap>
</div>
<div style={{ fontSize: '12px', color: 'rgba(255,255,255,0.3)', marginBottom: '6px' }}>From: {r.from_email} &middot; {r.received_at ? new Date(r.received_at).toLocaleString() : ''}</div>
<div style={{ fontSize: '13px', color: 'rgba(255,255,255,0.6)', whiteSpace: 'pre-wrap', maxHeight: '80px', overflow: 'hidden' }}>{r.body.slice(0, 300)}</div>
</div>
))}
</div>
</div>
)}
{/* Follow-up schedule */}
{followups.length > 0 && (
<div style={{ marginTop: '24px' }}>
<SectionHeader title="Follow-up Schedule" subtitle="Automated follow-up emails scheduled to re-engage leads who haven't replied." />
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{followups.map((f) => (
<div key={f.schedule_id} style={{
padding: '12px 16px', borderRadius: '8px',
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)',
fontSize: '13px', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
}}>
<span style={{ color: '#fff' }}>{f.subject}</span>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
{f.scheduled_for && <span style={{ color: 'rgba(255,255,255,0.4)', fontSize: '12px' }}>{new Date(f.scheduled_for).toLocaleDateString()}</span>}
<TooltipWrap text={f.sent ? 'This follow-up has been sent' : 'Awaiting scheduled send date'}>
<span style={{
padding: '3px 10px', borderRadius: '12px', fontSize: '11px', fontWeight: 600,
background: f.sent ? 'rgba(67,233,123,0.15)' : 'rgba(254,225,64,0.1)',
color: f.sent ? '#43e97b' : '#fee140',
border: `1px solid ${f.sent ? '#43e97b33' : '#fee14033'}`,
}}>
{f.sent ? 'Sent' : 'Pending'}
</span>
</TooltipWrap>
</div>
</div>
))}
</div>
</div>
)}
</div>
) : (
<p style={{ color: 'rgba(255,255,255,0.3)', textAlign: 'center', padding: '40px 0' }}>
Select a campaign from the <strong>Campaigns</strong> tab to view its leads.
</p>
)}
</div>
)}
{/* === COMPOSER TAB === */}
{activeTab === 'composer' && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
<div style={{ ...cardSx, padding: '24px' }}>
<SectionHeader title="AI Email Composer" subtitle="Generate personalized outreach emails with AI. Choose a tone, pick a template, and let AI craft your message." />
<div style={{ marginBottom: '14px' }}>
<label style={{ fontSize: '12px', fontWeight: 600, color: 'rgba(255,255,255,0.5)', textTransform: 'uppercase', letterSpacing: '0.5px', display: 'block', marginBottom: '6px' }}>Template</label>
<TooltipWrap text="Optional: start from an existing saved template to maintain consistent branding">
<select value={selectedTemplateId} onChange={(e) => setSelectedTemplateId(e.target.value)} style={selectSx}>
<option value="">-- No template (start fresh) --</option>
{templates.map((t) => <option key={t.template_id} value={t.template_id}>{t.name}</option>)}
</select>
</TooltipWrap>
</div>
<div style={{ marginBottom: '14px' }}>
<label style={{ fontSize: '12px', fontWeight: 600, color: 'rgba(255,255,255,0.5)', textTransform: 'uppercase', letterSpacing: '0.5px', display: 'block', marginBottom: '6px' }}>Topic / Keyword</label>
<input type="text" value={topic} onChange={(e) => setTopic(e.target.value)}
placeholder="e.g. AI marketing trends, SaaS growth strategies"
style={inputSx} />
</div>
<div style={{ marginBottom: '14px' }}>
<label style={{ fontSize: '12px', fontWeight: 600, color: 'rgba(255,255,255,0.5)', textTransform: 'uppercase', letterSpacing: '0.5px', display: 'block', marginBottom: '6px' }}>Target Site (optional)</label>
<TooltipWrap text="Mentioning the target site helps AI tailor the pitch to that specific publication">
<input type="text" value={targetSite} onChange={(e) => setTargetSite(e.target.value)}
placeholder="e.g. example.com"
style={inputSx} />
</TooltipWrap>
</div>
<div style={{ marginBottom: '14px' }}>
<label style={{ fontSize: '12px', fontWeight: 600, color: 'rgba(255,255,255,0.5)', textTransform: 'uppercase', letterSpacing: '0.5px', display: 'block', marginBottom: '6px' }}>Tone</label>
<TooltipWrap text="Controls the writing style — Professional (formal), Friendly (conversational), Casual (relaxed), or Formal (highly polished)">
<select value={tone} onChange={(e) => setTone(e.target.value as any)} style={selectSx}>
<option value="professional">Professional Formal & polished</option>
<option value="friendly">Friendly Warm & conversational</option>
<option value="casual">Casual Relaxed & informal</option>
<option value="formal">Formal Highly structured & official</option>
</select>
</TooltipWrap>
</div>
<TooltipWrap text="Generates a complete outreach email with subject + body using AI">
<button onClick={handleGenerate} disabled={!topic.trim() || isGenerating}
style={{ ...btnBase, width: '100%', padding: '14px', background: GRADIENT_PRIMARY, color: '#fff', marginBottom: '20px', opacity: !topic.trim() || isGenerating ? 0.5 : 1 }}>
{isGenerating ? 'Generating with AI...' : 'Generate with AI'}
</button>
</TooltipWrap>
<div style={{ marginBottom: '14px' }}>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '6px' }}>
<label style={{ fontSize: '12px', fontWeight: 600, color: 'rgba(255,255,255,0.5)', textTransform: 'uppercase', letterSpacing: '0.5px', flex: 1 }}>Subject Line</label>
<TooltipWrap text="AI suggests multiple subject line variants based on your email body">
<button onClick={handleSuggestSubjects} disabled={!body.trim() || isGenerating}
style={{ ...btnBase, padding: '6px 14px', fontSize: '12px', background: 'rgba(102,126,234,0.2)', color: '#8b9cf7', border: '1px solid rgba(102,126,234,0.3)' }}>
Suggest
</button>
</TooltipWrap>
</div>
<input type="text" value={subject} onChange={(e) => setSubject(e.target.value)} placeholder="Email subject line" style={inputSx} />
{subjectSuggestions.length > 0 && (
<div style={{ marginTop: '8px', padding: '10px', background: 'rgba(255,255,255,0.04)', borderRadius: '8px' }}>
<div style={{ fontSize: '11px', color: 'rgba(255,255,255,0.4)', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Click a suggestion to apply</div>
{subjectSuggestions.map((s, i) => (
<div key={i} onClick={() => applySuggestion(s)}
style={{ padding: '6px 10px', cursor: 'pointer', borderRadius: '6px', fontSize: '13px', color: '#8b9cf7', transition: 'background 0.2s' }}>
{s}
</div>
))}
</div>
)}
</div>
<div style={{ marginBottom: '14px' }}>
<label style={{ fontSize: '12px', fontWeight: 600, color: 'rgba(255,255,255,0.5)', textTransform: 'uppercase', letterSpacing: '0.5px', display: 'block', marginBottom: '6px' }}>Email Body</label>
<textarea value={body} onChange={(e) => setBody(e.target.value)} rows={10}
placeholder="Your email content — or let AI generate it above"
style={{ ...inputSx, fontFamily: 'monospace', fontSize: '13px', resize: 'vertical', lineHeight: 1.6 }} />
</div>
{/* Personalize */}
<div style={{ marginTop: '24px', padding: '16px', borderRadius: '10px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }}>
<h4 style={{ margin: '0 0 4px', color: '#fff', fontSize: '14px' }}>Personalize for Lead</h4>
<p style={{ margin: '0 0 12px', color: 'rgba(255,255,255,0.4)', fontSize: '12px' }}>Tailor the email to a specific lead by filling in their details.</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '8px' }}>
<input type="text" value={leadName} onChange={(e) => setLeadName(e.target.value)} placeholder="Lead name" style={inputSx} />
<input type="text" value={leadSite} onChange={(e) => setLeadSite(e.target.value)} placeholder="Their site" style={inputSx} />
</div>
<input type="text" value={leadContentTopic} onChange={(e) => setLeadContentTopic(e.target.value)}
placeholder="Topic of their relevant content"
style={{ ...inputSx, marginBottom: '10px' }} />
<TooltipWrap text="Rewrites your email to reference the specific lead's name, site, and content">
<button onClick={handlePersonalize} disabled={!leadName.trim() || !leadSite.trim() || !leadContentTopic.trim() || isGenerating}
style={{ ...btnBase, width: '100%', padding: '10px', background: GRADIENT_WARNING, color: '#1a1a2e', opacity: leadName.trim() && leadSite.trim() && leadContentTopic.trim() && !isGenerating ? 1 : 0.5 }}>
Personalize Email
</button>
</TooltipWrap>
</div>
{/* Follow-up */}
<div style={{ marginTop: '16px', padding: '16px', borderRadius: '10px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }}>
<h4 style={{ margin: '0 0 4px', color: '#fff', fontSize: '14px' }}>Draft Follow-up</h4>
<p style={{ margin: '0 0 12px', color: 'rgba(255,255,255,0.4)', fontSize: '12px' }}>Generate a polite follow-up email to re-engage a lead who hasn't responded.</p>
<div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}>
<input type="number" value={followUpDays} onChange={(e) => setFollowUpDays(Number(e.target.value))} min={1} max={90}
style={{ ...inputSx, width: '80px' }} />
<span style={{ padding: '10px 0', color: 'rgba(255,255,255,0.4)', fontSize: '13px' }}>days since original email</span>
</div>
<input type="text" value={replyContext} onChange={(e) => setReplyContext(e.target.value)}
placeholder="Their reply (if any) — leave blank for no-response follow-up"
style={{ ...inputSx, marginBottom: '10px' }} />
<TooltipWrap text="Creates a follow-up email that references the original and any reply context">
<button onClick={handleFollowUp} disabled={!subject.trim() || !body.trim() || isGenerating}
style={{ ...btnBase, width: '100%', padding: '10px', background: GRADIENT_SECONDARY, color: '#fff', opacity: subject.trim() && body.trim() && !isGenerating ? 1 : 0.5 }}>
Generate Follow-up
</button>
</TooltipWrap>
</div>
{/* Save template */}
<div style={{ marginTop: '16px', display: 'flex', gap: '8px', padding: '16px', borderRadius: '10px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }}>
<input type="text" value={templateName} onChange={(e) => setTemplateName(e.target.value)}
placeholder="Template name (e.g. 'Cold outreach v1')"
style={{ ...inputSx, flex: 1 }} />
<TooltipWrap text="Saves the current subject + body as a reusable template">
<button onClick={handleSaveTemplate} disabled={!templateName.trim() || !subject.trim() || !body.trim()}
style={{ ...btnBase, background: GRADIENT_SUCCESS, color: '#1a1a2e', opacity: templateName.trim() && subject.trim() && body.trim() ? 1 : 0.5 }}>
Save as Template
</button>
</TooltipWrap>
</div>
{selectedCampaign && subject.trim() && body.trim() && (
<div style={{ marginTop: '16px', padding: '14px', borderRadius: '10px', background: 'rgba(67,233,123,0.1)', border: '1px solid rgba(67,233,123,0.2)' }}>
<p style={{ margin: '0 0 8px', fontSize: '13px', color: '#43e97b' }}>
Ready to send this email to leads in <strong>{selectedCampaign.name}</strong>?
</p>
<TooltipWrap text="Go to the Leads tab to select recipients and send">
<button onClick={() => setActiveTab('leads')}
style={{ ...btnBase, padding: '8px 20px', background: GRADIENT_SUCCESS, color: '#1a1a2e', fontSize: '13px' }}>
Go to Campaign Leads
</button>
</TooltipWrap>
</div>
)}
</div>
{/* Preview pane */}
<div style={{ ...cardSx, padding: '24px' }}>
<SectionHeader title="Preview" subtitle="See how your email will look when received." />
<div style={{ padding: '24px', minHeight: '400px', borderRadius: '8px', background: '#fff' }}>
{subject || body ? (
<div>
<div style={{ fontWeight: 700, fontSize: '18px', marginBottom: '16px', padding: '0 0 12px', borderBottom: '2px solid #eee', color: '#333' }}>
{subject || '(no subject)'}
</div>
<div style={{ whiteSpace: 'pre-wrap', fontSize: '14px', lineHeight: 1.7, color: '#555' }}>
{body || '(no body)'}
</div>
</div>
) : (
<div style={{ textAlign: 'center', padding: '60px 0', color: '#999' }}>
<p>Generate an email to see a live preview here.</p>
<p style={{ fontSize: '12px', color: '#bbb' }}>Use the AI tools on the left to create your message.</p>
</div>
)}
</div>
</div>
</div>
)}
{/* === ANALYTICS TAB === */}
{activeTab === 'analytics' && (
<div style={{ ...cardSx, padding: '24px' }}>
{selectedCampaign ? (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexWrap: 'wrap', gap: '8px' }}>
<div>
<h3 style={{ margin: 0, color: '#fff' }}>{selectedCampaign.name}</h3>
<p style={{ margin: '4px 0 0', fontSize: '13px', color: 'rgba(255,255,255,0.4)' }}>Performance analytics & reporting</p>
</div>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<TooltipWrap text="Choose the time range for send volume data">
<select value={analyticsDays} onChange={(e) => setAnalyticsDays(Number(e.target.value))}
style={{ ...selectSx, padding: '8px 12px', fontSize: '13px' }}>
<option value={7}>Last 7 days</option>
<option value={30}>Last 30 days</option>
<option value={90}>Last 90 days</option>
</select>
</TooltipWrap>
<TooltipWrap text="Switch to a different campaign">
<select onChange={(e) => { const c = campaigns.find(x => x.campaign_id === e.target.value); if (c) selectCampaign(c.campaign_id); }}
value={selectedCampaign.campaign_id}
style={{ ...selectSx, padding: '8px 12px', fontSize: '13px', minWidth: '180px' }}>
{campaigns.map((c) => <option key={c.campaign_id} value={c.campaign_id}>{c.name}</option>)}
</select>
</TooltipWrap>
</div>
</div>
{analytics && (
<div style={{ display: 'flex', gap: '12px', marginBottom: '24px', flexWrap: 'wrap' }}>
{[{ label: 'Sent', value: analytics.send_volume, grad: GRADIENT_PRIMARY, desc: 'Total outreach emails sent' },
{ label: 'Response Rate', value: `${(analytics.response_rate * 100).toFixed(1)}%`, grad: GRADIENT_SUCCESS, desc: 'Percentage of sent emails that received a reply' },
{ label: 'Replies', value: analytics.reply_count, grad: GRADIENT_WARNING, desc: 'Total replies received from leads' },
{ label: 'Placement Rate', value: `${(analytics.placement_rate * 100).toFixed(1)}%`, grad: 'linear-gradient(135deg, #a18cd1, #fbc2eb)', desc: 'Percentage of leads that resulted in a published post' },
{ label: 'Blocked', value: analytics.blocked_count, grad: GRADIENT_SECONDARY, desc: 'Emails blocked by policy (suppression, caps, etc.)' },
].map(({ label, value, grad, desc }) => (
<TooltipWrap key={label} text={desc}>
<div style={{
flex: 1, minWidth: '110px', padding: '16px', borderRadius: '10px', textAlign: 'center',
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)',
}}>
<div style={{ fontSize: '24px', fontWeight: 700, background: grad, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>{value}</div>
<div style={{ fontSize: '11px', color: 'rgba(255,255,255,0.4)', marginTop: '2px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>{label}</div>
</div>
</TooltipWrap>
))}
</div>
)}
{isAnalyticsLoading && <p style={{ color: 'rgba(255,255,255,0.4)', textAlign: 'center', padding: '20px' }}>Loading analytics data...</p>}
{/* Volume chart */}
{volumeData.length > 0 && (
<div style={{ marginBottom: '24px' }}>
<SectionHeader title="Send Volume Trend" subtitle={`Daily outreach email send volume over the last ${analyticsDays} days.`} />
<div style={{ padding: '16px', borderRadius: '10px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)' }}>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={volumeData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" />
<XAxis dataKey="date" tick={{ fontSize: 12, fill: 'rgba(255,255,255,0.4)' }} />
<YAxis allowDecimals={false} tick={{ fontSize: 12, fill: 'rgba(255,255,255,0.4)' }} />
<RechartsTooltip contentStyle={{ background: '#1a1a2e', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }} />
<Line type="monotone" dataKey="count" stroke="#667eea" strokeWidth={2} dot={{ r: 3, fill: '#667eea' }} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
)}
{/* Funnel chart */}
{funnelData.length > 0 && (
<div style={{ marginBottom: '24px' }}>
<SectionHeader title="Lead Conversion Funnel" subtitle="Breakdown of leads by status stage. Shows where leads are in the outreach pipeline." />
<div style={{ padding: '16px', borderRadius: '10px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)' }}>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={funnelData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" />
<XAxis dataKey="status" tick={{ fontSize: 12, fill: 'rgba(255,255,255,0.4)' }} />
<YAxis allowDecimals={false} tick={{ fontSize: 12, fill: 'rgba(255,255,255,0.4)' }} />
<RechartsTooltip contentStyle={{ background: '#1a1a2e', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }} />
<Bar dataKey="count" fill="#667eea" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
{/* Reply classification */}
{analytics && Object.keys(analytics.reply_classification).length > 0 && (
<div style={{ marginBottom: '24px', padding: '16px', borderRadius: '10px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)' }}>
<SectionHeader title="Reply Classification Breakdown" subtitle="Categorization of replies by type — positive, negative, neutral, or out-of-office." />
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
{Object.entries(analytics.reply_classification).map(([cls, count]) => (
<TooltipWrap key={cls} text={`${count} replies classified as "${cls}"`}>
<div style={{ padding: '14px 20px', borderRadius: '10px', background: 'rgba(255,255,255,0.04)', minWidth: '100px', textAlign: 'center', border: '1px solid rgba(255,255,255,0.08)' }}>
<div style={{ fontSize: '22px', fontWeight: 700, background: GRADIENT_PRIMARY, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>{count}</div>
<div style={{ fontSize: '12px', color: 'rgba(255,255,255,0.4)', textTransform: 'capitalize', marginTop: '2px' }}>{cls}</div>
</div>
</TooltipWrap>
))}
</div>
</div>
)}
{/* Export */}
<div style={{ padding: '20px', borderRadius: '10px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }}>
<SectionHeader title="Export Data" subtitle="Download campaign data as CSV for offline analysis or reporting." />
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
<TooltipWrap text="Downloads all leads in this campaign as a CSV file">
<button onClick={() => handleExportCsv('leads')} disabled={isExporting === 'leads'}
style={{ ...btnBase, background: GRADIENT_SUCCESS, color: '#1a1a2e', fontSize: '13px', opacity: isExporting === 'leads' ? 0.5 : 1 }}>
{isExporting === 'leads' ? 'Exporting...' : 'Export Leads CSV'}
</button>
</TooltipWrap>
<TooltipWrap text="Downloads all outreach attempts with statuses as CSV">
<button onClick={() => handleExportCsv('attempts')} disabled={isExporting === 'attempts'}
style={{ ...btnBase, background: GRADIENT_PRIMARY, color: '#fff', fontSize: '13px', opacity: isExporting === 'attempts' ? 0.5 : 1 }}>
{isExporting === 'attempts' ? 'Exporting...' : 'Export Attempts CSV'}
</button>
</TooltipWrap>
<TooltipWrap text="Downloads all received replies with classifications as CSV">
<button onClick={() => handleExportCsv('replies')} disabled={isExporting === 'replies'}
style={{ ...btnBase, background: GRADIENT_WARNING, color: '#1a1a2e', fontSize: '13px', opacity: isExporting === 'replies' ? 0.5 : 1 }}>
{isExporting === 'replies' ? 'Exporting...' : 'Export Replies CSV'}
</button>
</TooltipWrap>
</div>
</div>
</div>
) : (
<p style={{ color: 'rgba(255,255,255,0.3)', textAlign: 'center', padding: '40px 0' }}>
Select a campaign from the <strong>Campaigns</strong> tab to view analytics.
</p>
)}
</div>
)}
</div>
</div>
);
};
export default BacklinkOutreachDashboard;