/* ===================================================== ETL Contact Wizard — integrated version Ported to ETL design tokens (--accent-amber, --font-mono, --bg-card, --radius-sm/md/lg, --space-*). No standalone chrome. Posts to formsubmit.co/ajax/sales@embeddedc.co.uk with Cloudflare Turnstile, matching the existing site. ===================================================== */ const { useState, useEffect, useMemo, useRef } = React; const FORM_ENDPOINT = 'https://formsubmit.co/ajax/sales@embeddedc.co.uk'; const TURNSTILE_SITEKEY = '0x4AAAAAACvzVSnkhV7BMa72'; const ENQUIRY_PATHS = [ { id: 'project', glyph: '>', title: 'New project / firmware build', sub: 'Board bring-up, BLE, IoT, embedded Linux, mobile pairing.', steps: ['path', 'scope', 'platform', 'timeline', 'budget', 'contact'], }, { id: 'retainer', glyph: '∿', title: 'Flexible workforce retainer', sub: 'Full team on tap — firmware, BLE, Linux, mobile. 30-day notice.', steps: ['path', 'retainer', 'coverage', 'timeline', 'contact'], }, { id: 'partnership', glyph: '◇', title: 'Recruitment / partnership', sub: 'Sub-contracting, specialist referrals, joint bids.', steps: ['path', 'partnership', 'contact'], }, ]; const STEP_LABELS = { path: 'Select path', scope: 'Project scope', platform: 'Stack', timeline: 'Timeline', budget: 'Budget', retainer: 'Retainer size', coverage: 'Coverage gap', partnership: 'Partnership', contact: 'Your details', }; const PLATFORMS = [ 'ARM Cortex M4/M7', 'Nordic nRF5340', 'ESP32', 'Raspberry Pi', 'Atmel', 'PIC', 'Ambiq', 'Custom / TBD', "Don't know" ]; const PROTOCOLS = ['BLE', 'WiFi', 'MQTT', 'LoRa', 'CAN', 'SPI / I2C', 'Cellular', 'USB']; const STAGES = [ { id: 'concept', t: 'Concept', s: 'Spec and architecture only' }, { id: 'proto', t: 'Prototype', s: 'Breadboard / dev-kit stage' }, { id: 'pilot', t: 'Pilot', s: 'Small-batch hardware, early firmware' }, { id: 'production', t: 'Production', s: 'Certifying or live' }, ]; const TEAM_SIZES = [ { id: 'one', t: '1 engineer', hrs: '40 hrs/mo' }, { id: 'two', t: '2 engineers', hrs: '80 hrs/mo' }, { id: 'team', t: 'Full team', hrs: 'Flex capacity' }, { id: 'scoping', t: 'Scoping call first', hrs: '' }, ]; const TIMELINE_OPTS = [ { id: 'urgent', t: 'Urgent — within 2 weeks', s: 'We hold a small urgent slice; let us know why it burns.', g: '!' }, { id: 'q1', t: 'Within the next quarter', s: 'The sweet spot. We can start in 1–2 weeks.', g: 'Q1' }, { id: 'q2', t: '3–6 months out', s: 'Scoping now gives the best architecture time.', g: 'Q2' }, { id: 'flex', t: 'Exploratory / flexible', s: 'Happy to shape the plan with you before committing dates.', g: '∞' }, ]; function budgetLabel(v) { if (v < 20) return 'Under £10k'; if (v < 40) return '£10k – £50k'; if (v < 60) return '£50k – £100k'; if (v < 80) return '£100k – £250k'; return '£250k+'; } function budgetValueForForm(v) { if (v < 20) return 'under-10k'; if (v < 40) return '10k-50k'; if (v < 60) return '50k-100k'; if (v < 80) return '100k-250k'; return '250k-plus'; } /* ----------------------------------------------------- Cloudflare Turnstile (rendered once, reused across path changes) ----------------------------------------------------- */ function TurnstileBox({ onToken }) { const ref = useRef(null); const rendered = useRef(false); useEffect(() => { if (rendered.current) return; const tryRender = () => { if (window.turnstile && ref.current) { window.turnstile.render(ref.current, { sitekey: TURNSTILE_SITEKEY, theme: 'dark', callback: (token) => onToken(token), }); rendered.current = true; } else { setTimeout(tryRender, 200); } }; tryRender(); }, [onToken]); return
; } /* ----------------------------------------------------- Wizard ----------------------------------------------------- */ function Wizard({ onComplete }) { const [pathId, setPathId] = useState(null); const path = useMemo(() => ENQUIRY_PATHS.find(p => p.id === pathId), [pathId]); const steps = path ? path.steps : ['path']; const [stepIdx, setStepIdx] = useState(0); const currentStep = steps[stepIdx]; const [data, setData] = useState({ scope: '', platforms: [], protocols: [], stage: null, timeline: 'q1', budget: 50, teamSize: null, disciplines: [], coverage: '', orgType: '', brief: '', name: '', email: '', company: '', phone: '', channel: 'email', nda: false, source: '', }); const [errors, setErrors] = useState({}); const [sending, setSending] = useState(false); const [submitError, setSubmitError] = useState(''); const [turnstileToken, setTurnstileToken] = useState(''); useEffect(() => { setStepIdx(0); setErrors({}); }, [pathId]); const update = (k, v) => setData(d => ({ ...d, [k]: v })); const toggleArr = (k, v) => setData(d => ({ ...d, [k]: d[k].includes(v) ? d[k].filter(x => x !== v) : [...d[k], v], })); function hasLink(s) { return /\b(?:https?:\/\/|www\.|ftp:\/\/)\S+/i.test(s) || /\b[a-z0-9-]+\.(?:com|net|org|io|co|uk|ru|cn|info|biz|xyz|top|online|site|live|shop|club|me|tv|app|dev|ly|gg|tk|ml|ga|cf)\b/i.test(s) || / s.id === data.stage)?.t}`); if (data.platforms.length) lines.push(`Platforms: ${data.platforms.join(', ')}`); if (data.protocols.length) lines.push(`Protocols: ${data.protocols.join(', ')}`); if (path?.id === 'project') { lines.push(`Timeline: ${TIMELINE_OPTS.find(t => t.id === data.timeline)?.t}`); lines.push(`Budget: ${budgetLabel(data.budget)}`); if (data.nda) lines.push('NDA: requested before first call.'); } if (path?.id === 'retainer') { const sz = TEAM_SIZES.find(t => t.id === data.teamSize); lines.push(`Retainer size: ${sz?.t}`); if (data.disciplines.length) lines.push(`Disciplines: ${data.disciplines.join(', ')}`); if (data.coverage) lines.push(`\nCoverage gap:\n${data.coverage}`); lines.push(`Timeline: ${TIMELINE_OPTS.find(t => t.id === data.timeline)?.t}`); } if (path?.id === 'partnership') { if (data.orgType) lines.push(`Org type: ${data.orgType}`); if (data.brief) lines.push(`\nBrief:\n${data.brief}`); } lines.push(`\nPreferred first contact: ${data.channel}`); return lines.join('\n'); } async function submit() { setSubmitError(''); if (!turnstileToken) { setSubmitError('Please complete the security check below.'); return; } setSending(true); const payload = { _subject: 'New enquiry from ETL website', _template: 'table', _captcha: 'false', name: data.name, email: data.email, company: data.company, phone: data.phone, enquiry_type: path?.id || '', budget: path?.id === 'project' ? budgetValueForForm(data.budget) : '', timeline: (path?.id === 'project' || path?.id === 'retainer') ? data.timeline : '', preferred_contact: data.channel, brief: buildBriefText(), 'cf-turnstile-response': turnstileToken, }; try { const res = await fetch(FORM_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error('bad status'); setSending(false); onComplete({ ...data, path: pathId }); } catch (err) { setSending(false); setSubmitError('Something went wrong. Please email us directly at sales@embeddedc.co.uk'); // Reset Turnstile so the user can retry if (window.turnstile) window.turnstile.reset(); setTurnstileToken(''); } } const totalSteps = steps.length; const pktCount = Math.max(1, totalSteps - 1); const pktIdx = Math.max(0, stepIdx - 1); return (Pick the closest match — we'll branch from here. Not a perfect fit? Select the nearest and we'll route you correctly after the first call.
A few sentences is plenty. What is it, what's the hardest part, and where are you stuck? Please do not include links or URLs.
Even a best-guess helps us assign the right engineer from day one. Pick as many as apply.
Our calendar is currently booking next-quarter — but we hold a slice of capacity for urgent briefs.
This doesn't lock anything in — we use it to size the first conversation. Ballpark is fine.
Retainers scale monthly. Pick the closest size — the exact mix gets shaped on the first call.
Absence cover, surge capacity, a skill you can't hire for? Tell us what's broken and we'll match the right people.
Sub-contracts, joint bids, specialist referrals — say what you're after and who you're representing.
We reply from a real engineer, not an auto-responder. Typical response within one business day.
We've logged your enquiry and a senior engineer will read it within one business day. Expect a reply from a real inbox — not an auto-responder.