"use client"; import { useState } from "react"; import Turnstile, { awaitTurnstile, resetTurnstile, turnstileOn } from "@/components/Turnstile"; import { Field, LiveRegion } from "@/components/ui"; const TOPICS = ["A question", "Book a walkthrough", "Security review", "Something’s missing"]; const SLOTS = ["A weekday morning", "A weekday afternoon", "Tell me what suits"]; const label: React.CSSProperties = { fontSize: 11, fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--color-neutral-700)" }; export default function ContactForm() { const [f, setF] = useState({ name: "", role: "", facility: "", email: "", message: "", company: "" }); const [topic, setTopic] = useState(TOPICS[0]); const [slot, setSlot] = useState(""); const [cfToken, setCfToken] = useState(""); const [busy, setBusy] = useState(false); const [sent, setSent] = useState(false); const [err, setErr] = useState(""); const set = (p: Partial) => setF({ ...f, ...p }); async function submit(e: React.FormEvent) { e.preventDefault(); if (busy || sent) return; setBusy(true); setErr(""); try { /* Waited for at submit, rather than demanded before the button works. * * Send used to be disabled until Turnstile handed over a token, and Turnstile's error * callback publishes an empty one — so a network that blocks or inspects * challenges.cloudflare.com left a permanently grey button with nothing said, on the only * support channel this site offers. Now the token is waited for, the message goes either way, * and if the check really is missing the server says so in words. */ const token = cfToken || (turnstileOn() ? await awaitTurnstile() : ""); const r = await fetch("/api/contact", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...f, topic, slot, cfToken: token }), }); const j = await r.json().catch(() => ({})); if (!r.ok) { setErr(j.error || "Couldn’t send that. Try again, or email hello@threadcount.tech."); setCfToken(""); resetTurnstile(); return; } setSent(true); } catch { setErr("Network error — nothing was sent. Try again, or email hello@threadcount.tech."); } finally { setBusy(false); } } return (
{/* The inline label style each of these carried is what .field label already applies, so the fields draw exactly as they did — and, unlike the old markup, each label is now tied to the box it names rather than merely sitting above it. */} {(c) => set({ name: e.target.value })} autoComplete="name" required />} {(c) => set({ role: e.target.value })} placeholder="Uniform coordinator" />} {(c) => set({ facility: e.target.value })} />} {(c) => set({ email: e.target.value })} autoComplete="email" required />}
What’s this about
{/* The chosen topic is a red fill and nothing else, and the heading above was a loose
attached to nothing. Both are now said in words. */}
{TOPICS.map((t) => )}
{topic === "Book a walkthrough" && (
When suits
{SLOTS.map((s) => )}
)} {(c) =>