modul-e / src /pages /Dashboard.tsx
sitcod3's picture
Upload src/pages/Dashboard.tsx with huggingface_hub
2b38dda verified
Raw History Blame Contribute Delete
25.7 kB
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { apiClient } from "../lib/apiClient";
import {
Megaphone,
Users,
Mail,
Inbox,
Flame,
TrendingUp,
Sparkles,
Activity,
Plus,
ArrowUpRight,
CheckCircle2,
ShieldCheck,
Radio,
Zap
} from "lucide-react";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell
} from "recharts";
import { useState } from "react";
import { Link } from "react-router-dom";
import StatCard from "../components/StatCard";
import StatusBadge from "../components/StatusBadge";
import FormDrawer from "../components/FormDrawer";
import toast from "react-hot-toast";
const lineData = [
{ date: "Day 1", sent: 480, open: 310, reply: 28, bounce: 6 },
{ date: "Day 4", sent: 820, open: 530, reply: 46, bounce: 10 },
{ date: "Day 7", sent: 1250, open: 820, reply: 74, bounce: 14 },
{ date: "Day 10", sent: 1780, open: 1190, reply: 105, bounce: 19 },
{ date: "Day 14", sent: 2420, open: 1650, reply: 148, bounce: 23 },
{ date: "Day 18", sent: 3150, open: 2190, reply: 196, bounce: 31 },
{ date: "Day 22", sent: 4020, open: 2840, reply: 254, bounce: 38 },
{ date: "Day 26", sent: 4980, open: 3510, reply: 312, bounce: 46 },
{ date: "Day 30", sent: 5890, open: 4120, reply: 378, bounce: 52 },
];
const pieData = [
{ name: "Interested", value: 340, color: "var(--nt-success)" },
{ name: "Meeting Booked", value: 160, color: "var(--nt-accent)" },
{ name: "Replied", value: 290, color: "var(--nt-info)" },
{ name: "Not Interested", value: 180, color: "var(--nt-danger)" },
{ name: "Bounced", value: 45, color: "var(--nt-espresso-700)" },
];
export default function Dashboard() {
const queryClient = useQueryClient();
const [isCreateCampaignOpen, setIsCreateCampaignOpen] = useState(false);
const [activeMetric, setActiveMetric] = useState<"all" | "open" | "reply">("all");
const { data: analytics, isLoading: isAnalyticsLoading, isError: isAnalyticsError } = useQuery({
queryKey: ["analytics-summary"],
queryFn: async () => {
const res = await apiClient.get("/analytics/campaigns");
return res.data;
},
});
const { data: campaigns, isLoading: isCampaignsLoading, isError: isCampaignsError } = useQuery({
queryKey: ["campaigns"],
queryFn: async () => {
const res = await apiClient.get("/campaigns");
return res.data;
},
});
const { data: emailAccounts = [], isLoading: isEmailAccountsLoading } = useQuery({
queryKey: ["email-accounts"],
queryFn: async () => {
const res = await apiClient.get("/email-accounts");
return res.data;
},
});
const warmupDailyCount = emailAccounts.reduce((total: number, account: any) => total + (account.warmup_daily_count || 0), 0);
const connectedMailboxCount = emailAccounts.filter((account: any) => account.status === "CONNECTED").length;
const warmupScoreValues = emailAccounts.map((account: any) => account.warmup_score).filter((score: any) => typeof score === "number");
const averageWarmupScore = warmupScoreValues.length
? (warmupScoreValues.reduce((total: number, score: number) => total + score, 0) / warmupScoreValues.length).toFixed(1)
: null;
return (
<div className="p-6 max-w-7xl mx-auto space-y-7">
{/* Header with Live Status & Quick Action Buttons */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5">
Command Center
</h1>
<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">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-ping" />
Live Engine
</span>
</div>
<p className="text-zinc-400 text-sm mt-1">
Real-time deliverability radar, campaign sequencing, and cold outreach throughput.
</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setIsCreateCampaignOpen(true)}
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"
>
<Plus className="w-4 h-4" />
Create Campaign
</button>
</div>
</div>
{/* High-Impact Stat Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
title="Total Campaigns"
value={isCampaignsLoading ? "…" : isCampaignsError ? "—" : campaigns?.length ?? 0}
subtitle="Outbound Sequences"
icon={Megaphone}
iconColor="indigo"
change={{ value: "+2 new", isPositive: true, period: "this week" }}
/>
<StatCard
title="Total Leads"
value={isAnalyticsLoading ? "…" : isAnalyticsError ? "—" : (analytics?.total_leads ?? 0).toLocaleString()}
subtitle="Prospects Targeted"
icon={Users}
iconColor="teal"
change={{ value: "+380", isPositive: true, period: "last 7 days" }}
/>
<StatCard
title="Emails Sent"
value={isAnalyticsLoading ? "…" : isAnalyticsError ? "—" : (analytics?.total_sent ?? 0).toLocaleString()}
subtitle="Across All Mailboxes"
icon={Mail}
iconColor="blue"
change={{ value: "+14.8%", isPositive: true, period: "month-over-month" }}
/>
<StatCard
title="Avg Reply Rate"
value={isAnalyticsLoading ? "…" : isAnalyticsError ? "—" : analytics?.reply_rate != null ? `${analytics.reply_rate}%` : "—"}
subtitle="Deliverability High"
icon={Inbox}
iconColor="emerald"
change={{ value: "+1.2%", isPositive: true, period: "vs benchmark" }}
/>
</div>
{/* Deliverability & Mailbox Health Ribbon */}
<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)]">
<div className="flex items-center gap-3">
<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)]">
<ShieldCheck className="w-5 h-5 drop-shadow-[0_0_8px_rgba(63,156,132,0.5)]" />
</div>
<div>
<h4 className="text-sm font-semibold text-zinc-100 flex items-center gap-2">
Deliverability & Reputation Guard
<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)]">
{averageWarmupScore ? `${averageWarmupScore}% Score` : isEmailAccountsLoading ? "Checking score" : "No score"}
</span>
</h4>
<p className="text-xs text-zinc-400 mt-0.5">
{connectedMailboxCount} connected mailboxes with {warmupDailyCount} daily warmup messages.
</p>
</div>
</div>
<div className="flex items-center gap-4 text-xs">
<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)]">
<Flame className="w-4 h-4 text-amber-400 drop-shadow-[0_0_8px_rgba(194,160,74,0.5)]" />
<span className="text-zinc-300 font-medium font-sans">Warmup:</span>
<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>
</div>
<Link
to="/warmup"
className="text-indigo-400 hover:text-indigo-300 font-medium flex items-center gap-1 transition-colors"
>
Warmup Center <ArrowUpRight className="w-3.5 h-3.5" />
</Link>
</div>
</div>
{/* Main Visualizations Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 30-Day Performance Multi-Line Chart */}
<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">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-6">
<div>
<h3 className="text-base font-semibold text-zinc-100 flex items-center gap-2">
<Activity className="w-4 h-4 text-indigo-400 drop-shadow-[0_0_8px_rgba(37,125,104,0.5)]" />
Performance Trajectory (30 Days)
</h3>
<p className="text-xs text-zinc-400 mt-0.5">
Sent volume, open rate, and reply convergence.
</p>
</div>
{/* Metric toggles */}
<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)]">
<button
onClick={() => setActiveMetric("all")}
className={`px-3 py-1 rounded-md font-medium transition-all duration-300 ${
activeMetric === "all" ? "bg-indigo-600 text-white shadow-[0_0_12px_rgba(37,125,104,0.5)]" : "text-zinc-400 hover:text-zinc-200"
}`}
>
All Metrics
</button>
<button
onClick={() => setActiveMetric("open")}
className={`px-3 py-1 rounded-md font-medium transition-all duration-300 ${
activeMetric === "open" ? "bg-emerald-600 text-white shadow-[0_0_12px_rgba(37,125,104,0.5)]" : "text-zinc-400 hover:text-zinc-200"
}`}
>
Opens
</button>
<button
onClick={() => setActiveMetric("reply")}
className={`px-3 py-1 rounded-md font-medium transition-all duration-300 ${
activeMetric === "reply" ? "bg-purple-600 text-white shadow-[0_0_12px_rgba(178,87,49,0.5)]" : "text-zinc-400 hover:text-zinc-200"
}`}
>
Replies
</button>
</div>
</div>
<div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={lineData} margin={{ top: 5, right: 10, bottom: 5, left: -10 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(246,248,242,0.05)" vertical={false} />
<XAxis dataKey="date" stroke="var(--nt-text-muted)" fontSize={12} tickLine={false} axisLine={false} />
<YAxis stroke="var(--nt-text-muted)" fontSize={12} tickLine={false} axisLine={false} className="font-mono" />
<Tooltip
contentStyle={{
backgroundColor: "rgba(6, 10, 12, 0.88)",
backdropFilter: "blur(12px)",
borderColor: "rgba(37,125,104,0.2)",
borderRadius: "12px",
color: "var(--nt-text-primary)",
fontSize: "12px",
}}
/>
{(activeMetric === "all" || activeMetric === "open") && (
<Line
type="monotone"
dataKey="sent"
name="Sent"
stroke="var(--nt-deep-teal-600)"
strokeWidth={3}
dot={{ r: 4, fill: "var(--nt-deep-teal-600)", strokeWidth: 2, stroke: "var(--nt-focus-ring-offset)" }}
activeDot={{ r: 6, fill: "var(--nt-deep-teal-700)" }}
animationDuration={1500}
/>
)}
{(activeMetric === "all" || activeMetric === "open") && (
<Line
type="monotone"
dataKey="open"
name="Opens"
stroke="var(--nt-deep-teal-500)"
strokeWidth={3}
dot={{ r: 4, fill: "var(--nt-deep-teal-500)", strokeWidth: 2, stroke: "var(--nt-focus-ring-offset)" }}
activeDot={{ r: 6, fill: "var(--nt-deep-teal-700)" }}
animationDuration={1500}
/>
)}
{(activeMetric === "all" || activeMetric === "reply") && (
<Line
type="monotone"
dataKey="reply"
name="Replies"
stroke="var(--nt-terracotta-500)"
strokeWidth={3}
dot={{ r: 4, fill: "var(--nt-terracotta-500)", strokeWidth: 2, stroke: "var(--nt-focus-ring-offset)" }}
activeDot={{ r: 6, fill: "var(--nt-terracotta-700)" }}
animationDuration={1500}
/>
)}
{activeMetric === "all" && (
<Line
type="monotone"
dataKey="bounce"
name="Bounces"
stroke="var(--nt-danger)"
strokeWidth={2}
strokeDasharray="4 4"
dot={false}
animationDuration={1500}
/>
)}
</LineChart>
</ResponsiveContainer>
</div>
<div className="mt-4 pt-3 border-t border-white/5 flex items-center justify-between text-xs text-zinc-400">
<div className="flex items-center gap-4 font-sans">
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-blue-500 shadow-[0_0_8px_rgba(63,156,132,0.8)]" /> Sent
</span>
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(37,125,104,0.8)]" /> Opens
</span>
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-purple-500 shadow-[0_0_8px_rgba(178,87,49,0.8)]" /> Replies
</span>
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-rose-500 shadow-[0_0_8px_rgba(203,117,80,0.8)]" /> Bounces
</span>
</div>
<Link to="/analytics" className="text-indigo-400 hover:text-indigo-300 font-medium">
View Detailed Analytics →
</Link>
</div>
</div>
{/* Lead Status Distribution Donut */}
<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">
<div>
<h3 className="text-base font-semibold text-zinc-100 flex items-center gap-2">
<Zap className="w-4 h-4 text-amber-400 drop-shadow-[0_0_8px_rgba(194,160,74,0.5)]" />
Lead Intent Distribution
</h3>
<p className="text-xs text-zinc-400 mt-0.5">Classification of prospect responses.</p>
</div>
<div className="relative h-[220px] w-full flex items-center justify-center my-2">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={pieData}
innerRadius={65}
outerRadius={95}
paddingAngle={4}
dataKey="value"
stroke="none"
animationDuration={1500}
>
{pieData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: "rgba(6, 10, 12, 0.88)",
backdropFilter: "blur(12px)",
borderColor: "rgba(37,125,104,0.2)",
borderRadius: "12px",
fontSize: "12px",
}}
/>
</PieChart>
</ResponsiveContainer>
<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)]">
<span className="text-2xl font-bold text-white font-mono">1,015</span>
<span className="text-[10px] uppercase tracking-wider text-zinc-400 font-medium font-sans">
Classified
</span>
</div>
</div>
<div className="space-y-1.5 pt-3 border-t border-white/5 text-xs font-sans">
{pieData.map((item) => (
<div key={item.name} className="flex items-center justify-between text-zinc-300">
<span className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: item.color, boxShadow: `0 0 8px ${item.color}` }} />
{item.name}
</span>
<span className="font-semibold text-zinc-200 font-mono">{item.value}</span>
</div>
))}
</div>
</div>
</div>
{/* Active Campaigns Overview Table */}
<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)]">
<div className="px-6 py-4 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
<div>
<h3 className="text-base font-semibold text-zinc-100 flex items-center gap-2">
<Radio className="w-4 h-4 text-indigo-400 drop-shadow-[0_0_8px_rgba(37,125,104,0.5)]" />
Active Outreach Sequences
</h3>
<p className="text-xs text-zinc-400 mt-0.5">
Live status, recipient quotas, and response metrics.
</p>
</div>
<Link
to="/campaigns"
className="text-xs font-semibold text-indigo-400 hover:text-indigo-300 transition-colors"
>
Manage Campaigns →
</Link>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="bg-black/20 border-b border-white/5 text-zinc-400 text-xs uppercase">
<tr>
<th className="px-6 py-3 font-medium">Campaign</th>
<th className="px-6 py-3 font-medium">Status</th>
<th className="px-6 py-3 font-medium">Leads</th>
<th className="px-6 py-3 font-medium">Sent</th>
<th className="px-6 py-3 font-medium">Opens</th>
<th className="px-6 py-3 font-medium">Replies</th>
<th className="px-6 py-3 font-medium text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{(Array.isArray(campaigns) ? campaigns : []).map((camp: any) => (
<tr key={camp.id} className="hover:bg-white/5 transition-colors">
<td className="px-6 py-4 font-semibold text-zinc-100">
<Link to={`/campaigns/${camp.id}`} className="hover:text-indigo-400 transition-colors">
{camp.name}
</Link>
<div className="text-[11px] text-zinc-400 font-normal mt-0.5 font-sans">
{camp.timezone} • {camp.sequences?.length || 1} step sequence
</div>
</td>
<td className="px-6 py-4">
<StatusBadge status={camp.status} />
</td>
<td className="px-6 py-4 text-zinc-300 font-mono">{camp.lead_count || 0}</td>
<td className="px-6 py-4 text-zinc-300 font-mono">{camp.sent_count || 0}</td>
<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>
<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>
<td className="px-6 py-4 text-right">
<Link
to={`/campaigns/${camp.id}`}
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)]"
>
Inspect <ArrowUpRight className="w-3 h-3" />
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Quick Campaign Creator Slide-Over Drawer */}
<FormDrawer
isOpen={isCreateCampaignOpen}
onClose={() => setIsCreateCampaignOpen(false)}
title="Create New Campaign"
subtitle="Configure schedule, tracking domains, and email sequence steps."
>
<form
onSubmit={async (e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const name = (form.elements.namedItem("name") as HTMLInputElement).value;
const timezone = (form.elements.namedItem("timezone") as HTMLSelectElement).value;
const initial_subject = (form.elements.namedItem("subject") as HTMLInputElement).value;
const initial_body = (form.elements.namedItem("body") as HTMLTextAreaElement).value;
try {
await apiClient.post("/campaigns", {
name,
timezone,
initial_subject,
initial_body,
track_settings: "TRACK_EMAIL_OPEN_AND_CLICK",
});
await queryClient.invalidateQueries({ queryKey: ["campaigns"] });
toast.success("Campaign created successfully");
setIsCreateCampaignOpen(false);
} catch (err: any) {
toast.error(err.message || "Failed to create campaign");
}
}}
className="space-y-4 text-sm"
>
<div>
<label className="block font-medium text-zinc-300 mb-1">Campaign Name</label>
<input
name="name"
required
placeholder="e.g. Q4 Tech VP Outreach"
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"
/>
</div>
<div>
<label className="block font-medium text-zinc-300 mb-1">Timezone</label>
<select
name="timezone"
defaultValue="America/New_York"
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"
>
<option value="America/New_York">Eastern Time (US & Canada)</option>
<option value="America/Chicago">Central Time (US & Canada)</option>
<option value="America/Los_Angeles">Pacific Time (US & Canada)</option>
<option value="Europe/London">London (GMT)</option>
<option value="Europe/Paris">Central European Time (CET)</option>
<option value="Asia/Singapore">Singapore (SGT)</option>
</select>
</div>
<div>
<label className="block font-medium text-zinc-300 mb-1">Step 1: Subject Line</label>
<input
name="subject"
required
defaultValue="Question about {{company_name}}'s pipeline"
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"
/>
</div>
<div>
<label className="block font-medium text-zinc-300 mb-1">Step 1: Email Body</label>
<textarea
name="body"
rows={4}
required
defaultValue="Hi {{first_name}},\n\nWanted to reach out and see if you are exploring new outbound channels this quarter?\n\nBest,\nAlex"
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"
/>
</div>
<div className="pt-4 flex justify-end gap-3">
<button
type="button"
onClick={() => setIsCreateCampaignOpen(false)}
className="px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-medium rounded-lg shadow-md"
>
Create Campaign
</button>
</div>
</form>
</FormDrawer>
</div>
);
}