// ===================================================================== // External Recruiter Portal — what 3rd-party agency recruiters see when // they log in. Stripped down: no kanban, no sidebar, just their submitted // candidates grouped by role, with clear status updates. // ===================================================================== const { useMemo: useMemoEX, useState: useStateEX, useRef: useRefEX } = React; // ===== Submit candidate modal ===== function SubmitCandidateModal({ onClose, agency, viewer }) { const [resume, setResume] = useStateEX(null); const [form, setForm] = useStateEX({ firstName: "", lastName: "", email: "", phone: "", linkedin: "", website: "", role: "", idealStart: "", relocateSeattle: "", inPerson: "", why: "", currentComp: "", expectedComp: "", }); const [touched, setTouched] = useStateEX(false); const [draftSaved, setDraftSaved] = useStateEX(false); const [parsing, setParsing] = useStateEX(false); const fileRef = useRefEX(null); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); const required = ["firstName","lastName","email","role","relocateSeattle","inPerson","why"]; const missing = required.filter(k => !form[k]) ; const ready = resume && missing.length === 0; const onPick = () => fileRef.current?.click(); const handleFile = async (f) => { if (!f) return; setResume(f); setParsing(true); try { const fd = new FormData(); fd.append("file", f); const resp = await fetch(`${API_BASE_URL}/parse-resume`, { method: "POST", body: fd }); if (resp.ok) { const data = await resp.json(); if (data.parsed) { setForm(prev => ({ ...prev, firstName: prev.firstName || data.first_name || "", lastName: prev.lastName || data.last_name || "", email: prev.email || data.email || "", phone: prev.phone || data.phone || "", linkedin: prev.linkedin || data.linkedin || "", website: prev.website || data.website || "", })); } } } catch (err) { console.warn("Resume parse failed:", err); } finally { setParsing(false); } }; const onFile = (e) => { handleFile(e.target.files?.[0]); }; const onDrop = (e) => { e.preventDefault(); handleFile(e.dataTransfer?.files?.[0]); }; const onDrag = (e) => { e.preventDefault(); }; const openRoles = JOBS.filter(j => j.status !== "inactive"); return (
e.stopPropagation()}>
{agency ? agency.name : "Agency"} · Submit to Nuance

New candidate submission

{/* Resume drop zone — primary CTA */}
{!resume ? ( <>
Drop résumé here
or click to browse · PDF, DOC, DOCX up to 10MB
We'll auto-parse name, role, company & links to pre-fill the form below.
) : ( <>
{parsing ? "⏳" : "✓"}
{resume.name}
{(resume.size/1024).toFixed(0)} KB · {parsing ? "extracting info..." : "fields auto-filled"}
)}
{/* Form sections */}
Candidate
set("firstName", e.target.value)} placeholder="Aria" /> set("lastName", e.target.value)} placeholder="Chen" /> set("email", e.target.value)} placeholder="aria.chen@example.com" /> set("phone", e.target.value)} placeholder="+1 (555) 010-2345" /> set("linkedin", e.target.value)} placeholder="linkedin.com/in/ariachen" /> set("website", e.target.value)} placeholder="ariachen.dev" />
Submission
set("idealStart", e.target.value)} /> set("expectedComp", e.target.value)} placeholder="$220k base, open on equity" />
Fit confirmations
set("relocateSeattle", v)} options={[ { v: "yes", label: "Yes, willing to relocate" }, { v: "already", label: "Already in Seattle" }, { v: "maybe", label: "Open to discuss" }, { v: "no", label: "No / remote only" }, ]} /> set("inPerson", v)} options={[ { v: "yes", label: "Yes, all 5 days" }, { v: "hybrid", label: "Prefers hybrid (3-4 days)" }, { v: "no", label: "Remote-only" }, ]} />