"use client"; import { useActionState, useState } from "react"; import { useDebounce } from "react-use"; import { SendIcon, Loader2Icon, CheckIcon, XIcon } from "lucide-react"; import Link from "@/components/link"; import Input from "@/components/ui/input"; import Textarea from "@/components/ui/textarea"; import Button from "@/components/ui/button"; import { MarkdownIcon } from "@/components/icons"; import { cn } from "@/lib/utils"; import { send, type ContactState } from "@/lib/server/contact"; import { ContactSchema, type ContactInput } from "@/lib/schemas/contact"; const ContactForm = () => { const [formState, formAction, pending] = useActionState(send, { success: false, message: "", }); // keep track of input so we can repopulate the fields if the form fails const [formFields, setFormFields] = useState({ name: "", email: "", message: "", }); // keep track of which fields have been touched const [touched, setTouched] = useState>({ name: false, email: false, message: false, }); // client-side validation using shared schema const [clientErrors, setClientErrors] = useState>>({}); useDebounce( () => { const result = ContactSchema.safeParse(formFields); setClientErrors(result.success ? {} : result.error.flatten().fieldErrors); }, 150, [formFields] ); const hasClientErrors = Object.values(clientErrors).some((errs) => (errs?.length || 0) > 0); const getErrorForField = (field: keyof ContactInput): string | undefined => { const ce = clientErrors[field]?.[0]; if (ce) return ce; if (!touched[field]) return formState.errors?.[field]?.[0]; return undefined; }; const nameError = getErrorForField("name"); const emailError = getErrorForField("email"); const messageError = getErrorForField("message"); return (
{ setFormFields({ ...formFields, name: e.target.value }); setTouched((t) => ({ ...t, name: true })); }} onBlur={() => setTouched((t) => ({ ...t, name: true }))} disabled={pending || formState.success} aria-invalid={nameError ? "true" : undefined} /> {nameError && {nameError}}
{ setFormFields({ ...formFields, email: e.target.value }); setTouched((t) => ({ ...t, email: true })); }} onBlur={() => setTouched((t) => ({ ...t, email: true }))} disabled={pending || formState.success} aria-invalid={emailError ? "true" : undefined} /> {emailError && {emailError}}