165 lines
5.0 KiB
TypeScript
165 lines
5.0 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import {
|
|
Card,
|
|
CardTitle,
|
|
CardHeader,
|
|
CardDescription,
|
|
CardContent,
|
|
CardFooter,
|
|
} from "@/components/ui/card"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { z } from "zod"
|
|
|
|
const exampleFormSchema = z.object({
|
|
name: z.string().min(1),
|
|
email: z.string().email(),
|
|
message: z.string().min(1),
|
|
})
|
|
|
|
export function ExampleForm() {
|
|
const [pending, setPending] = React.useState(false)
|
|
const [state, setState] = React.useState({
|
|
defaultValues: {
|
|
name: "",
|
|
email: "",
|
|
message: "",
|
|
},
|
|
success: false,
|
|
errors: {
|
|
name: "",
|
|
email: "",
|
|
message: "",
|
|
},
|
|
})
|
|
|
|
const handleSubmit = React.useCallback(
|
|
(e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault()
|
|
setPending(true)
|
|
|
|
const formData = new FormData(e.target as HTMLFormElement)
|
|
const data = Object.fromEntries(formData.entries())
|
|
const result = exampleFormSchema.safeParse(data)
|
|
|
|
if (!result.success) {
|
|
setState({
|
|
...state,
|
|
errors: Object.fromEntries(
|
|
Object.entries(result.error.flatten().fieldErrors).map(
|
|
([key, value]) => [key, value?.[0] ?? ""]
|
|
)
|
|
) as Record<keyof typeof state.errors, string>,
|
|
})
|
|
setPending(false)
|
|
return
|
|
}
|
|
|
|
setPending(false)
|
|
},
|
|
[state]
|
|
)
|
|
|
|
return (
|
|
<Card className="w-full max-w-sm">
|
|
<CardHeader>
|
|
<CardTitle>How can we help?</CardTitle>
|
|
<CardDescription>
|
|
Need help with your project? We're here to assist you.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<form onSubmit={handleSubmit}>
|
|
<CardContent className="flex flex-col gap-6">
|
|
<div
|
|
className="group/field grid gap-2"
|
|
data-invalid={!!state.errors?.name}
|
|
>
|
|
<Label
|
|
htmlFor="name"
|
|
className="group-data-[invalid=true]/field:text-destructive"
|
|
>
|
|
Name <span aria-hidden="true">*</span>
|
|
</Label>
|
|
<Input
|
|
id="name"
|
|
name="name"
|
|
placeholder="Lee Robinson"
|
|
className="group-data-[invalid=true]/field:border-destructive focus-visible:group-data-[invalid=true]/field:ring-destructive"
|
|
disabled={pending}
|
|
aria-invalid={!!state.errors?.name}
|
|
aria-errormessage="error-name"
|
|
defaultValue={state.defaultValues.name}
|
|
/>
|
|
{state.errors?.name && (
|
|
<p id="error-name" className="text-destructive text-sm">
|
|
{state.errors.name}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div
|
|
className="group/field grid gap-2"
|
|
data-invalid={!!state.errors?.email}
|
|
>
|
|
<Label
|
|
htmlFor="email"
|
|
className="group-data-[invalid=true]/field:text-destructive"
|
|
>
|
|
Email <span aria-hidden="true">*</span>
|
|
</Label>
|
|
<Input
|
|
id="email"
|
|
name="email"
|
|
placeholder="leerob@acme.com"
|
|
className="group-data-[invalid=true]/field:border-destructive focus-visible:group-data-[invalid=true]/field:ring-destructive"
|
|
disabled={pending}
|
|
aria-invalid={!!state.errors?.email}
|
|
aria-errormessage="error-email"
|
|
defaultValue={state.defaultValues.email}
|
|
/>
|
|
{state.errors?.email && (
|
|
<p id="error-email" className="text-destructive text-sm">
|
|
{state.errors.email}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div
|
|
className="group/field grid gap-2"
|
|
data-invalid={!!state.errors?.message}
|
|
>
|
|
<Label
|
|
htmlFor="message"
|
|
className="group-data-[invalid=true]/field:text-destructive"
|
|
>
|
|
Message <span aria-hidden="true">*</span>
|
|
</Label>
|
|
<Textarea
|
|
id="message"
|
|
name="message"
|
|
placeholder="Type your message here..."
|
|
className="group-data-[invalid=true]/field:border-destructive focus-visible:group-data-[invalid=true]/field:ring-destructive"
|
|
disabled={pending}
|
|
aria-invalid={!!state.errors?.message}
|
|
aria-errormessage="error-message"
|
|
defaultValue={state.defaultValues.message}
|
|
/>
|
|
{state.errors?.message && (
|
|
<p id="error-message" className="text-destructive text-sm">
|
|
{state.errors.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button type="submit" size="sm" disabled={pending}>
|
|
{pending ? "Sending..." : "Send Message"}
|
|
</Button>
|
|
</CardFooter>
|
|
</form>
|
|
</Card>
|
|
)
|
|
}
|