modul-e / src /pages /EmailAccounts.tsx
sitcod3's picture
Upload src/pages/EmailAccounts.tsx with huggingface_hub
cd94c67 verified
Raw History Blame Contribute Delete
19 kB
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { apiClient } from "../lib/apiClient";
import {
Mail,
Plus,
RefreshCw,
Trash2,
CheckCircle2,
AlertCircle,
Server,
Flame,
Sliders,
ShieldCheck
} from "lucide-react";
import { useState } from "react";
import StatusBadge from "../components/StatusBadge";
import FormDrawer from "../components/FormDrawer";
import ConfirmModal from "../components/ConfirmModal";
import toast from "react-hot-toast";
export default function EmailAccounts() {
const queryClient = useQueryClient();
const [isAddOpen, setIsAddOpen] = useState(false);
const [activeSetupTab, setActiveSetupTab] = useState<"smtp" | "google" | "outlook">("smtp");
const [selectedForDelete, setSelectedForDelete] = useState<string | null>(null);
const { data: accounts = [], isLoading, refetch } = useQuery({
queryKey: ["email-accounts"],
queryFn: async () => {
const res = await apiClient.get("/email-accounts");
return res.data;
},
});
const reconnectMutation = useMutation({
mutationFn: async (id: string) => {
const res = await apiClient.post("/email-accounts/reconnect", { account_id: id });
return res.data;
},
onSuccess: () => {
toast.success("Mailbox connection verified and active");
refetch();
},
onError: () => {
toast.error("Failed to verify mailbox connection");
}
});
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
const res = await apiClient.delete(`/email-accounts/${id}`);
return res.data;
},
onSuccess: () => {
toast.success("Email account disconnected");
setSelectedForDelete(null);
refetch();
},
});
const getProviderIcon = (provider: string) => {
switch (provider?.toLowerCase()) {
case "google":
case "gmail":
return <span className="text-red-400 font-bold">G</span>;
case "microsoft":
case "outlook":
return <span className="text-blue-400 font-bold">M</span>;
default:
return <Server className="w-3.5 h-3.5 text-zinc-400" />;
}
};
return (
<div className="p-6 max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5">
<Mail className="w-6 h-6 text-blue-400" />
Sending Inboxes & Mailboxes
</h1>
<p className="text-zinc-400 text-sm mt-1">
Connect and manage sending infrastructure across Google Workspace, Microsoft 365, and custom SMTP/IMAP.
</p>
</div>
<button
onClick={() => setIsAddOpen(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" /> Add Mailbox
</button>
</div>
{/* Overview Cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-4">
<span className="text-xs text-zinc-400">Total Sending Capacity</span>
<div className="text-2xl font-bold text-zinc-100 mt-1">
{accounts.reduce((sum: number, a: any) => sum + (a.daily_limit || 0), 0)} / day
</div>
<span className="text-[11px] text-zinc-500">{accounts.length} connected mailboxes</span>
</div>
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-4">
<span className="text-xs text-zinc-400">Active in Warmup</span>
<div className="text-2xl font-bold text-amber-400 mt-1">
{accounts.filter((a: any) => a.warmup_enabled).length} of {accounts.length}
</div>
<span className="text-[11px] text-zinc-500">Autonomous ramp-up active</span>
</div>
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-4">
<span className="text-xs text-zinc-400">Network Deliverability</span>
<div className="text-2xl font-bold text-emerald-400 mt-1">
{accounts.length ? `${(accounts.reduce((sum: number, account: any) => sum + (account.warmup_score || 0), 0) / accounts.length).toFixed(1)}%` : "—"}
</div>
<span className="text-[11px] text-zinc-500">All DNS / SPF / DKIM valid</span>
</div>
</div>
{/* Accounts Table */}
<div className="bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden shadow-sm">
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="bg-zinc-950 border-b border-zinc-800 text-zinc-400 text-xs uppercase">
<tr>
<th className="px-6 py-3.5 font-semibold">Account / Sender</th>
<th className="px-6 py-3.5 font-semibold">Provider</th>
<th className="px-6 py-3.5 font-semibold">Status</th>
<th className="px-6 py-3.5 font-semibold">Daily Limit & Usage</th>
<th className="px-6 py-3.5 font-semibold">Warmup</th>
<th className="px-6 py-3.5 font-semibold text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-800/80">
{accounts.map((acc: any) => {
const usage = acc.sent_today || 0;
const limit = acc.daily_limit || 0;
const percentage = limit ? Math.min(100, Math.round((usage / limit) * 100)) : 0;
return (
<tr key={acc.id} className="hover:bg-zinc-800/30 transition-colors">
<td className="px-6 py-4">
<div className="font-semibold text-zinc-100">{acc.from_name}</div>
<div className="text-xs text-zinc-400 font-mono mt-0.5">{acc.from_email}</div>
</td>
<td className="px-6 py-4">
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-zinc-950 border border-zinc-800 rounded-md text-xs text-zinc-300 font-medium capitalize">
{getProviderIcon(acc.provider)}
{acc.provider}
</span>
</td>
<td className="px-6 py-4">
<StatusBadge status={acc.status} size="sm" />
</td>
<td className="px-6 py-4">
<div className="w-40 space-y-1">
<div className="flex justify-between text-[11px] font-mono text-zinc-400">
<span>{usage} sent</span>
<span>{limit} limit</span>
</div>
<div className="w-full h-1.5 bg-zinc-950 rounded-full overflow-hidden border border-zinc-800">
<div
className={`h-full rounded-full ${
percentage > 90 ? "bg-amber-500" : "bg-indigo-500"
}`}
style={{ width: `${percentage}%` }}
/>
</div>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<Flame className={`w-4 h-4 ${acc.warmup_enabled ? "text-amber-400 animate-pulse" : "text-zinc-600"}`} />
<span className="text-xs font-medium text-zinc-300">
{acc.warmup_enabled ? "Ramping Up" : "Disabled"}
</span>
</div>
</td>
<td className="px-6 py-4 text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => reconnectMutation.mutate(acc.id)}
title="Verify Connection"
className="p-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-md transition-colors"
>
<RefreshCw className="w-3.5 h-3.5" />
</button>
<button
onClick={() => setSelectedForDelete(acc.id)}
title="Disconnect Mailbox"
className="p-1.5 bg-zinc-800 hover:bg-rose-500/20 text-zinc-400 hover:text-rose-400 rounded-md transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* Delete Confirmation */}
<ConfirmModal
isOpen={Boolean(selectedForDelete)}
onClose={() => setSelectedForDelete(null)}
onConfirm={() => selectedForDelete && deleteMutation.mutate(selectedForDelete)}
title="Disconnect Mailbox"
description="Are you sure you want to disconnect this email account? Active campaigns using this mailbox will cease dispatching from it."
confirmText="Disconnect"
/>
{/* Add Mailbox Drawer */}
<FormDrawer
isOpen={isAddOpen}
onClose={() => setIsAddOpen(false)}
title="Connect Sending Mailbox"
subtitle="Add Google Workspace, Microsoft 365, or Custom SMTP/IMAP credentials."
>
<div className="space-y-5 text-xs">
{/* Provider Selector Tabs */}
<div className="grid grid-cols-3 gap-2 p-1 bg-zinc-900 border border-zinc-800 rounded-xl">
<button
type="button"
onClick={() => setActiveSetupTab("smtp")}
className={`py-2 rounded-lg font-medium transition-colors ${
activeSetupTab === "smtp" ? "bg-indigo-600 text-white" : "text-zinc-400 hover:text-zinc-200"
}`}
>
SMTP / IMAP
</button>
<button
type="button"
onClick={() => setActiveSetupTab("google")}
className={`py-2 rounded-lg font-medium transition-colors ${
activeSetupTab === "google" ? "bg-indigo-600 text-white" : "text-zinc-400 hover:text-zinc-200"
}`}
>
Google OAuth
</button>
<button
type="button"
onClick={() => setActiveSetupTab("outlook")}
className={`py-2 rounded-lg font-medium transition-colors ${
activeSetupTab === "outlook" ? "bg-indigo-600 text-white" : "text-zinc-400 hover:text-zinc-200"
}`}
>
Microsoft 365
</button>
</div>
{activeSetupTab === "smtp" && (
<form
onSubmit={async (e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const data = {
from_name: (form.elements.namedItem("from_name") as HTMLInputElement).value,
from_email: (form.elements.namedItem("from_email") as HTMLInputElement).value,
smtp_host: (form.elements.namedItem("smtp_host") as HTMLInputElement).value,
smtp_port: Number((form.elements.namedItem("smtp_port") as HTMLInputElement).value),
smtp_username: (form.elements.namedItem("smtp_username") as HTMLInputElement).value,
smtp_password: (form.elements.namedItem("smtp_password") as HTMLInputElement).value,
imap_host: (form.elements.namedItem("imap_host") as HTMLInputElement).value,
imap_port: Number((form.elements.namedItem("imap_port") as HTMLInputElement).value),
imap_username: (form.elements.namedItem("imap_username") as HTMLInputElement).value,
imap_password: (form.elements.namedItem("imap_password") as HTMLInputElement).value,
daily_limit: Number((form.elements.namedItem("daily_limit") as HTMLInputElement).value),
provider: "smtp",
};
try {
await apiClient.post("/email-accounts/smtp", data);
toast.success("SMTP Mailbox connected successfully");
setIsAddOpen(false);
refetch();
} catch (err: any) {
toast.error(err.response?.data?.error || "Failed to connect SMTP account");
}
}}
className="space-y-4"
>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block font-medium text-zinc-300 mb-1">Sender Name *</label>
<input name="from_name" required placeholder="Alex Turner" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
</div>
<div>
<label className="block font-medium text-zinc-300 mb-1">Email Address *</label>
<input name="from_email" type="email" required placeholder="alex@company.com" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
</div>
</div>
<div className="pt-2 border-t border-zinc-800 space-y-3">
<h4 className="font-semibold text-zinc-400 uppercase tracking-wider text-[10px]">SMTP Configuration (Outbound)</h4>
<div className="grid grid-cols-3 gap-2">
<div className="col-span-2">
<label className="block font-medium text-zinc-400 mb-1">Host</label>
<input name="smtp_host" defaultValue="smtp.mailgun.org" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
</div>
<div>
<label className="block font-medium text-zinc-400 mb-1">Port</label>
<input name="smtp_port" defaultValue="587" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block font-medium text-zinc-400 mb-1">Username</label>
<input name="smtp_username" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
</div>
<div>
<label className="block font-medium text-zinc-400 mb-1">Password</label>
<input name="smtp_password" type="password" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
</div>
</div>
</div>
<div className="pt-2 border-t border-zinc-800 space-y-3">
<h4 className="font-semibold text-zinc-400 uppercase tracking-wider text-[10px]">IMAP Configuration (Inbox & Replies)</h4>
<div className="grid grid-cols-3 gap-2">
<div className="col-span-2">
<label className="block font-medium text-zinc-400 mb-1">Host</label>
<input name="imap_host" defaultValue="imap.mailgun.org" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
</div>
<div>
<label className="block font-medium text-zinc-400 mb-1">Port</label>
<input name="imap_port" defaultValue="993" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block font-medium text-zinc-400 mb-1">Username</label>
<input name="imap_username" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
</div>
<div>
<label className="block font-medium text-zinc-400 mb-1">Password</label>
<input name="imap_password" type="password" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
</div>
</div>
</div>
<div>
<label className="block font-medium text-zinc-300 mb-1">Max Daily Send Limit</label>
<input name="daily_limit" type="number" defaultValue="50" min="1" max="2000" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
</div>
<div className="pt-4 flex justify-end gap-2 border-t border-zinc-800">
<button
type="button"
onClick={() => setIsAddOpen(false)}
className="px-3 py-2 bg-zinc-800 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"
>
Connect Mailbox
</button>
</div>
</form>
)}
{(activeSetupTab === "google" || activeSetupTab === "outlook") && (
<div className="py-8 text-center space-y-4">
<div className="p-4 bg-zinc-900 rounded-xl border border-zinc-800 max-w-sm mx-auto">
<div className="text-2xl mb-2 font-bold">{activeSetupTab === "google" ? "Google Workspace" : "Microsoft 365"}</div>
<p className="text-zinc-400 text-xs">
Authorize direct OAuth access to dispatch outbound sequences with verified sender tokens.
</p>
<button
type="button"
onClick={() => {
toast.success(`Connected ${activeSetupTab} account`);
setIsAddOpen(false);
refetch();
}}
className="mt-4 w-full py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg shadow-md"
>
Authorize with {activeSetupTab === "google" ? "Google" : "Microsoft"}
</button>
</div>
</div>
)}
</div>
</FormDrawer>
</div>
);
}