Download src/pages/Leads.tsx from sitcod3/modul-e: direct link, hf CLI and curl.
- Browser
- Download file 28.3 kB
-
https://huggingface.co/spaces/sitcod3/modul-e/resolve/main/src/pages/Leads.tsx
- Command line
-
hf download hf://spaces/sitcod3/modul-e/src/pages/Leads.tsx
-
curl -L -o Leads.tsx https://huggingface.co/spaces/sitcod3/modul-e/resolve/main/src/pages/Leads.tsx
28.3 kB
| import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; | |
| import { apiClient } from "../lib/apiClient"; | |
| import { | |
| Users, | |
| Search, | |
| Plus, | |
| ShieldAlert, | |
| Trash2, | |
| Mail, | |
| Building, | |
| Globe, | |
| MapPin, | |
| Linkedin, | |
| Phone, | |
| Filter, | |
| Clock, | |
| CheckCircle2, | |
| Ban, | |
| ArrowUpRight | |
| } from "lucide-react"; | |
| import { useState } from "react"; | |
| import StatusBadge from "../components/StatusBadge"; | |
| import FormDrawer from "../components/FormDrawer"; | |
| import CsvExportButton from "../components/CsvExportButton"; | |
| import ConfirmModal from "../components/ConfirmModal"; | |
| import toast from "react-hot-toast"; | |
| const STATUSES = [ | |
| "ALL", | |
| "INTERESTED", | |
| "MEETING_BOOKED", | |
| "MEETING_DONE", | |
| "REPLIED", | |
| "NOT_INTERESTED", | |
| "UNSUBSCRIBED", | |
| "BOUNCED", | |
| "DO_NOT_CONTACT" | |
| ]; | |
| export default function Leads() { | |
| const queryClient = useQueryClient(); | |
| const [activeTab, setActiveTab] = useState<"leads" | "blocklist">("leads"); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const [selectedStatus, setSelectedStatus] = useState("ALL"); | |
| const [selectedLead, setSelectedLead] = useState<any | null>(null); | |
| const [isAddLeadOpen, setIsAddLeadOpen] = useState(false); | |
| const [isAddBlocklistOpen, setIsAddBlocklistOpen] = useState(false); | |
| const [blocklistType, setBlocklistType] = useState<"email" | "domain">("email"); | |
| const { data: leads = [], isLoading } = useQuery({ | |
| queryKey: ["leads-search", searchQuery, selectedStatus], | |
| queryFn: async () => { | |
| const res = await apiClient.get("/leads/search", { | |
| params: { q: searchQuery, status: selectedStatus }, | |
| }); | |
| return res.data; | |
| }, | |
| }); | |
| const { data: emailBlockList = [], refetch: refetchEmailBlockList } = useQuery({ | |
| queryKey: ["blocklist-emails"], | |
| queryFn: async () => { | |
| const res = await apiClient.get("/leads/block-list"); | |
| return res.data; | |
| }, | |
| }); | |
| const { data: domainBlockList = [], refetch: refetchDomainBlockList } = useQuery({ | |
| queryKey: ["blocklist-domains"], | |
| queryFn: async () => { | |
| const res = await apiClient.get("/leads/domain-block-list"); | |
| return res.data; | |
| }, | |
| }); | |
| const updateLeadStatusMutation = useMutation({ | |
| mutationFn: async ({ campaignId, leadId, status }: { campaignId: string; leadId: string; status: string }) => { | |
| const res = await apiClient.post(`/campaigns/${campaignId}/leads/status`, { lead_id: leadId, status }); | |
| return res.data; | |
| }, | |
| onSuccess: () => { | |
| toast.success("Lead status updated"); | |
| queryClient.invalidateQueries({ queryKey: ["leads-search"] }); | |
| }, | |
| }); | |
| const deleteLeadMutation = useMutation({ | |
| mutationFn: async ({ campaignId, leadId }: { campaignId: string; leadId: string }) => { | |
| const res = await apiClient.delete(`/campaigns/${campaignId}/leads/${leadId}`); | |
| return res.data; | |
| }, | |
| onSuccess: () => { | |
| toast.success("Lead removed"); | |
| setSelectedLead(null); | |
| queryClient.invalidateQueries({ queryKey: ["leads-search"] }); | |
| }, | |
| }); | |
| 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"> | |
| <Users className="w-6 h-6 text-teal-400" /> | |
| Lead Intelligence Database | |
| </h1> | |
| <p className="text-zinc-400 text-sm mt-1"> | |
| Global repository of targeted prospects, responses, and suppression blocklists. | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <button | |
| onClick={() => setIsAddLeadOpen(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 Prospect | |
| </button> | |
| </div> | |
| </div> | |
| {/* Main Tabs */} | |
| <div className="border-b border-zinc-800 flex items-center gap-2 text-sm font-medium"> | |
| <button | |
| onClick={() => setActiveTab("leads")} | |
| className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${ | |
| activeTab === "leads" | |
| ? "border-indigo-500 text-indigo-400 bg-indigo-500/5" | |
| : "border-transparent text-zinc-400 hover:text-zinc-200" | |
| }`} | |
| > | |
| <Users className="w-4 h-4" /> Global Leads ({leads.length}) | |
| </button> | |
| <button | |
| onClick={() => setActiveTab("blocklist")} | |
| className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${ | |
| activeTab === "blocklist" | |
| ? "border-indigo-500 text-indigo-400 bg-indigo-500/5" | |
| : "border-transparent text-zinc-400 hover:text-zinc-200" | |
| }`} | |
| > | |
| <ShieldAlert className="w-4 h-4" /> Suppression Blocklists | |
| </button> | |
| </div> | |
| {activeTab === "leads" && ( | |
| <div className="space-y-4"> | |
| {/* Filter Bar */} | |
| <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)]"> | |
| <div className="relative flex-1 max-w-md"> | |
| <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-400" /> | |
| <input | |
| type="text" | |
| placeholder="Search by name, email, or company..." | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| 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" | |
| /> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <select | |
| value={selectedStatus} | |
| onChange={(e) => setSelectedStatus(e.target.value)} | |
| 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" | |
| > | |
| {STATUSES.map((st) => ( | |
| <option key={st} value={st} className="bg-zinc-900"> | |
| Status: {st.replace(/_/g, " ")} | |
| </option> | |
| ))} | |
| </select> | |
| <CsvExportButton data={leads} filename="smartlead-global-leads.csv" /> | |
| </div> | |
| </div> | |
| {/* Leads 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="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 font-sans tracking-wider"> | |
| <tr> | |
| <th className="px-6 py-3.5 font-semibold">Prospect</th> | |
| <th className="px-6 py-3.5 font-semibold">Company</th> | |
| <th className="px-6 py-3.5 font-semibold">Status</th> | |
| <th className="px-6 py-3.5 font-semibold">Engagement</th> | |
| <th className="px-6 py-3.5 font-semibold">Location</th> | |
| <th className="px-6 py-3.5 font-semibold text-right">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-white/5"> | |
| {leads.map((lead: any) => ( | |
| <tr | |
| key={lead.id} | |
| onClick={() => setSelectedLead(lead)} | |
| className="hover:bg-white/5 transition-colors cursor-pointer" | |
| > | |
| <td className="px-6 py-4"> | |
| <div className="font-semibold text-zinc-100 flex items-center gap-2"> | |
| {lead.first_name} {lead.last_name} | |
| </div> | |
| <div className="text-xs text-zinc-400 font-mono flex items-center gap-1 mt-0.5"> | |
| <Mail className="w-3 h-3 text-zinc-500" /> {lead.email} | |
| </div> | |
| </td> | |
| <td className="px-6 py-4 text-zinc-300"> | |
| <div className="font-medium text-zinc-200">{lead.company_name}</div> | |
| {lead.website && ( | |
| <div className="text-[11px] text-zinc-500 font-mono truncate max-w-[150px]"> | |
| {lead.website.replace("https://", "")} | |
| </div> | |
| )} | |
| </td> | |
| <td className="px-6 py-4"> | |
| <StatusBadge status={lead.status} size="sm" /> | |
| </td> | |
| <td className="px-6 py-4"> | |
| <div className="text-xs text-zinc-300 font-mono"> | |
| <span className="text-emerald-400 drop-shadow-[0_0_8px_rgba(63,156,132,0.3)]">{lead.open_count || 0}</span> opens •{" "} | |
| <span className="text-indigo-400 drop-shadow-[0_0_8px_rgba(37,125,104,0.3)]">{lead.click_count || 0}</span> clicks | |
| </div> | |
| </td> | |
| <td className="px-6 py-4 text-xs text-zinc-400 font-sans"> | |
| {lead.location || "United States"} | |
| </td> | |
| <td className="px-6 py-4 text-right"> | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| setSelectedLead(lead); | |
| }} | |
| 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)]" | |
| > | |
| View Profile | |
| </button> | |
| </td> | |
| </tr> | |
| ))} | |
| {leads.length === 0 && !isLoading && ( | |
| <tr> | |
| <td colSpan={6} className="py-16 text-center text-xs text-zinc-500"> | |
| No prospect leads matching current filters. | |
| </td> | |
| </tr> | |
| )} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* SUB-TAB: BLOCKLIST MANAGER */} | |
| {activeTab === "blocklist" && ( | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| {/* Email Blocklist */} | |
| <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"> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <h3 className="text-sm font-semibold text-zinc-100 flex items-center gap-2"> | |
| <Mail className="w-4 h-4 text-rose-400 drop-shadow-[0_0_8px_rgba(203,117,80,0.5)]" /> | |
| Email Address Blocklist | |
| </h3> | |
| <p className="text-xs text-zinc-400 mt-0.5">Suppressed emails never receive outreach.</p> | |
| </div> | |
| <button | |
| onClick={() => { | |
| setBlocklistType("email"); | |
| setIsAddBlocklistOpen(true); | |
| }} | |
| 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)]" | |
| > | |
| + Add Email | |
| </button> | |
| </div> | |
| <div className="space-y-2"> | |
| {emailBlockList.map((item: any) => ( | |
| <div | |
| key={item.id} | |
| 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" | |
| > | |
| <div> | |
| <span className="font-mono text-zinc-200 font-medium">{item.email}</span> | |
| <div className="text-[10px] text-zinc-500 font-sans mt-0.5">{item.reason}</div> | |
| </div> | |
| <button | |
| onClick={async () => { | |
| await apiClient.delete(`/leads/block-list/${item.id}`); | |
| toast.success("Removed from blocklist"); | |
| refetchEmailBlockList(); | |
| }} | |
| className="text-zinc-500 hover:text-rose-400 p-1.5 rounded-md hover:bg-rose-500/10 transition-colors" | |
| > | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Domain Blocklist */} | |
| <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"> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <h3 className="text-sm font-semibold text-zinc-100 flex items-center gap-2"> | |
| <Globe className="w-4 h-4 text-amber-400 drop-shadow-[0_0_8px_rgba(194,160,74,0.5)]" /> | |
| Domain Suppression List | |
| </h3> | |
| <p className="text-xs text-zinc-400 mt-0.5">Blocks all emails under specified root domains.</p> | |
| </div> | |
| <button | |
| onClick={() => { | |
| setBlocklistType("domain"); | |
| setIsAddBlocklistOpen(true); | |
| }} | |
| 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)]" | |
| > | |
| + Add Domain | |
| </button> | |
| </div> | |
| <div className="space-y-2"> | |
| {domainBlockList.map((item: any) => ( | |
| <div | |
| key={item.id} | |
| 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" | |
| > | |
| <div> | |
| <span className="font-mono text-zinc-200 font-medium">@{item.domain}</span> | |
| <div className="text-[10px] text-zinc-500 font-sans mt-0.5">{item.reason}</div> | |
| </div> | |
| <button | |
| onClick={async () => { | |
| await apiClient.delete(`/leads/domain-block-list/${item.id}`); | |
| toast.success("Domain removed from blocklist"); | |
| refetchDomainBlockList(); | |
| }} | |
| className="text-zinc-500 hover:text-rose-400 p-1.5 rounded-md hover:bg-rose-500/10 transition-colors" | |
| > | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Lead Detail Slide-Over Drawer */} | |
| <FormDrawer | |
| isOpen={Boolean(selectedLead)} | |
| onClose={() => setSelectedLead(null)} | |
| title="Prospect Dossier" | |
| subtitle={selectedLead ? `${selectedLead.first_name} ${selectedLead.last_name} (${selectedLead.email})` : ""} | |
| > | |
| {selectedLead && ( | |
| <div className="space-y-6 text-xs"> | |
| {/* Status Change Card */} | |
| <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-4"> | |
| <label className="block font-medium text-zinc-400 mb-2">Lead Outreach Status</label> | |
| <select | |
| value={selectedLead.status} | |
| onChange={(e) => { | |
| updateLeadStatusMutation.mutate({ | |
| campaignId: selectedLead.campaign_id, | |
| leadId: selectedLead.id, | |
| status: e.target.value, | |
| }); | |
| setSelectedLead({ ...selectedLead, status: e.target.value }); | |
| }} | |
| className="w-full bg-zinc-950 border border-zinc-800 rounded-lg p-2 text-zinc-100 font-medium" | |
| > | |
| {STATUSES.filter((s) => s !== "ALL").map((st) => ( | |
| <option key={st} value={st}> | |
| {st.replace(/_/g, " ")} | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| {/* Profile Fields */} | |
| <div className="space-y-3 bg-zinc-900/60 border border-zinc-800 rounded-xl p-4"> | |
| <h4 className="font-semibold text-zinc-300 uppercase tracking-wider text-[10px]">Contact Info</h4> | |
| <div className="grid grid-cols-2 gap-3"> | |
| <div> | |
| <span className="text-zinc-500 block">Company</span> | |
| <span className="text-zinc-200 font-medium">{selectedLead.company_name}</span> | |
| </div> | |
| <div> | |
| <span className="text-zinc-500 block">Location</span> | |
| <span className="text-zinc-200 font-medium">{selectedLead.location || "N/A"}</span> | |
| </div> | |
| <div> | |
| <span className="text-zinc-500 block">Phone</span> | |
| <span className="text-zinc-200 font-medium">{selectedLead.phone_number || "N/A"}</span> | |
| </div> | |
| <div> | |
| <span className="text-zinc-500 block">LinkedIn</span> | |
| {selectedLead.linkedin_profile ? ( | |
| <a | |
| href={selectedLead.linkedin_profile} | |
| target="_blank" | |
| rel="noreferrer" | |
| className="text-indigo-400 hover:underline" | |
| > | |
| View LinkedIn Profile | |
| </a> | |
| ) : ( | |
| <span className="text-zinc-500">Not provided</span> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Custom Fields */} | |
| {selectedLead.custom_fields && Object.keys(selectedLead.custom_fields).length > 0 && ( | |
| <div className="space-y-2 bg-zinc-900/60 border border-zinc-800 rounded-xl p-4"> | |
| <h4 className="font-semibold text-zinc-300 uppercase tracking-wider text-[10px]">Custom Attributes</h4> | |
| <div className="grid grid-cols-2 gap-2 font-mono text-[11px]"> | |
| {Object.entries(selectedLead.custom_fields).map(([key, val]) => ( | |
| <div key={key} className="bg-zinc-950 p-2 rounded border border-zinc-800"> | |
| <span className="text-zinc-500 block text-[10px]">{key}</span> | |
| <span className="text-zinc-200 font-semibold">{String(val)}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Timeline */} | |
| <div className="space-y-3 bg-zinc-900/60 border border-zinc-800 rounded-xl p-4"> | |
| <h4 className="font-semibold text-zinc-300 uppercase tracking-wider text-[10px]">Activity Timeline</h4> | |
| <div className="space-y-3"> | |
| <div className="flex items-start gap-2.5"> | |
| <div className="w-2 h-2 rounded-full bg-emerald-400 mt-1" /> | |
| <div> | |
| <span className="text-zinc-300 font-medium">Email Delivered & Opened</span> | |
| <div className="text-[10px] text-zinc-500">2 hours ago</div> | |
| </div> | |
| </div> | |
| <div className="flex items-start gap-2.5"> | |
| <div className="w-2 h-2 rounded-full bg-indigo-400 mt-1" /> | |
| <div> | |
| <span className="text-zinc-300 font-medium">Enrolled in Enterprise SaaS Sequence</span> | |
| <div className="text-[10px] text-zinc-500">3 days ago</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Delete Lead Button */} | |
| <div className="pt-2 flex justify-end"> | |
| <button | |
| onClick={() => { | |
| deleteLeadMutation.mutate({ | |
| campaignId: selectedLead.campaign_id, | |
| leadId: selectedLead.id, | |
| }); | |
| }} | |
| 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" | |
| > | |
| <Trash2 className="w-3.5 h-3.5" /> Remove Lead | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </FormDrawer> | |
| {/* Add Lead Slide-Over */} | |
| <FormDrawer | |
| isOpen={isAddLeadOpen} | |
| onClose={() => setIsAddLeadOpen(false)} | |
| title="Add Global Prospect Lead" | |
| subtitle="Input contact info, company details, and custom variables for personalization." | |
| > | |
| <form | |
| onSubmit={async (e) => { | |
| e.preventDefault(); | |
| const form = e.target as HTMLFormElement; | |
| const newLead = { | |
| first_name: (form.elements.namedItem("first_name") as HTMLInputElement).value, | |
| last_name: (form.elements.namedItem("last_name") as HTMLInputElement).value, | |
| email: (form.elements.namedItem("email") as HTMLInputElement).value, | |
| phone_number: (form.elements.namedItem("phone_number") as HTMLInputElement).value, | |
| company_name: (form.elements.namedItem("company_name") as HTMLInputElement).value, | |
| website: (form.elements.namedItem("website") as HTMLInputElement).value, | |
| location: (form.elements.namedItem("location") as HTMLInputElement).value, | |
| linkedin_profile: (form.elements.namedItem("linkedin_profile") as HTMLInputElement).value, | |
| status: "INTERESTED", | |
| }; | |
| try { | |
| await apiClient.post("/campaigns/camp_9901/leads", newLead); | |
| toast.success("Prospect added to database"); | |
| setIsAddLeadOpen(false); | |
| queryClient.invalidateQueries({ queryKey: ["leads-search"] }); | |
| } catch (err: any) { | |
| toast.error("Failed to add prospect"); | |
| } | |
| }} | |
| className="space-y-3.5 text-xs" | |
| > | |
| <div className="grid grid-cols-2 gap-3"> | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">First Name *</label> | |
| <input name="first_name" 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-300 mb-1">Last Name</label> | |
| <input name="last_name" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" /> | |
| </div> | |
| </div> | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">Email Address *</label> | |
| <input type="email" name="email" required className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" /> | |
| </div> | |
| <div className="grid grid-cols-2 gap-3"> | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">Company Name</label> | |
| <input name="company_name" 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-300 mb-1">Phone Number</label> | |
| <input name="phone_number" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" /> | |
| </div> | |
| </div> | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">Website URL</label> | |
| <input name="website" placeholder="https://" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2 text-zinc-100" /> | |
| </div> | |
| <div className="grid grid-cols-2 gap-3"> | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">Location</label> | |
| <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" /> | |
| </div> | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">LinkedIn URL</label> | |
| <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" /> | |
| </div> | |
| </div> | |
| <div className="pt-4 flex justify-end gap-2"> | |
| <button | |
| type="button" | |
| onClick={() => setIsAddLeadOpen(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 text-white rounded-lg font-medium shadow" | |
| > | |
| Save Prospect | |
| </button> | |
| </div> | |
| </form> | |
| </FormDrawer> | |
| {/* Add Blocklist Modal */} | |
| <FormDrawer | |
| isOpen={isAddBlocklistOpen} | |
| onClose={() => setIsAddBlocklistOpen(false)} | |
| title={blocklistType === "email" ? "Add Email to Suppression Blocklist" : "Add Domain to Suppression Blocklist"} | |
| subtitle="Blocked records will never receive any emails from any campaign." | |
| > | |
| <form | |
| onSubmit={async (e) => { | |
| e.preventDefault(); | |
| const form = e.target as HTMLFormElement; | |
| const val = (form.elements.namedItem("val") as HTMLInputElement).value; | |
| const reason = (form.elements.namedItem("reason") as HTMLInputElement).value; | |
| try { | |
| if (blocklistType === "email") { | |
| await apiClient.post("/leads/block-list", { email: val, reason }); | |
| refetchEmailBlockList(); | |
| } else { | |
| await apiClient.post("/leads/domain-block-list", { domain: val, reason }); | |
| refetchDomainBlockList(); | |
| } | |
| toast.success("Suppression entry saved"); | |
| setIsAddBlocklistOpen(false); | |
| } catch (err: any) { | |
| toast.error("Failed to add entry"); | |
| } | |
| }} | |
| className="space-y-4 text-xs" | |
| > | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1"> | |
| {blocklistType === "email" ? "Email Address" : "Domain (e.g. competitor.com)"} | |
| </label> | |
| <input name="val" required 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">Reason for Suppression</label> | |
| <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" /> | |
| </div> | |
| <div className="pt-4 flex justify-end gap-2"> | |
| <button | |
| type="button" | |
| onClick={() => setIsAddBlocklistOpen(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-rose-600 text-white rounded-lg font-medium" | |
| > | |
| Block Record | |
| </button> | |
| </div> | |
| </form> | |
| </FormDrawer> | |
| </div> | |
| ); | |
| } | |