sitcod3 commited on
Commit
cd94c67
·
verified ·
1 Parent(s): 2b38dda

Upload src/pages/EmailAccounts.tsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/pages/EmailAccounts.tsx +393 -0
src/pages/EmailAccounts.tsx ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { apiClient } from "../lib/apiClient";
3
+ import {
4
+ Mail,
5
+ Plus,
6
+ RefreshCw,
7
+ Trash2,
8
+ CheckCircle2,
9
+ AlertCircle,
10
+ Server,
11
+ Flame,
12
+ Sliders,
13
+ ShieldCheck
14
+ } from "lucide-react";
15
+ import { useState } from "react";
16
+ import StatusBadge from "../components/StatusBadge";
17
+ import FormDrawer from "../components/FormDrawer";
18
+ import ConfirmModal from "../components/ConfirmModal";
19
+ import toast from "react-hot-toast";
20
+
21
+ export default function EmailAccounts() {
22
+ const queryClient = useQueryClient();
23
+ const [isAddOpen, setIsAddOpen] = useState(false);
24
+ const [activeSetupTab, setActiveSetupTab] = useState<"smtp" | "google" | "outlook">("smtp");
25
+ const [selectedForDelete, setSelectedForDelete] = useState<string | null>(null);
26
+
27
+ const { data: accounts = [], isLoading, refetch } = useQuery({
28
+ queryKey: ["email-accounts"],
29
+ queryFn: async () => {
30
+ const res = await apiClient.get("/email-accounts");
31
+ return res.data;
32
+ },
33
+ });
34
+
35
+ const reconnectMutation = useMutation({
36
+ mutationFn: async (id: string) => {
37
+ const res = await apiClient.post("/email-accounts/reconnect", { account_id: id });
38
+ return res.data;
39
+ },
40
+ onSuccess: () => {
41
+ toast.success("Mailbox connection verified and active");
42
+ refetch();
43
+ },
44
+ onError: () => {
45
+ toast.error("Failed to verify mailbox connection");
46
+ }
47
+ });
48
+
49
+ const deleteMutation = useMutation({
50
+ mutationFn: async (id: string) => {
51
+ const res = await apiClient.delete(`/email-accounts/${id}`);
52
+ return res.data;
53
+ },
54
+ onSuccess: () => {
55
+ toast.success("Email account disconnected");
56
+ setSelectedForDelete(null);
57
+ refetch();
58
+ },
59
+ });
60
+
61
+ const getProviderIcon = (provider: string) => {
62
+ switch (provider?.toLowerCase()) {
63
+ case "google":
64
+ case "gmail":
65
+ return <span className="text-red-400 font-bold">G</span>;
66
+ case "microsoft":
67
+ case "outlook":
68
+ return <span className="text-blue-400 font-bold">M</span>;
69
+ default:
70
+ return <Server className="w-3.5 h-3.5 text-zinc-400" />;
71
+ }
72
+ };
73
+
74
+ return (
75
+ <div className="p-6 max-w-7xl mx-auto space-y-6">
76
+ {/* Header */}
77
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
78
+ <div>
79
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5">
80
+ <Mail className="w-6 h-6 text-blue-400" />
81
+ Sending Inboxes & Mailboxes
82
+ </h1>
83
+ <p className="text-zinc-400 text-sm mt-1">
84
+ Connect and manage sending infrastructure across Google Workspace, Microsoft 365, and custom SMTP/IMAP.
85
+ </p>
86
+ </div>
87
+
88
+ <button
89
+ onClick={() => setIsAddOpen(true)}
90
+ 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"
91
+ >
92
+ <Plus className="w-4 h-4" /> Add Mailbox
93
+ </button>
94
+ </div>
95
+
96
+ {/* Overview Cards */}
97
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
98
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-4">
99
+ <span className="text-xs text-zinc-400">Total Sending Capacity</span>
100
+ <div className="text-2xl font-bold text-zinc-100 mt-1">
101
+ {accounts.reduce((sum: number, a: any) => sum + (a.daily_limit || 0), 0)} / day
102
+ </div>
103
+ <span className="text-[11px] text-zinc-500">{accounts.length} connected mailboxes</span>
104
+ </div>
105
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-4">
106
+ <span className="text-xs text-zinc-400">Active in Warmup</span>
107
+ <div className="text-2xl font-bold text-amber-400 mt-1">
108
+ {accounts.filter((a: any) => a.warmup_enabled).length} of {accounts.length}
109
+ </div>
110
+ <span className="text-[11px] text-zinc-500">Autonomous ramp-up active</span>
111
+ </div>
112
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-4">
113
+ <span className="text-xs text-zinc-400">Network Deliverability</span>
114
+ <div className="text-2xl font-bold text-emerald-400 mt-1">
115
+ {accounts.length ? `${(accounts.reduce((sum: number, account: any) => sum + (account.warmup_score || 0), 0) / accounts.length).toFixed(1)}%` : "—"}
116
+ </div>
117
+ <span className="text-[11px] text-zinc-500">All DNS / SPF / DKIM valid</span>
118
+ </div>
119
+ </div>
120
+
121
+ {/* Accounts Table */}
122
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden shadow-sm">
123
+ <div className="overflow-x-auto">
124
+ <table className="w-full text-sm text-left">
125
+ <thead className="bg-zinc-950 border-b border-zinc-800 text-zinc-400 text-xs uppercase">
126
+ <tr>
127
+ <th className="px-6 py-3.5 font-semibold">Account / Sender</th>
128
+ <th className="px-6 py-3.5 font-semibold">Provider</th>
129
+ <th className="px-6 py-3.5 font-semibold">Status</th>
130
+ <th className="px-6 py-3.5 font-semibold">Daily Limit & Usage</th>
131
+ <th className="px-6 py-3.5 font-semibold">Warmup</th>
132
+ <th className="px-6 py-3.5 font-semibold text-right">Actions</th>
133
+ </tr>
134
+ </thead>
135
+ <tbody className="divide-y divide-zinc-800/80">
136
+ {accounts.map((acc: any) => {
137
+ const usage = acc.sent_today || 0;
138
+ const limit = acc.daily_limit || 0;
139
+ const percentage = limit ? Math.min(100, Math.round((usage / limit) * 100)) : 0;
140
+
141
+ return (
142
+ <tr key={acc.id} className="hover:bg-zinc-800/30 transition-colors">
143
+ <td className="px-6 py-4">
144
+ <div className="font-semibold text-zinc-100">{acc.from_name}</div>
145
+ <div className="text-xs text-zinc-400 font-mono mt-0.5">{acc.from_email}</div>
146
+ </td>
147
+ <td className="px-6 py-4">
148
+ <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">
149
+ {getProviderIcon(acc.provider)}
150
+ {acc.provider}
151
+ </span>
152
+ </td>
153
+ <td className="px-6 py-4">
154
+ <StatusBadge status={acc.status} size="sm" />
155
+ </td>
156
+ <td className="px-6 py-4">
157
+ <div className="w-40 space-y-1">
158
+ <div className="flex justify-between text-[11px] font-mono text-zinc-400">
159
+ <span>{usage} sent</span>
160
+ <span>{limit} limit</span>
161
+ </div>
162
+ <div className="w-full h-1.5 bg-zinc-950 rounded-full overflow-hidden border border-zinc-800">
163
+ <div
164
+ className={`h-full rounded-full ${
165
+ percentage > 90 ? "bg-amber-500" : "bg-indigo-500"
166
+ }`}
167
+ style={{ width: `${percentage}%` }}
168
+ />
169
+ </div>
170
+ </div>
171
+ </td>
172
+ <td className="px-6 py-4">
173
+ <div className="flex items-center gap-2">
174
+ <Flame className={`w-4 h-4 ${acc.warmup_enabled ? "text-amber-400 animate-pulse" : "text-zinc-600"}`} />
175
+ <span className="text-xs font-medium text-zinc-300">
176
+ {acc.warmup_enabled ? "Ramping Up" : "Disabled"}
177
+ </span>
178
+ </div>
179
+ </td>
180
+ <td className="px-6 py-4 text-right">
181
+ <div className="flex items-center justify-end gap-2">
182
+ <button
183
+ onClick={() => reconnectMutation.mutate(acc.id)}
184
+ title="Verify Connection"
185
+ className="p-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-md transition-colors"
186
+ >
187
+ <RefreshCw className="w-3.5 h-3.5" />
188
+ </button>
189
+ <button
190
+ onClick={() => setSelectedForDelete(acc.id)}
191
+ title="Disconnect Mailbox"
192
+ className="p-1.5 bg-zinc-800 hover:bg-rose-500/20 text-zinc-400 hover:text-rose-400 rounded-md transition-colors"
193
+ >
194
+ <Trash2 className="w-3.5 h-3.5" />
195
+ </button>
196
+ </div>
197
+ </td>
198
+ </tr>
199
+ );
200
+ })}
201
+ </tbody>
202
+ </table>
203
+ </div>
204
+ </div>
205
+
206
+ {/* Delete Confirmation */}
207
+ <ConfirmModal
208
+ isOpen={Boolean(selectedForDelete)}
209
+ onClose={() => setSelectedForDelete(null)}
210
+ onConfirm={() => selectedForDelete && deleteMutation.mutate(selectedForDelete)}
211
+ title="Disconnect Mailbox"
212
+ description="Are you sure you want to disconnect this email account? Active campaigns using this mailbox will cease dispatching from it."
213
+ confirmText="Disconnect"
214
+ />
215
+
216
+ {/* Add Mailbox Drawer */}
217
+ <FormDrawer
218
+ isOpen={isAddOpen}
219
+ onClose={() => setIsAddOpen(false)}
220
+ title="Connect Sending Mailbox"
221
+ subtitle="Add Google Workspace, Microsoft 365, or Custom SMTP/IMAP credentials."
222
+ >
223
+ <div className="space-y-5 text-xs">
224
+ {/* Provider Selector Tabs */}
225
+ <div className="grid grid-cols-3 gap-2 p-1 bg-zinc-900 border border-zinc-800 rounded-xl">
226
+ <button
227
+ type="button"
228
+ onClick={() => setActiveSetupTab("smtp")}
229
+ className={`py-2 rounded-lg font-medium transition-colors ${
230
+ activeSetupTab === "smtp" ? "bg-indigo-600 text-white" : "text-zinc-400 hover:text-zinc-200"
231
+ }`}
232
+ >
233
+ SMTP / IMAP
234
+ </button>
235
+ <button
236
+ type="button"
237
+ onClick={() => setActiveSetupTab("google")}
238
+ className={`py-2 rounded-lg font-medium transition-colors ${
239
+ activeSetupTab === "google" ? "bg-indigo-600 text-white" : "text-zinc-400 hover:text-zinc-200"
240
+ }`}
241
+ >
242
+ Google OAuth
243
+ </button>
244
+ <button
245
+ type="button"
246
+ onClick={() => setActiveSetupTab("outlook")}
247
+ className={`py-2 rounded-lg font-medium transition-colors ${
248
+ activeSetupTab === "outlook" ? "bg-indigo-600 text-white" : "text-zinc-400 hover:text-zinc-200"
249
+ }`}
250
+ >
251
+ Microsoft 365
252
+ </button>
253
+ </div>
254
+
255
+ {activeSetupTab === "smtp" && (
256
+ <form
257
+ onSubmit={async (e) => {
258
+ e.preventDefault();
259
+ const form = e.target as HTMLFormElement;
260
+ const data = {
261
+ from_name: (form.elements.namedItem("from_name") as HTMLInputElement).value,
262
+ from_email: (form.elements.namedItem("from_email") as HTMLInputElement).value,
263
+ smtp_host: (form.elements.namedItem("smtp_host") as HTMLInputElement).value,
264
+ smtp_port: Number((form.elements.namedItem("smtp_port") as HTMLInputElement).value),
265
+ smtp_username: (form.elements.namedItem("smtp_username") as HTMLInputElement).value,
266
+ smtp_password: (form.elements.namedItem("smtp_password") as HTMLInputElement).value,
267
+ imap_host: (form.elements.namedItem("imap_host") as HTMLInputElement).value,
268
+ imap_port: Number((form.elements.namedItem("imap_port") as HTMLInputElement).value),
269
+ imap_username: (form.elements.namedItem("imap_username") as HTMLInputElement).value,
270
+ imap_password: (form.elements.namedItem("imap_password") as HTMLInputElement).value,
271
+ daily_limit: Number((form.elements.namedItem("daily_limit") as HTMLInputElement).value),
272
+ provider: "smtp",
273
+ };
274
+
275
+ try {
276
+ await apiClient.post("/email-accounts/smtp", data);
277
+ toast.success("SMTP Mailbox connected successfully");
278
+ setIsAddOpen(false);
279
+ refetch();
280
+ } catch (err: any) {
281
+ toast.error(err.response?.data?.error || "Failed to connect SMTP account");
282
+ }
283
+ }}
284
+ className="space-y-4"
285
+ >
286
+ <div className="grid grid-cols-2 gap-3">
287
+ <div>
288
+ <label className="block font-medium text-zinc-300 mb-1">Sender Name *</label>
289
+ <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" />
290
+ </div>
291
+ <div>
292
+ <label className="block font-medium text-zinc-300 mb-1">Email Address *</label>
293
+ <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" />
294
+ </div>
295
+ </div>
296
+
297
+ <div className="pt-2 border-t border-zinc-800 space-y-3">
298
+ <h4 className="font-semibold text-zinc-400 uppercase tracking-wider text-[10px]">SMTP Configuration (Outbound)</h4>
299
+ <div className="grid grid-cols-3 gap-2">
300
+ <div className="col-span-2">
301
+ <label className="block font-medium text-zinc-400 mb-1">Host</label>
302
+ <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" />
303
+ </div>
304
+ <div>
305
+ <label className="block font-medium text-zinc-400 mb-1">Port</label>
306
+ <input name="smtp_port" defaultValue="587" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
307
+ </div>
308
+ </div>
309
+ <div className="grid grid-cols-2 gap-2">
310
+ <div>
311
+ <label className="block font-medium text-zinc-400 mb-1">Username</label>
312
+ <input name="smtp_username" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
313
+ </div>
314
+ <div>
315
+ <label className="block font-medium text-zinc-400 mb-1">Password</label>
316
+ <input name="smtp_password" type="password" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
317
+ </div>
318
+ </div>
319
+ </div>
320
+
321
+ <div className="pt-2 border-t border-zinc-800 space-y-3">
322
+ <h4 className="font-semibold text-zinc-400 uppercase tracking-wider text-[10px]">IMAP Configuration (Inbox & Replies)</h4>
323
+ <div className="grid grid-cols-3 gap-2">
324
+ <div className="col-span-2">
325
+ <label className="block font-medium text-zinc-400 mb-1">Host</label>
326
+ <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" />
327
+ </div>
328
+ <div>
329
+ <label className="block font-medium text-zinc-400 mb-1">Port</label>
330
+ <input name="imap_port" defaultValue="993" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
331
+ </div>
332
+ </div>
333
+ <div className="grid grid-cols-2 gap-2">
334
+ <div>
335
+ <label className="block font-medium text-zinc-400 mb-1">Username</label>
336
+ <input name="imap_username" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
337
+ </div>
338
+ <div>
339
+ <label className="block font-medium text-zinc-400 mb-1">Password</label>
340
+ <input name="imap_password" type="password" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
341
+ </div>
342
+ </div>
343
+ </div>
344
+
345
+ <div>
346
+ <label className="block font-medium text-zinc-300 mb-1">Max Daily Send Limit</label>
347
+ <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" />
348
+ </div>
349
+
350
+ <div className="pt-4 flex justify-end gap-2 border-t border-zinc-800">
351
+ <button
352
+ type="button"
353
+ onClick={() => setIsAddOpen(false)}
354
+ className="px-3 py-2 bg-zinc-800 text-zinc-300 rounded-lg"
355
+ >
356
+ Cancel
357
+ </button>
358
+ <button
359
+ type="submit"
360
+ className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-medium rounded-lg"
361
+ >
362
+ Connect Mailbox
363
+ </button>
364
+ </div>
365
+ </form>
366
+ )}
367
+
368
+ {(activeSetupTab === "google" || activeSetupTab === "outlook") && (
369
+ <div className="py-8 text-center space-y-4">
370
+ <div className="p-4 bg-zinc-900 rounded-xl border border-zinc-800 max-w-sm mx-auto">
371
+ <div className="text-2xl mb-2 font-bold">{activeSetupTab === "google" ? "Google Workspace" : "Microsoft 365"}</div>
372
+ <p className="text-zinc-400 text-xs">
373
+ Authorize direct OAuth access to dispatch outbound sequences with verified sender tokens.
374
+ </p>
375
+ <button
376
+ type="button"
377
+ onClick={() => {
378
+ toast.success(`Connected ${activeSetupTab} account`);
379
+ setIsAddOpen(false);
380
+ refetch();
381
+ }}
382
+ className="mt-4 w-full py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg shadow-md"
383
+ >
384
+ Authorize with {activeSetupTab === "google" ? "Google" : "Microsoft"}
385
+ </button>
386
+ </div>
387
+ </div>
388
+ )}
389
+ </div>
390
+ </FormDrawer>
391
+ </div>
392
+ );
393
+ }