sitcod3 commited on
Commit
88194ab
·
verified ·
1 Parent(s): afa093e

Upload src\pages\Leads.tsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. src//pages//Leads.tsx +622 -0
src//pages//Leads.tsx ADDED
@@ -0,0 +1,622 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { apiClient } from "../lib/apiClient";
3
+ import {
4
+ Users,
5
+ Search,
6
+ Plus,
7
+ ShieldAlert,
8
+ Trash2,
9
+ Mail,
10
+ Building,
11
+ Globe,
12
+ MapPin,
13
+ Linkedin,
14
+ Phone,
15
+ Filter,
16
+ Clock,
17
+ CheckCircle2,
18
+ Ban,
19
+ ArrowUpRight
20
+ } from "lucide-react";
21
+ import { useState } from "react";
22
+ import StatusBadge from "../components/StatusBadge";
23
+ import FormDrawer from "../components/FormDrawer";
24
+ import CsvExportButton from "../components/CsvExportButton";
25
+ import ConfirmModal from "../components/ConfirmModal";
26
+ import toast from "react-hot-toast";
27
+
28
+ const STATUSES = [
29
+ "ALL",
30
+ "INTERESTED",
31
+ "MEETING_BOOKED",
32
+ "MEETING_DONE",
33
+ "REPLIED",
34
+ "NOT_INTERESTED",
35
+ "UNSUBSCRIBED",
36
+ "BOUNCED",
37
+ "DO_NOT_CONTACT"
38
+ ];
39
+
40
+ export default function Leads() {
41
+ const queryClient = useQueryClient();
42
+ const [activeTab, setActiveTab] = useState<"leads" | "blocklist">("leads");
43
+ const [searchQuery, setSearchQuery] = useState("");
44
+ const [selectedStatus, setSelectedStatus] = useState("ALL");
45
+ const [selectedLead, setSelectedLead] = useState<any | null>(null);
46
+ const [isAddLeadOpen, setIsAddLeadOpen] = useState(false);
47
+ const [isAddBlocklistOpen, setIsAddBlocklistOpen] = useState(false);
48
+ const [blocklistType, setBlocklistType] = useState<"email" | "domain">("email");
49
+
50
+ const { data: leads = [], isLoading } = useQuery({
51
+ queryKey: ["leads-search", searchQuery, selectedStatus],
52
+ queryFn: async () => {
53
+ const res = await apiClient.get("/leads/search", {
54
+ params: { q: searchQuery, status: selectedStatus },
55
+ });
56
+ return res.data;
57
+ },
58
+ });
59
+
60
+ const { data: emailBlockList = [], refetch: refetchEmailBlockList } = useQuery({
61
+ queryKey: ["blocklist-emails"],
62
+ queryFn: async () => {
63
+ const res = await apiClient.get("/leads/block-list");
64
+ return res.data;
65
+ },
66
+ });
67
+
68
+ const { data: domainBlockList = [], refetch: refetchDomainBlockList } = useQuery({
69
+ queryKey: ["blocklist-domains"],
70
+ queryFn: async () => {
71
+ const res = await apiClient.get("/leads/domain-block-list");
72
+ return res.data;
73
+ },
74
+ });
75
+
76
+ const updateLeadStatusMutation = useMutation({
77
+ mutationFn: async ({ campaignId, leadId, status }: { campaignId: string; leadId: string; status: string }) => {
78
+ const res = await apiClient.post(`/campaigns/${campaignId}/leads/status`, { lead_id: leadId, status });
79
+ return res.data;
80
+ },
81
+ onSuccess: () => {
82
+ toast.success("Lead status updated");
83
+ queryClient.invalidateQueries({ queryKey: ["leads-search"] });
84
+ },
85
+ });
86
+
87
+ const deleteLeadMutation = useMutation({
88
+ mutationFn: async ({ campaignId, leadId }: { campaignId: string; leadId: string }) => {
89
+ const res = await apiClient.delete(`/campaigns/${campaignId}/leads/${leadId}`);
90
+ return res.data;
91
+ },
92
+ onSuccess: () => {
93
+ toast.success("Lead removed");
94
+ setSelectedLead(null);
95
+ queryClient.invalidateQueries({ queryKey: ["leads-search"] });
96
+ },
97
+ });
98
+
99
+ return (
100
+ <div className="p-6 max-w-7xl mx-auto space-y-6">
101
+ {/* Header */}
102
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
103
+ <div>
104
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5">
105
+ <Users className="w-6 h-6 text-teal-400" />
106
+ Lead Intelligence Database
107
+ </h1>
108
+ <p className="text-zinc-400 text-sm mt-1">
109
+ Global repository of targeted prospects, responses, and suppression blocklists.
110
+ </p>
111
+ </div>
112
+
113
+ <div className="flex items-center gap-3">
114
+ <button
115
+ onClick={() => setIsAddLeadOpen(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" /> Add Prospect
119
+ </button>
120
+ </div>
121
+ </div>
122
+
123
+ {/* Main Tabs */}
124
+ <div className="border-b border-zinc-800 flex items-center gap-2 text-sm font-medium">
125
+ <button
126
+ onClick={() => setActiveTab("leads")}
127
+ className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${
128
+ activeTab === "leads"
129
+ ? "border-indigo-500 text-indigo-400 bg-indigo-500/5"
130
+ : "border-transparent text-zinc-400 hover:text-zinc-200"
131
+ }`}
132
+ >
133
+ <Users className="w-4 h-4" /> Global Leads ({leads.length})
134
+ </button>
135
+ <button
136
+ onClick={() => setActiveTab("blocklist")}
137
+ className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${
138
+ activeTab === "blocklist"
139
+ ? "border-indigo-500 text-indigo-400 bg-indigo-500/5"
140
+ : "border-transparent text-zinc-400 hover:text-zinc-200"
141
+ }`}
142
+ >
143
+ <ShieldAlert className="w-4 h-4" /> Suppression Blocklists
144
+ </button>
145
+ </div>
146
+
147
+ {activeTab === "leads" && (
148
+ <div className="space-y-4">
149
+ {/* Filter Bar */}
150
+ <div className="bg-white/5 border border-white/10 backdrop-blur-md rounded-xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-3 shadow-[0_8px_32px_-8px_rgba(6,10,12,0.65)]">
151
+ <div className="relative flex-1 max-w-md">
152
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-400" />
153
+ <input
154
+ type="text"
155
+ placeholder="Search by name, email, or company..."
156
+ value={searchQuery}
157
+ onChange={(e) => setSearchQuery(e.target.value)}
158
+ className="w-full bg-black/40 border border-white/5 rounded-lg py-2 pl-9 pr-4 text-xs text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:border-indigo-500 shadow-[inset_0_1px_1px_rgba(246,248,242,0.02)] transition-all"
159
+ />
160
+ </div>
161
+
162
+ <div className="flex items-center gap-3">
163
+ <select
164
+ value={selectedStatus}
165
+ onChange={(e) => setSelectedStatus(e.target.value)}
166
+ className="bg-black/40 border border-white/5 rounded-lg py-2 px-3 text-xs text-zinc-300 focus:outline-none focus:border-indigo-500 shadow-[inset_0_1px_1px_rgba(246,248,242,0.02)] transition-all"
167
+ >
168
+ {STATUSES.map((st) => (
169
+ <option key={st} value={st} className="bg-zinc-900">
170
+ Status: {st.replace(/_/g, " ")}
171
+ </option>
172
+ ))}
173
+ </select>
174
+ <CsvExportButton data={leads} filename="smartlead-global-leads.csv" />
175
+ </div>
176
+ </div>
177
+
178
+ {/* Leads Table */}
179
+ <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)]">
180
+ <div className="overflow-x-auto">
181
+ <table className="w-full text-sm text-left">
182
+ <thead className="bg-black/20 border-b border-white/5 text-zinc-400 text-xs uppercase font-sans tracking-wider">
183
+ <tr>
184
+ <th className="px-6 py-3.5 font-semibold">Prospect</th>
185
+ <th className="px-6 py-3.5 font-semibold">Company</th>
186
+ <th className="px-6 py-3.5 font-semibold">Status</th>
187
+ <th className="px-6 py-3.5 font-semibold">Engagement</th>
188
+ <th className="px-6 py-3.5 font-semibold">Location</th>
189
+ <th className="px-6 py-3.5 font-semibold text-right">Actions</th>
190
+ </tr>
191
+ </thead>
192
+ <tbody className="divide-y divide-white/5">
193
+ {leads.map((lead: any) => (
194
+ <tr
195
+ key={lead.id}
196
+ onClick={() => setSelectedLead(lead)}
197
+ className="hover:bg-white/5 transition-colors cursor-pointer"
198
+ >
199
+ <td className="px-6 py-4">
200
+ <div className="font-semibold text-zinc-100 flex items-center gap-2">
201
+ {lead.first_name} {lead.last_name}
202
+ </div>
203
+ <div className="text-xs text-zinc-400 font-mono flex items-center gap-1 mt-0.5">
204
+ <Mail className="w-3 h-3 text-zinc-500" /> {lead.email}
205
+ </div>
206
+ </td>
207
+ <td className="px-6 py-4 text-zinc-300">
208
+ <div className="font-medium text-zinc-200">{lead.company_name}</div>
209
+ {lead.website && (
210
+ <div className="text-[11px] text-zinc-500 font-mono truncate max-w-[150px]">
211
+ {lead.website.replace("https://", "")}
212
+ </div>
213
+ )}
214
+ </td>
215
+ <td className="px-6 py-4">
216
+ <StatusBadge status={lead.status} size="sm" />
217
+ </td>
218
+ <td className="px-6 py-4">
219
+ <div className="text-xs text-zinc-300 font-mono">
220
+ <span className="text-emerald-400 drop-shadow-[0_0_8px_rgba(63,156,132,0.3)]">{lead.open_count || 0}</span> opens •{" "}
221
+ <span className="text-indigo-400 drop-shadow-[0_0_8px_rgba(37,125,104,0.3)]">{lead.click_count || 0}</span> clicks
222
+ </div>
223
+ </td>
224
+ <td className="px-6 py-4 text-xs text-zinc-400 font-sans">
225
+ {lead.location || "United States"}
226
+ </td>
227
+ <td className="px-6 py-4 text-right">
228
+ <button
229
+ onClick={(e) => {
230
+ e.stopPropagation();
231
+ setSelectedLead(lead);
232
+ }}
233
+ className="px-3 py-1.5 bg-white/5 hover:bg-white/10 text-zinc-300 text-xs font-medium rounded-lg border border-white/5 transition-all shadow-[inset_0_1px_1px_rgba(246,248,242,0.05)]"
234
+ >
235
+ View Profile
236
+ </button>
237
+ </td>
238
+ </tr>
239
+ ))}
240
+
241
+ {leads.length === 0 && !isLoading && (
242
+ <tr>
243
+ <td colSpan={6} className="py-16 text-center text-xs text-zinc-500">
244
+ No prospect leads matching current filters.
245
+ </td>
246
+ </tr>
247
+ )}
248
+ </tbody>
249
+ </table>
250
+ </div>
251
+ </div>
252
+ </div>
253
+ )}
254
+
255
+ {/* SUB-TAB: BLOCKLIST MANAGER */}
256
+ {activeTab === "blocklist" && (
257
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
258
+ {/* Email Blocklist */}
259
+ <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)] space-y-4">
260
+ <div className="flex items-center justify-between">
261
+ <div>
262
+ <h3 className="text-sm font-semibold text-zinc-100 flex items-center gap-2">
263
+ <Mail className="w-4 h-4 text-rose-400 drop-shadow-[0_0_8px_rgba(203,117,80,0.5)]" />
264
+ Email Address Blocklist
265
+ </h3>
266
+ <p className="text-xs text-zinc-400 mt-0.5">Suppressed emails never receive outreach.</p>
267
+ </div>
268
+ <button
269
+ onClick={() => {
270
+ setBlocklistType("email");
271
+ setIsAddBlocklistOpen(true);
272
+ }}
273
+ className="px-2.5 py-1.5 bg-white/5 hover:bg-white/10 text-zinc-200 text-xs font-medium rounded-md border border-white/5 transition-all shadow-[inset_0_1px_1px_rgba(246,248,242,0.05)]"
274
+ >
275
+ + Add Email
276
+ </button>
277
+ </div>
278
+
279
+ <div className="space-y-2">
280
+ {emailBlockList.map((item: any) => (
281
+ <div
282
+ key={item.id}
283
+ className="flex items-center justify-between p-3 bg-black/40 border border-white/5 rounded-lg text-xs hover:bg-white/5 transition-colors"
284
+ >
285
+ <div>
286
+ <span className="font-mono text-zinc-200 font-medium">{item.email}</span>
287
+ <div className="text-[10px] text-zinc-500 font-sans mt-0.5">{item.reason}</div>
288
+ </div>
289
+ <button
290
+ onClick={async () => {
291
+ await apiClient.delete(`/leads/block-list/${item.id}`);
292
+ toast.success("Removed from blocklist");
293
+ refetchEmailBlockList();
294
+ }}
295
+ className="text-zinc-500 hover:text-rose-400 p-1.5 rounded-md hover:bg-rose-500/10 transition-colors"
296
+ >
297
+ <Trash2 className="w-3.5 h-3.5" />
298
+ </button>
299
+ </div>
300
+ ))}
301
+ </div>
302
+ </div>
303
+
304
+ {/* Domain Blocklist */}
305
+ <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)] space-y-4">
306
+ <div className="flex items-center justify-between">
307
+ <div>
308
+ <h3 className="text-sm font-semibold text-zinc-100 flex items-center gap-2">
309
+ <Globe className="w-4 h-4 text-amber-400 drop-shadow-[0_0_8px_rgba(194,160,74,0.5)]" />
310
+ Domain Suppression List
311
+ </h3>
312
+ <p className="text-xs text-zinc-400 mt-0.5">Blocks all emails under specified root domains.</p>
313
+ </div>
314
+ <button
315
+ onClick={() => {
316
+ setBlocklistType("domain");
317
+ setIsAddBlocklistOpen(true);
318
+ }}
319
+ className="px-2.5 py-1.5 bg-white/5 hover:bg-white/10 text-zinc-200 text-xs font-medium rounded-md border border-white/5 transition-all shadow-[inset_0_1px_1px_rgba(246,248,242,0.05)]"
320
+ >
321
+ + Add Domain
322
+ </button>
323
+ </div>
324
+
325
+ <div className="space-y-2">
326
+ {domainBlockList.map((item: any) => (
327
+ <div
328
+ key={item.id}
329
+ className="flex items-center justify-between p-3 bg-black/40 border border-white/5 rounded-lg text-xs hover:bg-white/5 transition-colors"
330
+ >
331
+ <div>
332
+ <span className="font-mono text-zinc-200 font-medium">@{item.domain}</span>
333
+ <div className="text-[10px] text-zinc-500 font-sans mt-0.5">{item.reason}</div>
334
+ </div>
335
+ <button
336
+ onClick={async () => {
337
+ await apiClient.delete(`/leads/domain-block-list/${item.id}`);
338
+ toast.success("Domain removed from blocklist");
339
+ refetchDomainBlockList();
340
+ }}
341
+ className="text-zinc-500 hover:text-rose-400 p-1.5 rounded-md hover:bg-rose-500/10 transition-colors"
342
+ >
343
+ <Trash2 className="w-3.5 h-3.5" />
344
+ </button>
345
+ </div>
346
+ ))}
347
+ </div>
348
+ </div>
349
+ </div>
350
+ )}
351
+
352
+ {/* Lead Detail Slide-Over Drawer */}
353
+ <FormDrawer
354
+ isOpen={Boolean(selectedLead)}
355
+ onClose={() => setSelectedLead(null)}
356
+ title="Prospect Dossier"
357
+ subtitle={selectedLead ? `${selectedLead.first_name} ${selectedLead.last_name} (${selectedLead.email})` : ""}
358
+ >
359
+ {selectedLead && (
360
+ <div className="space-y-6 text-xs">
361
+ {/* Status Change Card */}
362
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-4">
363
+ <label className="block font-medium text-zinc-400 mb-2">Lead Outreach Status</label>
364
+ <select
365
+ value={selectedLead.status}
366
+ onChange={(e) => {
367
+ updateLeadStatusMutation.mutate({
368
+ campaignId: selectedLead.campaign_id,
369
+ leadId: selectedLead.id,
370
+ status: e.target.value,
371
+ });
372
+ setSelectedLead({ ...selectedLead, status: e.target.value });
373
+ }}
374
+ className="w-full bg-zinc-950 border border-zinc-800 rounded-lg p-2 text-zinc-100 font-medium"
375
+ >
376
+ {STATUSES.filter((s) => s !== "ALL").map((st) => (
377
+ <option key={st} value={st}>
378
+ {st.replace(/_/g, " ")}
379
+ </option>
380
+ ))}
381
+ </select>
382
+ </div>
383
+
384
+ {/* Profile Fields */}
385
+ <div className="space-y-3 bg-zinc-900/60 border border-zinc-800 rounded-xl p-4">
386
+ <h4 className="font-semibold text-zinc-300 uppercase tracking-wider text-[10px]">Contact Info</h4>
387
+ <div className="grid grid-cols-2 gap-3">
388
+ <div>
389
+ <span className="text-zinc-500 block">Company</span>
390
+ <span className="text-zinc-200 font-medium">{selectedLead.company_name}</span>
391
+ </div>
392
+ <div>
393
+ <span className="text-zinc-500 block">Location</span>
394
+ <span className="text-zinc-200 font-medium">{selectedLead.location || "N/A"}</span>
395
+ </div>
396
+ <div>
397
+ <span className="text-zinc-500 block">Phone</span>
398
+ <span className="text-zinc-200 font-medium">{selectedLead.phone_number || "N/A"}</span>
399
+ </div>
400
+ <div>
401
+ <span className="text-zinc-500 block">LinkedIn</span>
402
+ {selectedLead.linkedin_profile ? (
403
+ <a
404
+ href={selectedLead.linkedin_profile}
405
+ target="_blank"
406
+ rel="noreferrer"
407
+ className="text-indigo-400 hover:underline"
408
+ >
409
+ View LinkedIn Profile
410
+ </a>
411
+ ) : (
412
+ <span className="text-zinc-500">Not provided</span>
413
+ )}
414
+ </div>
415
+ </div>
416
+ </div>
417
+
418
+ {/* Custom Fields */}
419
+ {selectedLead.custom_fields && Object.keys(selectedLead.custom_fields).length > 0 && (
420
+ <div className="space-y-2 bg-zinc-900/60 border border-zinc-800 rounded-xl p-4">
421
+ <h4 className="font-semibold text-zinc-300 uppercase tracking-wider text-[10px]">Custom Attributes</h4>
422
+ <div className="grid grid-cols-2 gap-2 font-mono text-[11px]">
423
+ {Object.entries(selectedLead.custom_fields).map(([key, val]) => (
424
+ <div key={key} className="bg-zinc-950 p-2 rounded border border-zinc-800">
425
+ <span className="text-zinc-500 block text-[10px]">{key}</span>
426
+ <span className="text-zinc-200 font-semibold">{String(val)}</span>
427
+ </div>
428
+ ))}
429
+ </div>
430
+ </div>
431
+ )}
432
+
433
+ {/* Timeline */}
434
+ <div className="space-y-3 bg-zinc-900/60 border border-zinc-800 rounded-xl p-4">
435
+ <h4 className="font-semibold text-zinc-300 uppercase tracking-wider text-[10px]">Activity Timeline</h4>
436
+ <div className="space-y-3">
437
+ <div className="flex items-start gap-2.5">
438
+ <div className="w-2 h-2 rounded-full bg-emerald-400 mt-1" />
439
+ <div>
440
+ <span className="text-zinc-300 font-medium">Email Delivered & Opened</span>
441
+ <div className="text-[10px] text-zinc-500">2 hours ago</div>
442
+ </div>
443
+ </div>
444
+ <div className="flex items-start gap-2.5">
445
+ <div className="w-2 h-2 rounded-full bg-indigo-400 mt-1" />
446
+ <div>
447
+ <span className="text-zinc-300 font-medium">Enrolled in Enterprise SaaS Sequence</span>
448
+ <div className="text-[10px] text-zinc-500">3 days ago</div>
449
+ </div>
450
+ </div>
451
+ </div>
452
+ </div>
453
+
454
+ {/* Delete Lead Button */}
455
+ <div className="pt-2 flex justify-end">
456
+ <button
457
+ onClick={() => {
458
+ deleteLeadMutation.mutate({
459
+ campaignId: selectedLead.campaign_id,
460
+ leadId: selectedLead.id,
461
+ });
462
+ }}
463
+ className="px-3 py-1.5 bg-rose-600/20 hover:bg-rose-600 text-rose-300 hover:text-white rounded-lg text-xs font-medium transition-colors flex items-center gap-1.5"
464
+ >
465
+ <Trash2 className="w-3.5 h-3.5" /> Remove Lead
466
+ </button>
467
+ </div>
468
+ </div>
469
+ )}
470
+ </FormDrawer>
471
+
472
+ {/* Add Lead Slide-Over */}
473
+ <FormDrawer
474
+ isOpen={isAddLeadOpen}
475
+ onClose={() => setIsAddLeadOpen(false)}
476
+ title="Add Global Prospect Lead"
477
+ subtitle="Input contact info, company details, and custom variables for personalization."
478
+ >
479
+ <form
480
+ onSubmit={async (e) => {
481
+ e.preventDefault();
482
+ const form = e.target as HTMLFormElement;
483
+ const newLead = {
484
+ first_name: (form.elements.namedItem("first_name") as HTMLInputElement).value,
485
+ last_name: (form.elements.namedItem("last_name") as HTMLInputElement).value,
486
+ email: (form.elements.namedItem("email") as HTMLInputElement).value,
487
+ phone_number: (form.elements.namedItem("phone_number") as HTMLInputElement).value,
488
+ company_name: (form.elements.namedItem("company_name") as HTMLInputElement).value,
489
+ website: (form.elements.namedItem("website") as HTMLInputElement).value,
490
+ location: (form.elements.namedItem("location") as HTMLInputElement).value,
491
+ linkedin_profile: (form.elements.namedItem("linkedin_profile") as HTMLInputElement).value,
492
+ status: "INTERESTED",
493
+ };
494
+
495
+ try {
496
+ await apiClient.post("/campaigns/camp_9901/leads", newLead);
497
+ toast.success("Prospect added to database");
498
+ setIsAddLeadOpen(false);
499
+ queryClient.invalidateQueries({ queryKey: ["leads-search"] });
500
+ } catch (err: any) {
501
+ toast.error("Failed to add prospect");
502
+ }
503
+ }}
504
+ className="space-y-3.5 text-xs"
505
+ >
506
+ <div className="grid grid-cols-2 gap-3">
507
+ <div>
508
+ <label className="block font-medium text-zinc-300 mb-1">First Name *</label>
509
+ <input name="first_name" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
510
+ </div>
511
+ <div>
512
+ <label className="block font-medium text-zinc-300 mb-1">Last Name</label>
513
+ <input name="last_name" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
514
+ </div>
515
+ </div>
516
+ <div>
517
+ <label className="block font-medium text-zinc-300 mb-1">Email Address *</label>
518
+ <input type="email" name="email" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
519
+ </div>
520
+ <div className="grid grid-cols-2 gap-3">
521
+ <div>
522
+ <label className="block font-medium text-zinc-300 mb-1">Company Name</label>
523
+ <input name="company_name" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
524
+ </div>
525
+ <div>
526
+ <label className="block font-medium text-zinc-300 mb-1">Phone Number</label>
527
+ <input name="phone_number" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
528
+ </div>
529
+ </div>
530
+ <div>
531
+ <label className="block font-medium text-zinc-300 mb-1">Website URL</label>
532
+ <input name="website" placeholder="https://" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
533
+ </div>
534
+ <div className="grid grid-cols-2 gap-3">
535
+ <div>
536
+ <label className="block font-medium text-zinc-300 mb-1">Location</label>
537
+ <input name="location" placeholder="e.g. San Francisco, CA" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
538
+ </div>
539
+ <div>
540
+ <label className="block font-medium text-zinc-300 mb-1">LinkedIn URL</label>
541
+ <input name="linkedin_profile" placeholder="https://linkedin.com/in/..." className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" />
542
+ </div>
543
+ </div>
544
+
545
+ <div className="pt-4 flex justify-end gap-2">
546
+ <button
547
+ type="button"
548
+ onClick={() => setIsAddLeadOpen(false)}
549
+ className="px-3 py-2 bg-zinc-800 text-zinc-300 rounded-lg"
550
+ >
551
+ Cancel
552
+ </button>
553
+ <button
554
+ type="submit"
555
+ className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium shadow"
556
+ >
557
+ Save Prospect
558
+ </button>
559
+ </div>
560
+ </form>
561
+ </FormDrawer>
562
+
563
+ {/* Add Blocklist Modal */}
564
+ <FormDrawer
565
+ isOpen={isAddBlocklistOpen}
566
+ onClose={() => setIsAddBlocklistOpen(false)}
567
+ title={blocklistType === "email" ? "Add Email to Suppression Blocklist" : "Add Domain to Suppression Blocklist"}
568
+ subtitle="Blocked records will never receive any emails from any campaign."
569
+ >
570
+ <form
571
+ onSubmit={async (e) => {
572
+ e.preventDefault();
573
+ const form = e.target as HTMLFormElement;
574
+ const val = (form.elements.namedItem("val") as HTMLInputElement).value;
575
+ const reason = (form.elements.namedItem("reason") as HTMLInputElement).value;
576
+
577
+ try {
578
+ if (blocklistType === "email") {
579
+ await apiClient.post("/leads/block-list", { email: val, reason });
580
+ refetchEmailBlockList();
581
+ } else {
582
+ await apiClient.post("/leads/domain-block-list", { domain: val, reason });
583
+ refetchDomainBlockList();
584
+ }
585
+ toast.success("Suppression entry saved");
586
+ setIsAddBlocklistOpen(false);
587
+ } catch (err: any) {
588
+ toast.error("Failed to add entry");
589
+ }
590
+ }}
591
+ className="space-y-4 text-xs"
592
+ >
593
+ <div>
594
+ <label className="block font-medium text-zinc-300 mb-1">
595
+ {blocklistType === "email" ? "Email Address" : "Domain (e.g. competitor.com)"}
596
+ </label>
597
+ <input name="val" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
598
+ </div>
599
+ <div>
600
+ <label className="block font-medium text-zinc-300 mb-1">Reason for Suppression</label>
601
+ <input name="reason" placeholder="e.g. Legal opt-out, Competitor" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
602
+ </div>
603
+ <div className="pt-4 flex justify-end gap-2">
604
+ <button
605
+ type="button"
606
+ onClick={() => setIsAddBlocklistOpen(false)}
607
+ className="px-3 py-2 bg-zinc-800 text-zinc-300 rounded-lg"
608
+ >
609
+ Cancel
610
+ </button>
611
+ <button
612
+ type="submit"
613
+ className="px-4 py-2 bg-rose-600 text-white rounded-lg font-medium"
614
+ >
615
+ Block Record
616
+ </button>
617
+ </div>
618
+ </form>
619
+ </FormDrawer>
620
+ </div>
621
+ );
622
+ }