// CareerApplicationForm.jsx // // Real, in-site application form - replaces the old "Apply via Email" // mailto: link on CareersPage.jsx. Posts to the same /api/mail-handler.php // endpoint DemoRequestForm.jsx uses (formType=career instead of demo), so // there's one backend, one PHPMailer setup, one spam-guard implementation // to maintain, not two. // // Deliberately reuses MAIL_ENDPOINT, TURNSTILE_SITE_KEY, loadTurnstileScript, // and useTurnstile from DemoRequestForm.jsx as plain globals rather than // redefining them - classic scripts on this site share one global lexical // scope (see Navbar.jsx's header comment), and DemoRequestForm.jsx loads // before this file in careers/index.html's script list specifically so // this works, the same pattern NotFoundPage.jsx uses for REAL_MODULES and // SolutionsIndexPage.jsx uses for SOLUTIONS. // // Resume validation mirrors the server's rules (PDF/DOC/DOCX, 5MB) so a // visitor sees the real problem immediately instead of waiting on a round // trip - but the server re-checks everything itself (real MIME-type // sniffing, not just the extension a browser reports) since client-side // checks are trivially bypassable. // // Loaded as a classic (non-module) Babel-transformed script - see // index.html's header comment. const MAX_RESUME_BYTES = 5 * 1024 * 1024; const ALLOWED_RESUME_EXTENSIONS = [".pdf", ".doc", ".docx"]; function CareerApplicationForm({ role, onDone }) { const [form, setForm] = React.useState({ name: "", email: "", phone: "", message: "" }); const [resumeFile, setResumeFile] = React.useState(null); const [honeypot, setHoneypot] = React.useState(""); const [errors, setErrors] = React.useState({}); const [status, setStatus] = React.useState("idle"); // idle | submitting | done | error const [serverMessage, setServerMessage] = React.useState(""); const mountedAt = React.useRef(Date.now()); const { containerRef, getToken } = useTurnstile(); const update = (field) => (e) => setForm((f) => ({ ...f, [field]: e.target.value })); const handleFileChange = (e) => { const file = e.target.files && e.target.files[0]; e.target.value = ""; // let the same file be re-selected after a validation error if (!file) return; const ext = "." + (file.name.split(".").pop() || "").toLowerCase(); if (!ALLOWED_RESUME_EXTENSIONS.includes(ext)) { setErrors((er) => ({ ...er, resume: "Please upload a PDF or Word document (.pdf, .doc, .docx)." })); setResumeFile(null); return; } if (file.size > MAX_RESUME_BYTES) { setErrors((er) => ({ ...er, resume: "Resume must be under 5MB." })); setResumeFile(null); return; } setErrors((er) => { const next = { ...er }; delete next.resume; return next; }); setResumeFile(file); }; const validate = () => { const next = {}; if (!form.name.trim()) next.name = "Name is required."; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) next.email = "Enter a valid email."; if (!resumeFile) next.resume = "Please attach your resume."; setErrors(next); return Object.keys(next).length === 0; }; const handleSubmit = async (e) => { e.preventDefault(); if (!validate()) return; setStatus("submitting"); setServerMessage(""); try { const token = await getToken(); const body = new FormData(); body.append("formType", "career"); body.append("name", form.name); body.append("email", form.email); body.append("phone", form.phone); body.append("role", role); body.append("message", form.message); body.append("resume", resumeFile); body.append("company_website", honeypot); body.append("form_elapsed_ms", String(Date.now() - mountedAt.current)); body.append("cf-turnstile-response", token); const res = await fetch(MAIL_ENDPOINT, { method: "POST", body }); const data = await res.json(); if (data.success) { setServerMessage(data.message || ""); setStatus("done"); } else { setServerMessage(data.message || "Something went wrong - please try again."); setStatus("error"); } } catch (err) { setServerMessage("We couldn't reach the server - check your connection and try again."); setStatus("error"); } }; if (status === "done") { return (
{serverMessage || "We've got your application and will get back to you directly."}