'use client'; /** * Contact form (#702). Posts to /api/contact; see lib/contact.ts for what * happens to the message. * * Spam protection without a captcha, which would be another third party: a * honeypot field people never see, and `startedAt`, the time the form was * rendered, which the server compares against the submission. */ import { useState } from 'react'; import { useDictionary } from './I18nProvider'; interface Props { retentionDays: number; } type State = 'idle' | 'sending' | 'done'; export default function ContactForm({ retentionDays }: Props) { const t = useDictionary().contact; // When the form became usable. A bot posting straight to the endpoint has // none; one filling the form on load is faster than MIN_FILL_MS. const [startedAt] = useState(() => Date.now()); const [state, setState] = useState('idle'); const [error, setError] = useState(''); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); const data = new FormData(e.currentTarget); setError(''); setState('sending'); try { const res = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: data.get('name'), email: data.get('email'), message: data.get('message'), website: data.get('website'), startedAt, }), }); if (res.ok) { setState('done'); return; } setError(res.status === 400 ? t.invalid : res.status === 429 ? t.tooMany : t.failed); } catch { setError(t.failed); } setState('idle'); } if (state === 'done') { return (

{t.successTitle}

{t.successText}

); } const sending = state === 'sending'; return (