"use client"; import { useActionState, useState } from "react"; import { useFormStatus } from "react-dom"; import { motion } from "framer-motion"; import { CheckMark } from "../ui/CheckMark"; import { Spinner } from "../ui/Spinner"; import { sendContact, type ContactState } from "@/app/actions/contact"; import { CONTACT_CHANNELS } from "@/lib/site"; import { EASE_OUT_EXPO } from "@/lib/motion"; const initialState: ContactState = { status: "idle", message: "" }; function SubmitButton() { const { pending } = useFormStatus(); return ( ); } const inputBase = "w-full bg-surface border px-4 py-3 text-sm text-foreground placeholder:text-foreground-muted focus:outline-none transition-colors duration-200"; /** Field errors slide in under their input rather than appearing under it. */ function FieldError({ children, id }: { children: string; id: number }) { return ( {children} ); } export function ContactForm() { const [state, formAction] = useActionState(sendContact, initialState); // A rejected submission has to be felt even when the message is identical to // the last one. React would keep the same paragraph mounted and replay // nothing, so every attempt gets a number and the number is the React key. // // Counted during render by comparing with the previous result rather than in // an effect: the action already returns a fresh object per submission, so // this needs no second render pass to stay in sync. const [seen, setSeen] = useState(state); const [attempt, setAttempt] = useState(0); if (seen !== state) { setSeen(state); setAttempt((count) => count + 1); } if (state.status === "success") { return (

MESSAGE SENT

{state.message}

); } const err = state.fieldErrors ?? {}; return (
{err.name && {err.name}}
{err.email && {err.email}}
{/* The desk the message is for. It replaces the three mailto cards as the default path: the visitor picks a reason instead of guessing which address to write to. */}
{err.subject && {err.subject}}