Download frontend/src/components/workspace/DealLinkSearch.jsx from Seth0330/EMAILOUT: direct link, hf CLI and curl.
- Browser
- Download file 7.27 kB
-
https://huggingface.co/spaces/Seth0330/EMAILOUT/resolve/main/frontend/src/components/workspace/DealLinkSearch.jsx
- Command line
-
hf download hf://spaces/Seth0330/EMAILOUT/frontend/src/components/workspace/DealLinkSearch.jsx
-
curl -L -o DealLinkSearch.jsx https://huggingface.co/spaces/Seth0330/EMAILOUT/resolve/main/frontend/src/components/workspace/DealLinkSearch.jsx
7.27 kB
| import React, { useEffect, useState } from 'react'; | |
| import { Loader2, Search, UserPlus, Building2 } from 'lucide-react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Input } from '@/components/ui/input'; | |
| import { cn } from '@/lib/utils'; | |
| import { apiFetch } from '@/lib/api'; | |
| /** Search CRM contacts and link one to the deal (`PATCH contact_id`). */ | |
| export function DealContactSearch({ linkedContactId, onPatchDeal, className }) { | |
| const [q, setQ] = useState(''); | |
| const [results, setResults] = useState([]); | |
| const [loading, setLoading] = useState(false); | |
| const [linking, setLinking] = useState(false); | |
| useEffect(() => { | |
| if (!q.trim()) { | |
| setResults([]); | |
| return; | |
| } | |
| const t = setTimeout(async () => { | |
| setLoading(true); | |
| try { | |
| const res = await apiFetch( | |
| `/api/contacts?search=${encodeURIComponent(q.trim())}&limit=12&sort_by=created_at&sort_dir=desc` | |
| ); | |
| const data = await res.json().catch(() => ({})); | |
| setResults(res.ok ? data.contacts || [] : []); | |
| } catch { | |
| setResults([]); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }, 280); | |
| return () => clearTimeout(t); | |
| }, [q]); | |
| const pick = async (c) => { | |
| setLinking(true); | |
| try { | |
| await onPatchDeal({ contact_id: c.id }); | |
| setQ(''); | |
| setResults([]); | |
| } finally { | |
| setLinking(false); | |
| } | |
| }; | |
| const unlink = async () => { | |
| setLinking(true); | |
| try { | |
| await onPatchDeal({ contact_id: null }); | |
| } finally { | |
| setLinking(false); | |
| } | |
| }; | |
| return ( | |
| <div className={cn('rounded-lg border border-slate-200 bg-slate-50/80 p-3 mb-3', className)}> | |
| <div className="flex items-center gap-2 text-xs font-medium text-slate-600 mb-2"> | |
| <UserPlus className="h-3.5 w-3.5 shrink-0" /> | |
| Link contact from CRM | |
| </div> | |
| <div className="relative"> | |
| <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400 pointer-events-none" /> | |
| <Input | |
| value={q} | |
| onChange={(e) => setQ(e.target.value)} | |
| placeholder="Search name, email, company…" | |
| className="pl-9 text-sm bg-white" | |
| disabled={linking} | |
| /> | |
| {loading ? ( | |
| <Loader2 className="absolute right-2.5 top-1/2 -translate-y-1/2 h-4 w-4 animate-spin text-slate-400" /> | |
| ) : null} | |
| </div> | |
| {results.length > 0 ? ( | |
| <ul className="mt-2 max-h-40 overflow-y-auto rounded-md border border-slate-200 bg-white text-sm divide-y divide-slate-100"> | |
| {results.map((c) => ( | |
| <li key={c.id}> | |
| <button | |
| type="button" | |
| disabled={linking || linkedContactId === c.id} | |
| className="w-full text-left px-3 py-2 hover:bg-violet-50 disabled:opacity-50" | |
| onClick={() => pick(c)} | |
| > | |
| <div className="font-medium text-slate-800 truncate"> | |
| {[c.first_name, c.last_name].filter(Boolean).join(' ') || '—'} | |
| </div> | |
| <div className="text-xs text-slate-500 truncate">{c.email || '—'}</div> | |
| <div className="text-xs text-slate-400 truncate">{c.company || ''}</div> | |
| </button> | |
| </li> | |
| ))} | |
| </ul> | |
| ) : null} | |
| {linkedContactId ? ( | |
| <div className="mt-2 flex justify-end"> | |
| <Button type="button" variant="ghost" size="sm" className="text-slate-600" onClick={unlink} disabled={linking}> | |
| Remove contact link | |
| </Button> | |
| </div> | |
| ) : null} | |
| </div> | |
| ); | |
| } | |
| /** Search distinct company names from contacts; sets deal `account_name`. */ | |
| export function DealCompanySearch({ onPatchDeal, className }) { | |
| const [q, setQ] = useState(''); | |
| const [results, setResults] = useState([]); | |
| const [loading, setLoading] = useState(false); | |
| const [applying, setApplying] = useState(false); | |
| useEffect(() => { | |
| if (!q.trim()) { | |
| setResults([]); | |
| return; | |
| } | |
| const t = setTimeout(async () => { | |
| setLoading(true); | |
| try { | |
| const res = await apiFetch(`/api/company-names?q=${encodeURIComponent(q.trim())}&limit=20`); | |
| const data = await res.json().catch(() => ({})); | |
| setResults(res.ok ? data.names || [] : []); | |
| } catch { | |
| setResults([]); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }, 280); | |
| return () => clearTimeout(t); | |
| }, [q]); | |
| const pick = async (name) => { | |
| setApplying(true); | |
| try { | |
| await onPatchDeal({ account_name: name }); | |
| setQ(''); | |
| setResults([]); | |
| } finally { | |
| setApplying(false); | |
| } | |
| }; | |
| return ( | |
| <div className={cn('rounded-lg border border-slate-200 bg-slate-50/80 p-3 mb-3', className)}> | |
| <div className="flex items-center gap-2 text-xs font-medium text-slate-600 mb-2"> | |
| <Building2 className="h-3.5 w-3.5 shrink-0" /> | |
| Add company from CRM (account name) | |
| </div> | |
| <div className="relative"> | |
| <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400 pointer-events-none" /> | |
| <Input | |
| value={q} | |
| onChange={(e) => setQ(e.target.value)} | |
| placeholder="Search company names…" | |
| className="pl-9 text-sm bg-white" | |
| disabled={applying} | |
| /> | |
| {loading ? ( | |
| <Loader2 className="absolute right-2.5 top-1/2 -translate-y-1/2 h-4 w-4 animate-spin text-slate-400" /> | |
| ) : null} | |
| </div> | |
| {results.length > 0 ? ( | |
| <ul className="mt-2 max-h-36 overflow-y-auto rounded-md border border-slate-200 bg-white text-sm divide-y divide-slate-100"> | |
| {results.map((name) => ( | |
| <li key={name}> | |
| <button | |
| type="button" | |
| disabled={applying} | |
| className="w-full text-left px-3 py-2 hover:bg-violet-50" | |
| onClick={() => pick(name)} | |
| > | |
| {name} | |
| </button> | |
| </li> | |
| ))} | |
| </ul> | |
| ) : null} | |
| </div> | |
| ); | |
| } | |