Form
Overview
The primary form is the booking inquiry form at /contact. All form handling is done via Next.js Server Actions with Zod validation — no client-side form library is used.
Field Inventory
| Field | Type | Required | Validation |
|---|---|---|---|
| Name | Text input | Yes | Min 2 chars |
| Organization | Text input | Yes | Min 2 chars |
| Engagement Type | Select | Yes | One of defined options |
| Budget Range | Select | No | One of defined options |
| Preferred Date | Date input | No | Must be future date |
| Message | Textarea | Yes | Min 20 chars, max 1000 |
Field Styling
All form fields share the same base styling:
const fieldBase = 'w-full rounded-sm border border-brand-gray bg-brand-dark px-4 py-3 ' + 'text-sm text-white placeholder:text-white/30 ' + 'focus:border-brand-red focus:outline-none focus:ring-1 focus:ring-brand-red ' + 'transition-colors duration-150';Text Input
<input type="text" className={fieldBase} placeholder="Brett Johnson" aria-describedby="name-error"/>Select
<select className={`${fieldBase} appearance-none cursor-pointer`}> <option value="">Select type…</option> <option value="keynote">Keynote Speaking</option> <option value="workshop">Executive Workshop</option> <option value="training">Law Enforcement Training</option> <option value="consulting">Enterprise Consulting</option> <option value="media">Media / Documentary</option> <option value="podcast">Podcast / Interview</option></select>Textarea
<textarea rows={5} className={fieldBase} placeholder="Tell Brett about the engagement…"/>Validation — Zod Schema
import { z } from 'zod';
export const bookingSchema = z.object({ name: z.string().min(2).max(100), organization: z.string().min(2).max(100), engagementType: z.enum(['keynote', 'workshop', 'training', 'consulting', 'media', 'podcast']), budgetRange: z.enum(['under-10k', '10k-25k', '25k-50k', '50k-plus', 'tbd']).optional(), preferredDate: z.string().optional(), message: z.string().min(20).max(1000),});Server Action
'use server';
import { bookingSchema } from '@/lib/schemas/booking';import { saveLead } from '@/lib/supabase';import { sendConfirmation } from '@/lib/email';
export async function submitBookingInquiry(formData: FormData) { const raw = Object.fromEntries(formData.entries()); const result = bookingSchema.safeParse(raw);
if (!result.success) { return { error: 'Validation failed', fields: result.error.flatten().fieldErrors }; }
await saveLead(result.data); await sendConfirmation(result.data);
return { success: true };}Feedback States
Inline Field Error
{errors.name && ( <p id="name-error" className="mt-1 text-xs text-red-400" role="alert"> {errors.name} </p>)}Form-Level Success
{state.success && ( <div className="rounded-sm border border-brand-red/30 bg-brand-red/10 p-4 text-sm text-white"> Got it — Brett's team will follow up within 24 hours. </div>)}Form-Level Error
{state.error && ( <div className="rounded-sm border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-300" role="alert"> Something went wrong. Try again or email directly. </div>)}Accessibility
- Every field has an associated
<label>(visually present orsr-only) - Error messages are linked via
aria-describedby - Error messages use
role="alert"for screen reader announcement - Submit button is
disabledduring pending server action - Cloudflare Turnstile is rendered on the form for bot protection