Upload src/pages/Dashboard.tsx with huggingface_hub
Browse files- src/pages/Dashboard.tsx +551 -0
src/pages/Dashboard.tsx
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
| 2 |
+
import { apiClient } from "../lib/apiClient";
|
| 3 |
+
import {
|
| 4 |
+
Megaphone,
|
| 5 |
+
Users,
|
| 6 |
+
Mail,
|
| 7 |
+
Inbox,
|
| 8 |
+
Flame,
|
| 9 |
+
TrendingUp,
|
| 10 |
+
Sparkles,
|
| 11 |
+
Activity,
|
| 12 |
+
Plus,
|
| 13 |
+
ArrowUpRight,
|
| 14 |
+
CheckCircle2,
|
| 15 |
+
ShieldCheck,
|
| 16 |
+
Radio,
|
| 17 |
+
Zap
|
| 18 |
+
} from "lucide-react";
|
| 19 |
+
import {
|
| 20 |
+
LineChart,
|
| 21 |
+
Line,
|
| 22 |
+
XAxis,
|
| 23 |
+
YAxis,
|
| 24 |
+
CartesianGrid,
|
| 25 |
+
Tooltip,
|
| 26 |
+
ResponsiveContainer,
|
| 27 |
+
PieChart,
|
| 28 |
+
Pie,
|
| 29 |
+
Cell
|
| 30 |
+
} from "recharts";
|
| 31 |
+
import { useState } from "react";
|
| 32 |
+
import { Link } from "react-router-dom";
|
| 33 |
+
import StatCard from "../components/StatCard";
|
| 34 |
+
import StatusBadge from "../components/StatusBadge";
|
| 35 |
+
import FormDrawer from "../components/FormDrawer";
|
| 36 |
+
import toast from "react-hot-toast";
|
| 37 |
+
|
| 38 |
+
const lineData = [
|
| 39 |
+
{ date: "Day 1", sent: 480, open: 310, reply: 28, bounce: 6 },
|
| 40 |
+
{ date: "Day 4", sent: 820, open: 530, reply: 46, bounce: 10 },
|
| 41 |
+
{ date: "Day 7", sent: 1250, open: 820, reply: 74, bounce: 14 },
|
| 42 |
+
{ date: "Day 10", sent: 1780, open: 1190, reply: 105, bounce: 19 },
|
| 43 |
+
{ date: "Day 14", sent: 2420, open: 1650, reply: 148, bounce: 23 },
|
| 44 |
+
{ date: "Day 18", sent: 3150, open: 2190, reply: 196, bounce: 31 },
|
| 45 |
+
{ date: "Day 22", sent: 4020, open: 2840, reply: 254, bounce: 38 },
|
| 46 |
+
{ date: "Day 26", sent: 4980, open: 3510, reply: 312, bounce: 46 },
|
| 47 |
+
{ date: "Day 30", sent: 5890, open: 4120, reply: 378, bounce: 52 },
|
| 48 |
+
];
|
| 49 |
+
|
| 50 |
+
const pieData = [
|
| 51 |
+
{ name: "Interested", value: 340, color: "var(--nt-success)" },
|
| 52 |
+
{ name: "Meeting Booked", value: 160, color: "var(--nt-accent)" },
|
| 53 |
+
{ name: "Replied", value: 290, color: "var(--nt-info)" },
|
| 54 |
+
{ name: "Not Interested", value: 180, color: "var(--nt-danger)" },
|
| 55 |
+
{ name: "Bounced", value: 45, color: "var(--nt-espresso-700)" },
|
| 56 |
+
];
|
| 57 |
+
|
| 58 |
+
export default function Dashboard() {
|
| 59 |
+
const queryClient = useQueryClient();
|
| 60 |
+
const [isCreateCampaignOpen, setIsCreateCampaignOpen] = useState(false);
|
| 61 |
+
const [activeMetric, setActiveMetric] = useState<"all" | "open" | "reply">("all");
|
| 62 |
+
|
| 63 |
+
const { data: analytics, isLoading: isAnalyticsLoading, isError: isAnalyticsError } = useQuery({
|
| 64 |
+
queryKey: ["analytics-summary"],
|
| 65 |
+
queryFn: async () => {
|
| 66 |
+
const res = await apiClient.get("/analytics/campaigns");
|
| 67 |
+
return res.data;
|
| 68 |
+
},
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
const { data: campaigns, isLoading: isCampaignsLoading, isError: isCampaignsError } = useQuery({
|
| 72 |
+
queryKey: ["campaigns"],
|
| 73 |
+
queryFn: async () => {
|
| 74 |
+
const res = await apiClient.get("/campaigns");
|
| 75 |
+
return res.data;
|
| 76 |
+
},
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
const { data: emailAccounts = [], isLoading: isEmailAccountsLoading } = useQuery({
|
| 80 |
+
queryKey: ["email-accounts"],
|
| 81 |
+
queryFn: async () => {
|
| 82 |
+
const res = await apiClient.get("/email-accounts");
|
| 83 |
+
return res.data;
|
| 84 |
+
},
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
const warmupDailyCount = emailAccounts.reduce((total: number, account: any) => total + (account.warmup_daily_count || 0), 0);
|
| 88 |
+
const connectedMailboxCount = emailAccounts.filter((account: any) => account.status === "CONNECTED").length;
|
| 89 |
+
const warmupScoreValues = emailAccounts.map((account: any) => account.warmup_score).filter((score: any) => typeof score === "number");
|
| 90 |
+
const averageWarmupScore = warmupScoreValues.length
|
| 91 |
+
? (warmupScoreValues.reduce((total: number, score: number) => total + score, 0) / warmupScoreValues.length).toFixed(1)
|
| 92 |
+
: null;
|
| 93 |
+
|
| 94 |
+
return (
|
| 95 |
+
<div className="p-6 max-w-7xl mx-auto space-y-7">
|
| 96 |
+
{/* Header with Live Status & Quick Action Buttons */}
|
| 97 |
+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
| 98 |
+
<div>
|
| 99 |
+
<div className="flex items-center gap-2">
|
| 100 |
+
<h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5">
|
| 101 |
+
Command Center
|
| 102 |
+
</h1>
|
| 103 |
+
<span className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2.5 py-0.5 rounded-full">
|
| 104 |
+
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-ping" />
|
| 105 |
+
Live Engine
|
| 106 |
+
</span>
|
| 107 |
+
</div>
|
| 108 |
+
<p className="text-zinc-400 text-sm mt-1">
|
| 109 |
+
Real-time deliverability radar, campaign sequencing, and cold outreach throughput.
|
| 110 |
+
</p>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div className="flex items-center gap-3">
|
| 114 |
+
<button
|
| 115 |
+
onClick={() => setIsCreateCampaignOpen(true)}
|
| 116 |
+
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-all shadow-md shadow-indigo-600/20 flex items-center gap-2"
|
| 117 |
+
>
|
| 118 |
+
<Plus className="w-4 h-4" />
|
| 119 |
+
Create Campaign
|
| 120 |
+
</button>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
{/* High-Impact Stat Cards */}
|
| 125 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
| 126 |
+
<StatCard
|
| 127 |
+
title="Total Campaigns"
|
| 128 |
+
value={isCampaignsLoading ? "…" : isCampaignsError ? "—" : campaigns?.length ?? 0}
|
| 129 |
+
subtitle="Outbound Sequences"
|
| 130 |
+
icon={Megaphone}
|
| 131 |
+
iconColor="indigo"
|
| 132 |
+
change={{ value: "+2 new", isPositive: true, period: "this week" }}
|
| 133 |
+
/>
|
| 134 |
+
<StatCard
|
| 135 |
+
title="Total Leads"
|
| 136 |
+
value={isAnalyticsLoading ? "…" : isAnalyticsError ? "—" : (analytics?.total_leads ?? 0).toLocaleString()}
|
| 137 |
+
subtitle="Prospects Targeted"
|
| 138 |
+
icon={Users}
|
| 139 |
+
iconColor="teal"
|
| 140 |
+
change={{ value: "+380", isPositive: true, period: "last 7 days" }}
|
| 141 |
+
/>
|
| 142 |
+
<StatCard
|
| 143 |
+
title="Emails Sent"
|
| 144 |
+
value={isAnalyticsLoading ? "…" : isAnalyticsError ? "—" : (analytics?.total_sent ?? 0).toLocaleString()}
|
| 145 |
+
subtitle="Across All Mailboxes"
|
| 146 |
+
icon={Mail}
|
| 147 |
+
iconColor="blue"
|
| 148 |
+
change={{ value: "+14.8%", isPositive: true, period: "month-over-month" }}
|
| 149 |
+
/>
|
| 150 |
+
<StatCard
|
| 151 |
+
title="Avg Reply Rate"
|
| 152 |
+
value={isAnalyticsLoading ? "…" : isAnalyticsError ? "—" : analytics?.reply_rate != null ? `${analytics.reply_rate}%` : "—"}
|
| 153 |
+
subtitle="Deliverability High"
|
| 154 |
+
icon={Inbox}
|
| 155 |
+
iconColor="emerald"
|
| 156 |
+
change={{ value: "+1.2%", isPositive: true, period: "vs benchmark" }}
|
| 157 |
+
/>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
{/* Deliverability & Mailbox Health Ribbon */}
|
| 161 |
+
<div className="command-signal-ribbon bg-gradient-to-r from-white/5 via-indigo-900/20 to-white/5 border border-white/10 backdrop-blur-md rounded-xl p-4 flex flex-col md:flex-row items-center justify-between gap-4 shadow-[0_8px_32px_-8px_rgba(6,10,12,0.65)]">
|
| 162 |
+
<div className="flex items-center gap-3">
|
| 163 |
+
<div className="p-2.5 bg-emerald-500/20 border border-emerald-500/30 rounded-xl text-emerald-300 shadow-[inset_0_1px_1px_rgba(246,248,242,0.1)]">
|
| 164 |
+
<ShieldCheck className="w-5 h-5 drop-shadow-[0_0_8px_rgba(63,156,132,0.5)]" />
|
| 165 |
+
</div>
|
| 166 |
+
<div>
|
| 167 |
+
<h4 className="text-sm font-semibold text-zinc-100 flex items-center gap-2">
|
| 168 |
+
Deliverability & Reputation Guard
|
| 169 |
+
<span className="text-[10px] font-bold uppercase tracking-wider bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 px-2 py-0.5 rounded-full drop-shadow-[0_0_8px_rgba(63,156,132,0.3)]">
|
| 170 |
+
{averageWarmupScore ? `${averageWarmupScore}% Score` : isEmailAccountsLoading ? "Checking score" : "No score"}
|
| 171 |
+
</span>
|
| 172 |
+
</h4>
|
| 173 |
+
<p className="text-xs text-zinc-400 mt-0.5">
|
| 174 |
+
{connectedMailboxCount} connected mailboxes with {warmupDailyCount} daily warmup messages.
|
| 175 |
+
</p>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
<div className="flex items-center gap-4 text-xs">
|
| 180 |
+
<div className="flex items-center gap-2 bg-black/40 border border-white/5 px-3 py-1.5 rounded-lg shadow-[inset_0_1px_1px_rgba(246,248,242,0.02)]">
|
| 181 |
+
<Flame className="w-4 h-4 text-amber-400 drop-shadow-[0_0_8px_rgba(194,160,74,0.5)]" />
|
| 182 |
+
<span className="text-zinc-300 font-medium font-sans">Warmup:</span>
|
| 183 |
+
<span className="text-amber-400 font-bold font-mono drop-shadow-[0_0_8px_rgba(194,160,74,0.3)]">{warmupDailyCount} daily / {connectedMailboxCount} inboxes</span>
|
| 184 |
+
</div>
|
| 185 |
+
<Link
|
| 186 |
+
to="/warmup"
|
| 187 |
+
className="text-indigo-400 hover:text-indigo-300 font-medium flex items-center gap-1 transition-colors"
|
| 188 |
+
>
|
| 189 |
+
Warmup Center <ArrowUpRight className="w-3.5 h-3.5" />
|
| 190 |
+
</Link>
|
| 191 |
+
</div>
|
| 192 |
+
</div>
|
| 193 |
+
|
| 194 |
+
{/* Main Visualizations Grid */}
|
| 195 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
| 196 |
+
{/* 30-Day Performance Multi-Line Chart */}
|
| 197 |
+
<div className="lg:col-span-2 bg-white/5 border border-white/10 backdrop-blur-md rounded-xl p-5 shadow-[0_8px_32px_-8px_rgba(6,10,12,0.65)] flex flex-col">
|
| 198 |
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-6">
|
| 199 |
+
<div>
|
| 200 |
+
<h3 className="text-base font-semibold text-zinc-100 flex items-center gap-2">
|
| 201 |
+
<Activity className="w-4 h-4 text-indigo-400 drop-shadow-[0_0_8px_rgba(37,125,104,0.5)]" />
|
| 202 |
+
Performance Trajectory (30 Days)
|
| 203 |
+
</h3>
|
| 204 |
+
<p className="text-xs text-zinc-400 mt-0.5">
|
| 205 |
+
Sent volume, open rate, and reply convergence.
|
| 206 |
+
</p>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
{/* Metric toggles */}
|
| 210 |
+
<div className="flex items-center bg-black/40 p-1 rounded-lg border border-white/5 text-xs shadow-[inset_0_1px_1px_rgba(246,248,242,0.02)]">
|
| 211 |
+
<button
|
| 212 |
+
onClick={() => setActiveMetric("all")}
|
| 213 |
+
className={`px-3 py-1 rounded-md font-medium transition-all duration-300 ${
|
| 214 |
+
activeMetric === "all" ? "bg-indigo-600 text-white shadow-[0_0_12px_rgba(37,125,104,0.5)]" : "text-zinc-400 hover:text-zinc-200"
|
| 215 |
+
}`}
|
| 216 |
+
>
|
| 217 |
+
All Metrics
|
| 218 |
+
</button>
|
| 219 |
+
<button
|
| 220 |
+
onClick={() => setActiveMetric("open")}
|
| 221 |
+
className={`px-3 py-1 rounded-md font-medium transition-all duration-300 ${
|
| 222 |
+
activeMetric === "open" ? "bg-emerald-600 text-white shadow-[0_0_12px_rgba(37,125,104,0.5)]" : "text-zinc-400 hover:text-zinc-200"
|
| 223 |
+
}`}
|
| 224 |
+
>
|
| 225 |
+
Opens
|
| 226 |
+
</button>
|
| 227 |
+
<button
|
| 228 |
+
onClick={() => setActiveMetric("reply")}
|
| 229 |
+
className={`px-3 py-1 rounded-md font-medium transition-all duration-300 ${
|
| 230 |
+
activeMetric === "reply" ? "bg-purple-600 text-white shadow-[0_0_12px_rgba(178,87,49,0.5)]" : "text-zinc-400 hover:text-zinc-200"
|
| 231 |
+
}`}
|
| 232 |
+
>
|
| 233 |
+
Replies
|
| 234 |
+
</button>
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
|
| 238 |
+
<div className="h-[300px] w-full">
|
| 239 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 240 |
+
<LineChart data={lineData} margin={{ top: 5, right: 10, bottom: 5, left: -10 }}>
|
| 241 |
+
<CartesianGrid strokeDasharray="3 3" stroke="rgba(246,248,242,0.05)" vertical={false} />
|
| 242 |
+
<XAxis dataKey="date" stroke="var(--nt-text-muted)" fontSize={12} tickLine={false} axisLine={false} />
|
| 243 |
+
<YAxis stroke="var(--nt-text-muted)" fontSize={12} tickLine={false} axisLine={false} className="font-mono" />
|
| 244 |
+
<Tooltip
|
| 245 |
+
contentStyle={{
|
| 246 |
+
backgroundColor: "rgba(6, 10, 12, 0.88)",
|
| 247 |
+
backdropFilter: "blur(12px)",
|
| 248 |
+
borderColor: "rgba(37,125,104,0.2)",
|
| 249 |
+
borderRadius: "12px",
|
| 250 |
+
color: "var(--nt-text-primary)",
|
| 251 |
+
fontSize: "12px",
|
| 252 |
+
}}
|
| 253 |
+
/>
|
| 254 |
+
{(activeMetric === "all" || activeMetric === "open") && (
|
| 255 |
+
<Line
|
| 256 |
+
type="monotone"
|
| 257 |
+
dataKey="sent"
|
| 258 |
+
name="Sent"
|
| 259 |
+
stroke="var(--nt-deep-teal-600)"
|
| 260 |
+
strokeWidth={3}
|
| 261 |
+
dot={{ r: 4, fill: "var(--nt-deep-teal-600)", strokeWidth: 2, stroke: "var(--nt-focus-ring-offset)" }}
|
| 262 |
+
activeDot={{ r: 6, fill: "var(--nt-deep-teal-700)" }}
|
| 263 |
+
animationDuration={1500}
|
| 264 |
+
/>
|
| 265 |
+
)}
|
| 266 |
+
{(activeMetric === "all" || activeMetric === "open") && (
|
| 267 |
+
<Line
|
| 268 |
+
type="monotone"
|
| 269 |
+
dataKey="open"
|
| 270 |
+
name="Opens"
|
| 271 |
+
stroke="var(--nt-deep-teal-500)"
|
| 272 |
+
strokeWidth={3}
|
| 273 |
+
dot={{ r: 4, fill: "var(--nt-deep-teal-500)", strokeWidth: 2, stroke: "var(--nt-focus-ring-offset)" }}
|
| 274 |
+
activeDot={{ r: 6, fill: "var(--nt-deep-teal-700)" }}
|
| 275 |
+
animationDuration={1500}
|
| 276 |
+
/>
|
| 277 |
+
)}
|
| 278 |
+
{(activeMetric === "all" || activeMetric === "reply") && (
|
| 279 |
+
<Line
|
| 280 |
+
type="monotone"
|
| 281 |
+
dataKey="reply"
|
| 282 |
+
name="Replies"
|
| 283 |
+
stroke="var(--nt-terracotta-500)"
|
| 284 |
+
strokeWidth={3}
|
| 285 |
+
dot={{ r: 4, fill: "var(--nt-terracotta-500)", strokeWidth: 2, stroke: "var(--nt-focus-ring-offset)" }}
|
| 286 |
+
activeDot={{ r: 6, fill: "var(--nt-terracotta-700)" }}
|
| 287 |
+
animationDuration={1500}
|
| 288 |
+
/>
|
| 289 |
+
)}
|
| 290 |
+
{activeMetric === "all" && (
|
| 291 |
+
<Line
|
| 292 |
+
type="monotone"
|
| 293 |
+
dataKey="bounce"
|
| 294 |
+
name="Bounces"
|
| 295 |
+
stroke="var(--nt-danger)"
|
| 296 |
+
strokeWidth={2}
|
| 297 |
+
strokeDasharray="4 4"
|
| 298 |
+
dot={false}
|
| 299 |
+
animationDuration={1500}
|
| 300 |
+
/>
|
| 301 |
+
)}
|
| 302 |
+
</LineChart>
|
| 303 |
+
</ResponsiveContainer>
|
| 304 |
+
</div>
|
| 305 |
+
|
| 306 |
+
<div className="mt-4 pt-3 border-t border-white/5 flex items-center justify-between text-xs text-zinc-400">
|
| 307 |
+
<div className="flex items-center gap-4 font-sans">
|
| 308 |
+
<span className="flex items-center gap-1.5">
|
| 309 |
+
<span className="w-2.5 h-2.5 rounded-full bg-blue-500 shadow-[0_0_8px_rgba(63,156,132,0.8)]" /> Sent
|
| 310 |
+
</span>
|
| 311 |
+
<span className="flex items-center gap-1.5">
|
| 312 |
+
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(37,125,104,0.8)]" /> Opens
|
| 313 |
+
</span>
|
| 314 |
+
<span className="flex items-center gap-1.5">
|
| 315 |
+
<span className="w-2.5 h-2.5 rounded-full bg-purple-500 shadow-[0_0_8px_rgba(178,87,49,0.8)]" /> Replies
|
| 316 |
+
</span>
|
| 317 |
+
<span className="flex items-center gap-1.5">
|
| 318 |
+
<span className="w-2.5 h-2.5 rounded-full bg-rose-500 shadow-[0_0_8px_rgba(203,117,80,0.8)]" /> Bounces
|
| 319 |
+
</span>
|
| 320 |
+
</div>
|
| 321 |
+
<Link to="/analytics" className="text-indigo-400 hover:text-indigo-300 font-medium">
|
| 322 |
+
View Detailed Analytics →
|
| 323 |
+
</Link>
|
| 324 |
+
</div>
|
| 325 |
+
</div>
|
| 326 |
+
|
| 327 |
+
{/* Lead Status Distribution Donut */}
|
| 328 |
+
<div className="bg-white/5 border border-white/10 backdrop-blur-md rounded-xl p-5 shadow-[0_8px_32px_-8px_rgba(6,10,12,0.65)] flex flex-col justify-between">
|
| 329 |
+
<div>
|
| 330 |
+
<h3 className="text-base font-semibold text-zinc-100 flex items-center gap-2">
|
| 331 |
+
<Zap className="w-4 h-4 text-amber-400 drop-shadow-[0_0_8px_rgba(194,160,74,0.5)]" />
|
| 332 |
+
Lead Intent Distribution
|
| 333 |
+
</h3>
|
| 334 |
+
<p className="text-xs text-zinc-400 mt-0.5">Classification of prospect responses.</p>
|
| 335 |
+
</div>
|
| 336 |
+
|
| 337 |
+
<div className="relative h-[220px] w-full flex items-center justify-center my-2">
|
| 338 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 339 |
+
<PieChart>
|
| 340 |
+
<Pie
|
| 341 |
+
data={pieData}
|
| 342 |
+
innerRadius={65}
|
| 343 |
+
outerRadius={95}
|
| 344 |
+
paddingAngle={4}
|
| 345 |
+
dataKey="value"
|
| 346 |
+
stroke="none"
|
| 347 |
+
animationDuration={1500}
|
| 348 |
+
>
|
| 349 |
+
{pieData.map((entry, index) => (
|
| 350 |
+
<Cell key={`cell-${index}`} fill={entry.color} />
|
| 351 |
+
))}
|
| 352 |
+
</Pie>
|
| 353 |
+
<Tooltip
|
| 354 |
+
contentStyle={{
|
| 355 |
+
backgroundColor: "rgba(6, 10, 12, 0.88)",
|
| 356 |
+
backdropFilter: "blur(12px)",
|
| 357 |
+
borderColor: "rgba(37,125,104,0.2)",
|
| 358 |
+
borderRadius: "12px",
|
| 359 |
+
fontSize: "12px",
|
| 360 |
+
}}
|
| 361 |
+
/>
|
| 362 |
+
</PieChart>
|
| 363 |
+
</ResponsiveContainer>
|
| 364 |
+
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none drop-shadow-[0_0_12px_rgba(246,248,242,0.3)]">
|
| 365 |
+
<span className="text-2xl font-bold text-white font-mono">1,015</span>
|
| 366 |
+
<span className="text-[10px] uppercase tracking-wider text-zinc-400 font-medium font-sans">
|
| 367 |
+
Classified
|
| 368 |
+
</span>
|
| 369 |
+
</div>
|
| 370 |
+
</div>
|
| 371 |
+
|
| 372 |
+
<div className="space-y-1.5 pt-3 border-t border-white/5 text-xs font-sans">
|
| 373 |
+
{pieData.map((item) => (
|
| 374 |
+
<div key={item.name} className="flex items-center justify-between text-zinc-300">
|
| 375 |
+
<span className="flex items-center gap-2">
|
| 376 |
+
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: item.color, boxShadow: `0 0 8px ${item.color}` }} />
|
| 377 |
+
{item.name}
|
| 378 |
+
</span>
|
| 379 |
+
<span className="font-semibold text-zinc-200 font-mono">{item.value}</span>
|
| 380 |
+
</div>
|
| 381 |
+
))}
|
| 382 |
+
</div>
|
| 383 |
+
</div>
|
| 384 |
+
</div>
|
| 385 |
+
|
| 386 |
+
{/* Active Campaigns Overview Table */}
|
| 387 |
+
<div className="bg-white/5 border border-white/10 backdrop-blur-md rounded-xl overflow-hidden shadow-[0_8px_32px_-8px_rgba(6,10,12,0.65)]">
|
| 388 |
+
<div className="px-6 py-4 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
|
| 389 |
+
<div>
|
| 390 |
+
<h3 className="text-base font-semibold text-zinc-100 flex items-center gap-2">
|
| 391 |
+
<Radio className="w-4 h-4 text-indigo-400 drop-shadow-[0_0_8px_rgba(37,125,104,0.5)]" />
|
| 392 |
+
Active Outreach Sequences
|
| 393 |
+
</h3>
|
| 394 |
+
<p className="text-xs text-zinc-400 mt-0.5">
|
| 395 |
+
Live status, recipient quotas, and response metrics.
|
| 396 |
+
</p>
|
| 397 |
+
</div>
|
| 398 |
+
<Link
|
| 399 |
+
to="/campaigns"
|
| 400 |
+
className="text-xs font-semibold text-indigo-400 hover:text-indigo-300 transition-colors"
|
| 401 |
+
>
|
| 402 |
+
Manage Campaigns →
|
| 403 |
+
</Link>
|
| 404 |
+
</div>
|
| 405 |
+
|
| 406 |
+
<div className="overflow-x-auto">
|
| 407 |
+
<table className="w-full text-sm text-left">
|
| 408 |
+
<thead className="bg-black/20 border-b border-white/5 text-zinc-400 text-xs uppercase">
|
| 409 |
+
<tr>
|
| 410 |
+
<th className="px-6 py-3 font-medium">Campaign</th>
|
| 411 |
+
<th className="px-6 py-3 font-medium">Status</th>
|
| 412 |
+
<th className="px-6 py-3 font-medium">Leads</th>
|
| 413 |
+
<th className="px-6 py-3 font-medium">Sent</th>
|
| 414 |
+
<th className="px-6 py-3 font-medium">Opens</th>
|
| 415 |
+
<th className="px-6 py-3 font-medium">Replies</th>
|
| 416 |
+
<th className="px-6 py-3 font-medium text-right">Action</th>
|
| 417 |
+
</tr>
|
| 418 |
+
</thead>
|
| 419 |
+
<tbody className="divide-y divide-white/5">
|
| 420 |
+
{(Array.isArray(campaigns) ? campaigns : []).map((camp: any) => (
|
| 421 |
+
<tr key={camp.id} className="hover:bg-white/5 transition-colors">
|
| 422 |
+
<td className="px-6 py-4 font-semibold text-zinc-100">
|
| 423 |
+
<Link to={`/campaigns/${camp.id}`} className="hover:text-indigo-400 transition-colors">
|
| 424 |
+
{camp.name}
|
| 425 |
+
</Link>
|
| 426 |
+
<div className="text-[11px] text-zinc-400 font-normal mt-0.5 font-sans">
|
| 427 |
+
{camp.timezone} • {camp.sequences?.length || 1} step sequence
|
| 428 |
+
</div>
|
| 429 |
+
</td>
|
| 430 |
+
<td className="px-6 py-4">
|
| 431 |
+
<StatusBadge status={camp.status} />
|
| 432 |
+
</td>
|
| 433 |
+
<td className="px-6 py-4 text-zinc-300 font-mono">{camp.lead_count || 0}</td>
|
| 434 |
+
<td className="px-6 py-4 text-zinc-300 font-mono">{camp.sent_count || 0}</td>
|
| 435 |
+
<td className="px-6 py-4 text-emerald-400 font-mono drop-shadow-[0_0_8px_rgba(63,156,132,0.3)]">{camp.open_count || 0}</td>
|
| 436 |
+
<td className="px-6 py-4 text-purple-400 font-mono font-semibold drop-shadow-[0_0_8px_rgba(178,87,49,0.3)]">{camp.reply_count || 0}</td>
|
| 437 |
+
<td className="px-6 py-4 text-right">
|
| 438 |
+
<Link
|
| 439 |
+
to={`/campaigns/${camp.id}`}
|
| 440 |
+
className="px-3 py-1.5 bg-white/5 hover:bg-white/10 text-zinc-200 text-xs font-medium rounded-lg border border-white/5 transition-all inline-flex items-center gap-1 shadow-[inset_0_1px_1px_rgba(246,248,242,0.05)]"
|
| 441 |
+
>
|
| 442 |
+
Inspect <ArrowUpRight className="w-3 h-3" />
|
| 443 |
+
</Link>
|
| 444 |
+
</td>
|
| 445 |
+
</tr>
|
| 446 |
+
))}
|
| 447 |
+
</tbody>
|
| 448 |
+
</table>
|
| 449 |
+
</div>
|
| 450 |
+
</div>
|
| 451 |
+
|
| 452 |
+
{/* Quick Campaign Creator Slide-Over Drawer */}
|
| 453 |
+
<FormDrawer
|
| 454 |
+
isOpen={isCreateCampaignOpen}
|
| 455 |
+
onClose={() => setIsCreateCampaignOpen(false)}
|
| 456 |
+
title="Create New Campaign"
|
| 457 |
+
subtitle="Configure schedule, tracking domains, and email sequence steps."
|
| 458 |
+
>
|
| 459 |
+
<form
|
| 460 |
+
onSubmit={async (e) => {
|
| 461 |
+
e.preventDefault();
|
| 462 |
+
const form = e.target as HTMLFormElement;
|
| 463 |
+
const name = (form.elements.namedItem("name") as HTMLInputElement).value;
|
| 464 |
+
const timezone = (form.elements.namedItem("timezone") as HTMLSelectElement).value;
|
| 465 |
+
const initial_subject = (form.elements.namedItem("subject") as HTMLInputElement).value;
|
| 466 |
+
const initial_body = (form.elements.namedItem("body") as HTMLTextAreaElement).value;
|
| 467 |
+
|
| 468 |
+
try {
|
| 469 |
+
await apiClient.post("/campaigns", {
|
| 470 |
+
name,
|
| 471 |
+
timezone,
|
| 472 |
+
initial_subject,
|
| 473 |
+
initial_body,
|
| 474 |
+
track_settings: "TRACK_EMAIL_OPEN_AND_CLICK",
|
| 475 |
+
});
|
| 476 |
+
await queryClient.invalidateQueries({ queryKey: ["campaigns"] });
|
| 477 |
+
toast.success("Campaign created successfully");
|
| 478 |
+
setIsCreateCampaignOpen(false);
|
| 479 |
+
} catch (err: any) {
|
| 480 |
+
toast.error(err.message || "Failed to create campaign");
|
| 481 |
+
}
|
| 482 |
+
}}
|
| 483 |
+
className="space-y-4 text-sm"
|
| 484 |
+
>
|
| 485 |
+
<div>
|
| 486 |
+
<label className="block font-medium text-zinc-300 mb-1">Campaign Name</label>
|
| 487 |
+
<input
|
| 488 |
+
name="name"
|
| 489 |
+
required
|
| 490 |
+
placeholder="e.g. Q4 Tech VP Outreach"
|
| 491 |
+
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100 focus:outline-none focus:border-indigo-500"
|
| 492 |
+
/>
|
| 493 |
+
</div>
|
| 494 |
+
|
| 495 |
+
<div>
|
| 496 |
+
<label className="block font-medium text-zinc-300 mb-1">Timezone</label>
|
| 497 |
+
<select
|
| 498 |
+
name="timezone"
|
| 499 |
+
defaultValue="America/New_York"
|
| 500 |
+
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100 focus:outline-none focus:border-indigo-500"
|
| 501 |
+
>
|
| 502 |
+
<option value="America/New_York">Eastern Time (US & Canada)</option>
|
| 503 |
+
<option value="America/Chicago">Central Time (US & Canada)</option>
|
| 504 |
+
<option value="America/Los_Angeles">Pacific Time (US & Canada)</option>
|
| 505 |
+
<option value="Europe/London">London (GMT)</option>
|
| 506 |
+
<option value="Europe/Paris">Central European Time (CET)</option>
|
| 507 |
+
<option value="Asia/Singapore">Singapore (SGT)</option>
|
| 508 |
+
</select>
|
| 509 |
+
</div>
|
| 510 |
+
|
| 511 |
+
<div>
|
| 512 |
+
<label className="block font-medium text-zinc-300 mb-1">Step 1: Subject Line</label>
|
| 513 |
+
<input
|
| 514 |
+
name="subject"
|
| 515 |
+
required
|
| 516 |
+
defaultValue="Question about {{company_name}}'s pipeline"
|
| 517 |
+
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100 focus:outline-none focus:border-indigo-500"
|
| 518 |
+
/>
|
| 519 |
+
</div>
|
| 520 |
+
|
| 521 |
+
<div>
|
| 522 |
+
<label className="block font-medium text-zinc-300 mb-1">Step 1: Email Body</label>
|
| 523 |
+
<textarea
|
| 524 |
+
name="body"
|
| 525 |
+
rows={4}
|
| 526 |
+
required
|
| 527 |
+
defaultValue="Hi {{first_name}},\n\nWanted to reach out and see if you are exploring new outbound channels this quarter?\n\nBest,\nAlex"
|
| 528 |
+
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100 focus:outline-none focus:border-indigo-500 font-mono text-xs"
|
| 529 |
+
/>
|
| 530 |
+
</div>
|
| 531 |
+
|
| 532 |
+
<div className="pt-4 flex justify-end gap-3">
|
| 533 |
+
<button
|
| 534 |
+
type="button"
|
| 535 |
+
onClick={() => setIsCreateCampaignOpen(false)}
|
| 536 |
+
className="px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg"
|
| 537 |
+
>
|
| 538 |
+
Cancel
|
| 539 |
+
</button>
|
| 540 |
+
<button
|
| 541 |
+
type="submit"
|
| 542 |
+
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-medium rounded-lg shadow-md"
|
| 543 |
+
>
|
| 544 |
+
Create Campaign
|
| 545 |
+
</button>
|
| 546 |
+
</div>
|
| 547 |
+
</form>
|
| 548 |
+
</FormDrawer>
|
| 549 |
+
</div>
|
| 550 |
+
);
|
| 551 |
+
}
|