File size: 14,073 Bytes
e9d1895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import { useState, useRef } from "react";
import { Search, Users, Loader2, FileSpreadsheet, Zap, Eye, Sparkles, Globe, Link2 } from "lucide-react";
import { batchAPI } from "../api/client";
import toast from "react-hot-toast";

export default function RecipientInput({ onResearch, onBatchStart, loading, disabled }) {
    const [mode, setMode] = useState("single");
    const [singleInput, setSingleInput] = useState("");
    const [batchInput, setBatchInput] = useState("");
    const [extractedEmails, setExtractedEmails] = useState([]);
    const [batchMode, setBatchMode] = useState("manual");
    const [delayMin, setDelayMin] = useState(15);
    const [delayMax, setDelayMax] = useState(45);
    const [extracting, setExtracting] = useState(false);
    const [directoryUrl, setDirectoryUrl] = useState("");
    const [scraping, setScraping] = useState(false);
    const fileRef = useRef(null);

    const handleSingleSubmit = (e) => { e.preventDefault(); if (singleInput.trim() && onResearch) onResearch(singleInput.trim()); };

    const handleCSVUpload = async (e) => {
        const file = e.target.files?.[0]; if (!file) return;
        setExtracting(true);
        try { const res = await batchAPI.uploadCSV(file); setExtractedEmails(res.data.emails); setBatchInput(res.data.emails.join("\n")); toast.success(`Found ${res.data.count} emails from ${file.name}`); }
        catch (err) { toast.error(err.response?.data?.detail || "Failed to parse file"); }
        setExtracting(false); if (fileRef.current) fileRef.current.value = "";
    };

    const handleExtractFromText = async () => {
        if (!batchInput.trim()) return; setExtracting(true);
        try { const res = await batchAPI.extractEmails(batchInput); setExtractedEmails(res.data.emails); setBatchInput(res.data.emails.join("\n")); toast.success(`Extracted ${res.data.count} emails`); }
        catch { toast.error("No emails found in the text"); }
        setExtracting(false);
    };

    const handleUrlScrape = async () => {
        if (!directoryUrl.trim()) return; setScraping(true);
        try { const res = await batchAPI.scrapeFaculty(directoryUrl.trim()); const emails = res.data.emails || []; setBatchInput(emails.join("\n")); setExtractedEmails(emails); setMode("batch"); toast.success(`Found ${res.data.count} email addresses!`); }
        catch (err) { toast.error(err.response?.data?.detail || "Could not scrape page"); }
        setScraping(false);
    };

    const handleBatchSubmit = () => {
        const list = batchInput.split("\n").map((l) => l.trim()).filter(Boolean);
        if (list.length > 0 && onBatchStart) onBatchStart(list, batchMode, delayMin, delayMax);
    };

    const emailCount = batchInput.split("\n").filter((l) => l.trim()).length;

    const tabClass = (active) =>
        `flex-1 text-[10px] py-1.5 px-1 rounded-lg font-semibold transition-all ${active
            ? "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border border-indigo-500/20"
            : "text-[var(--c-text-muted)] hover:text-[var(--c-text-secondary)] border border-transparent"
        }`;

    return (
        <div className="card-dark p-3.5">
            <div className="flex items-center gap-2 mb-3">
                <Sparkles size={14} className="text-indigo-500 dark:text-indigo-400" />
                <h3 className="text-xs font-semibold text-[var(--c-text-secondary)] uppercase tracking-wider">Compose & Send</h3>
            </div>

            <div className="flex gap-1 mb-3 bg-[var(--c-bg-subtle)] p-1 rounded-xl border border-[var(--c-border)]">
                <button onClick={() => setMode("single")} className={tabClass(mode === "single")}>Single</button>
                <button onClick={() => setMode("batch")} className={tabClass(mode === "batch")}>Batch</button>
                <button onClick={() => setMode("scrape")} className={tabClass(mode === "scrape")}>Scrape URL</button>
            </div>

            {mode === "single" && (
                <form onSubmit={handleSingleSubmit}>
                    <input type="text" value={singleInput} onChange={(e) => setSingleInput(e.target.value)} placeholder="recipient@example.com" disabled={disabled || loading} className="w-full input-dark text-sm mb-2.5" />
                    <button type="submit" disabled={!singleInput.trim() || loading || disabled} className="w-full btn-primary text-xs py-2.5">
                        {loading ? <><Loader2 size={13} className="animate-spin" /> Generating...</> : <><Search size={13} /> Generate Email</>}
                    </button>
                </form>
            )}

            {mode === "scrape" && (
                <div className="space-y-2.5">
                    <div className="rounded-xl border border-[var(--c-border)] bg-[var(--c-bg-subtle)] overflow-hidden focus-within:border-indigo-500/30 transition-all">
                        <div className="flex items-center gap-2 px-3 pt-2.5 pb-1">
                            <Link2 size={13} className="text-[var(--c-text-muted)] shrink-0" />
                            <input type="text" value={directoryUrl} onChange={(e) => setDirectoryUrl(e.target.value)} placeholder="https://example.com/team" disabled={disabled || scraping}
                                className="w-full text-[12px] outline-none bg-transparent text-[var(--c-text-primary)] placeholder:text-[var(--c-text-muted)]" />
                        </div>
                        <div className="px-3 py-1.5 bg-[var(--c-bg-subtle)] border-t border-[var(--c-border)]">
                            <span className="text-[9px] text-[var(--c-text-muted)]">Paste any page URL to extract email addresses</span>
                        </div>
                    </div>
                    <button onClick={handleUrlScrape} disabled={!directoryUrl.trim() || scraping || disabled}
                        className="w-full flex items-center justify-center gap-2 bg-gradient-to-r from-violet-500 to-purple-600 hover:from-violet-400 hover:to-purple-500 disabled:opacity-40 text-white text-xs font-semibold py-2.5 rounded-xl transition-all shadow-lg shadow-violet-500/15">
                        {scraping ? <><Loader2 size={13} className="animate-spin" /> Scraping page...</> : <><Globe size={13} /> Extract All Emails</>}
                    </button>
                    {extractedEmails.length > 0 && (
                        <div className="bg-emerald-500/5 border border-emerald-500/20 rounded-xl p-3">
                            <p className="text-[11px] font-semibold text-emerald-600 dark:text-emerald-400 mb-0.5">Found {extractedEmails.length} emails</p>
                            <p className="text-[10px] text-emerald-600/60 dark:text-emerald-500/60">Switched to Batch tab with all emails loaded</p>
                        </div>
                    )}
                </div>
            )}

            {mode === "batch" && (
                <div className="space-y-2.5">
                    <input ref={fileRef} type="file" accept=".csv,.txt,.xlsx" onChange={handleCSVUpload} className="hidden" />
                    <button onClick={() => fileRef.current?.click()} disabled={disabled || loading}
                        className="w-full flex items-center justify-center gap-2 border border-dashed border-gray-300 dark:border-white/10 hover:border-indigo-500/30 text-[var(--c-text-muted)] hover:text-indigo-500 dark:hover:text-indigo-400 text-[11px] font-medium py-2 rounded-xl transition-all hover:bg-indigo-500/5">
                        {extracting ? <><Loader2 size={12} className="animate-spin" /> Extracting...</> : <><FileSpreadsheet size={12} /> Upload CSV / Text File</>}
                    </button>

                    <div className="rounded-xl border border-[var(--c-border)] bg-[var(--c-bg-subtle)] overflow-hidden focus-within:border-indigo-500/30 transition-all">
                        <textarea value={batchInput} onChange={(e) => setBatchInput(e.target.value)} placeholder={"contact1@example.com\ncontact2@company.org\ncontact3@domain.net"} disabled={disabled || loading} rows={4}
                            className="w-full text-[12px] leading-relaxed px-3 py-2.5 outline-none resize-none bg-transparent text-[var(--c-text-primary)] placeholder:text-[var(--c-text-muted)] disabled:opacity-50" />
                        <div className="flex items-center justify-between px-3 py-1.5 bg-[var(--c-bg-subtle)] border-t border-[var(--c-border)]">
                            <span className="text-[10px] font-medium text-[var(--c-text-muted)]">{emailCount} {emailCount === 1 ? "recipient" : "recipients"}</span>
                            <span className="text-[9px] text-[var(--c-text-muted)]">One per line or paste any text</span>
                        </div>
                    </div>

                    {batchInput.trim() && !batchInput.includes("@") && (
                        <button onClick={handleExtractFromText} disabled={extracting} className="w-full text-[10px] text-indigo-500 dark:text-indigo-400 hover:text-indigo-600 dark:hover:text-indigo-300 font-semibold">
                            Extract emails from text
                        </button>
                    )}

                    <div className="bg-[var(--c-bg-subtle)] rounded-xl p-3 space-y-2 border border-[var(--c-border)]">
                        <div className="text-[10px] font-semibold text-[var(--c-text-muted)] uppercase tracking-wider">Send Mode</div>
                        <div className="flex gap-1 bg-[var(--c-bg-subtle)] p-0.5 rounded-lg border border-[var(--c-border)]">
                            <button onClick={() => setBatchMode("manual")}
                                className={`flex-1 flex items-center justify-center gap-1.5 text-[11px] py-2 rounded-lg font-semibold transition-all ${batchMode === "manual" ? "bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20" : "text-[var(--c-text-muted)] hover:text-[var(--c-text-secondary)] border border-transparent"}`}>
                                <Eye size={11} /> Review
                            </button>
                            <button onClick={() => setBatchMode("auto")}
                                className={`flex-1 flex items-center justify-center gap-1.5 text-[11px] py-2 rounded-lg font-semibold transition-all ${batchMode === "auto" ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20" : "text-[var(--c-text-muted)] hover:text-[var(--c-text-secondary)] border border-transparent"}`}>
                                <Zap size={11} /> Auto
                            </button>
                        </div>
                        <p className="text-[9px] text-[var(--c-text-muted)] leading-relaxed">
                            {batchMode === "manual" ? "Review & approve each email before sending" : "Generate & send all automatically"}
                        </p>
                        {batchMode === "auto" && (
                            <div className="pt-2.5 border-t border-[var(--c-border)]">
                                <div className="text-[10px] font-semibold text-[var(--c-text-muted)] uppercase tracking-wider mb-2">Delay between emails</div>
                                <div className="flex items-center gap-2">
                                    <div className="flex-1 min-w-[65px] relative group">
                                        <input type="number" value={delayMin} onChange={(e) => setDelayMin(Number(e.target.value))} min={5} max={120}
                                            className="w-full text-sm font-bold input-dark px-3 py-2.5 text-center tabular-nums [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none focus:ring-2 focus:ring-indigo-500/10 transition-all" />
                                        <div className="absolute inset-0 rounded-xl border border-transparent group-focus-within:border-indigo-500/30 pointer-events-none transition-all" />
                                    </div>
                                    <span className="text-[10px] font-bold text-[var(--c-text-muted)] uppercase shrink-0">to</span>
                                    <div className="flex-1 min-w-[65px] relative group">
                                        <input type="number" value={delayMax} onChange={(e) => setDelayMax(Number(e.target.value))} min={10} max={300}
                                            className="w-full text-sm font-bold input-dark px-3 py-2.5 text-center tabular-nums [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none focus:ring-2 focus:ring-indigo-500/10 transition-all" />
                                        <div className="absolute inset-0 rounded-xl border border-transparent group-focus-within:border-indigo-500/30 pointer-events-none transition-all" />
                                    </div>
                                    <div className="shrink-0 flex items-center justify-center bg-[var(--c-bg-subtle)] border border-[var(--c-border)] rounded-xl px-2.5 py-2.5">
                                        <span className="text-[10px] font-bold text-[var(--c-text-muted)] uppercase">sec</span>
                                    </div>
                                </div>
                            </div>
                        )}
                    </div>

                    <button onClick={handleBatchSubmit} disabled={emailCount === 0 || loading || disabled}
                        className="w-full flex items-center justify-center gap-2 bg-gradient-to-r from-indigo-500 to-violet-600 hover:from-indigo-400 hover:to-violet-500 disabled:opacity-40 text-white text-xs font-semibold py-2.5 rounded-xl transition-all shadow-lg shadow-indigo-500/15">
                        {loading ? <><Loader2 size={13} className="animate-spin" /> Processing...</> : <><Users size={13} /> Start Batch ({emailCount})</>}
                    </button>
                </div>
            )}
        </div>
    );
}