/* Contact form (A9). Loaded only on the contact page. Client validation + honeypot + time-trap, POST to the relay, then success/error panel swap. Progressive enhancement over a visible e-mail address (the form degrades to mailto if this never runs). */ const form = document.getElementById('contact-form') as HTMLFormElement | null; if (form) { const loadedAt = Date.now(); const successPanel = document.getElementById('form-success'); const errorPanel = document.getElementById('form-error'); const submitBtn = form.querySelector('button[type="submit"]'); const submitLabel = submitBtn?.querySelector('[data-label]'); const sendingText = submitBtn?.dataset.sending ?? 'Sending'; const sendText = submitLabel?.textContent ?? 'Send'; const msgRequired = form.dataset.msgRequired ?? 'Required'; const msgEmail = form.dataset.msgEmail ?? 'Invalid e-mail'; function fieldError(name: string, message: string | null): void { const field = form!.querySelector(`[name="${name}"]`); const err = form!.querySelector(`[data-error-for="${name}"]`); if (field) field.setAttribute('aria-invalid', message ? 'true' : 'false'); if (err) { err.textContent = message ?? ''; err.hidden = !message; } } function validate(data: Record): boolean { let ok = true; let firstInvalid: string | null = null; const require = (name: string) => { if (!data[name]?.trim()) { fieldError(name, msgRequired); ok = false; firstInvalid ??= name; } else { fieldError(name, null); } }; require('name'); if (!data.email?.trim()) { fieldError('email', msgRequired); ok = false; firstInvalid ??= 'email'; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) { fieldError('email', msgEmail); ok = false; firstInvalid ??= 'email'; } else { fieldError('email', null); } require('message'); if (firstInvalid) form!.querySelector(`[name="${firstInvalid}"]`)?.focus(); return ok; } form.addEventListener('submit', async (e) => { e.preventDefault(); const fd = new FormData(form); const data = { name: String(fd.get('name') ?? ''), email: String(fd.get('email') ?? ''), message: String(fd.get('message') ?? ''), company: String(fd.get('company') ?? ''), // honeypot }; // Validate first, so a real person who submits an incomplete form always sees // the field errors (never a false "success"). if (!validate(data)) return; // Honeypot filled or submitted implausibly fast → treat as bot, feign success. if (data.company || Date.now() - loadedAt < 2000) { form.hidden = true; successPanel?.removeAttribute('hidden'); return; } submitBtn?.setAttribute('disabled', 'true'); if (submitLabel) submitLabel.textContent = sendingText; submitBtn?.classList.add('is-sending'); try { const res = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: data.name, email: data.email, message: data.message }), }); if (!res.ok) throw new Error(String(res.status)); form.hidden = true; successPanel?.removeAttribute('hidden'); } catch { errorPanel?.removeAttribute('hidden'); } finally { submitBtn?.removeAttribute('disabled'); submitBtn?.classList.remove('is-sending'); if (submitLabel) submitLabel.textContent = sendText; } }); }