Download src/pages/EmailAccounts.tsx from sitcod3/modul-e: direct link, hf CLI and curl.
- Browser
- Download file 19 kB
-
https://huggingface.co/spaces/sitcod3/modul-e/resolve/main/src/pages/EmailAccounts.tsx
- Command line
-
hf download hf://spaces/sitcod3/modul-e/src/pages/EmailAccounts.tsx
-
curl -L -o EmailAccounts.tsx https://huggingface.co/spaces/sitcod3/modul-e/resolve/main/src/pages/EmailAccounts.tsx
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> | |
| ); | |
| } | |