mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-10-27 01:36:25 -04:00
Refactor contact form validation to use useDebounce for improved performance and streamline error handling. Simplify error retrieval logic and ensure touched state is updated on input changes.
This commit is contained in:
3 files changed
+29
-15
No files matched your search
+28
-14
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useActionState, useEffect, useState } from "react";
|
import { useActionState, useState } from "react";
|
||||||
|
import { useDebounce } from "react-use";
|
||||||
import { SendIcon, Loader2Icon, CheckIcon, XIcon } from "lucide-react";
|
import { SendIcon, Loader2Icon, CheckIcon, XIcon } from "lucide-react";
|
||||||
import Link from "@/components/link";
|
import Link from "@/components/link";
|
||||||
import Input from "@/components/ui/input";
|
import Input from "@/components/ui/input";
|
||||||
@@ -8,8 +9,8 @@ import Textarea from "@/components/ui/textarea";
|
|||||||
import Button from "@/components/ui/button";
|
import Button from "@/components/ui/button";
|
||||||
import { MarkdownIcon } from "@/components/icons";
|
import { MarkdownIcon } from "@/components/icons";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { send, type ContactState } from "@/lib/server/resend";
|
import { send, type ContactState } from "@/lib/server/contact";
|
||||||
import { ContactSchema, type ContactInput } from "@/lib/validation/contact";
|
import { ContactSchema, type ContactInput } from "@/lib/schemas/contact";
|
||||||
|
|
||||||
const ContactForm = () => {
|
const ContactForm = () => {
|
||||||
const [formState, formAction, pending] = useActionState<ContactState, FormData>(send, {
|
const [formState, formAction, pending] = useActionState<ContactState, FormData>(send, {
|
||||||
@@ -24,27 +25,37 @@ const ContactForm = () => {
|
|||||||
message: "",
|
message: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// client-side validation using shared schema
|
// keep track of which fields have been touched
|
||||||
const [clientErrors, setClientErrors] = useState<Partial<Record<keyof ContactInput, string[]>>>({});
|
const [touched, setTouched] = useState<Record<keyof ContactInput, boolean>>({
|
||||||
const [touched, setTouched] = useState<{ name: boolean; email: boolean; message: boolean }>({
|
|
||||||
name: false,
|
name: false,
|
||||||
email: false,
|
email: false,
|
||||||
message: false,
|
message: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
// client-side validation using shared schema
|
||||||
const id = setTimeout(() => {
|
const [clientErrors, setClientErrors] = useState<Partial<Record<keyof ContactInput, string[]>>>({});
|
||||||
|
|
||||||
|
useDebounce(
|
||||||
|
() => {
|
||||||
const result = ContactSchema.safeParse(formFields);
|
const result = ContactSchema.safeParse(formFields);
|
||||||
setClientErrors(result.success ? {} : result.error.flatten().fieldErrors);
|
setClientErrors(result.success ? {} : result.error.flatten().fieldErrors);
|
||||||
}, 200);
|
},
|
||||||
return () => clearTimeout(id);
|
150,
|
||||||
}, [formFields]);
|
[formFields]
|
||||||
|
);
|
||||||
|
|
||||||
const hasClientErrors = Object.values(clientErrors).some((errs) => (errs?.length || 0) > 0);
|
const hasClientErrors = Object.values(clientErrors).some((errs) => (errs?.length || 0) > 0);
|
||||||
|
|
||||||
const nameError = (touched.name ? clientErrors.name?.[0] : undefined) ?? formState.errors?.name?.[0];
|
const getErrorForField = (field: keyof ContactInput): string | undefined => {
|
||||||
const emailError = (touched.email ? clientErrors.email?.[0] : undefined) ?? formState.errors?.email?.[0];
|
const ce = clientErrors[field]?.[0];
|
||||||
const messageError = (touched.message ? clientErrors.message?.[0] : undefined) ?? formState.errors?.message?.[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 (
|
return (
|
||||||
<form action={formAction} className="my-6 space-y-4">
|
<form action={formAction} className="my-6 space-y-4">
|
||||||
@@ -56,6 +67,7 @@ const ContactForm = () => {
|
|||||||
value={formFields.name}
|
value={formFields.name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setFormFields({ ...formFields, name: e.target.value });
|
setFormFields({ ...formFields, name: e.target.value });
|
||||||
|
setTouched((t) => ({ ...t, name: true }));
|
||||||
}}
|
}}
|
||||||
onBlur={() => setTouched((t) => ({ ...t, name: true }))}
|
onBlur={() => setTouched((t) => ({ ...t, name: true }))}
|
||||||
disabled={pending || formState.success}
|
disabled={pending || formState.success}
|
||||||
@@ -73,6 +85,7 @@ const ContactForm = () => {
|
|||||||
value={formFields.email}
|
value={formFields.email}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setFormFields({ ...formFields, email: e.target.value });
|
setFormFields({ ...formFields, email: e.target.value });
|
||||||
|
setTouched((t) => ({ ...t, email: true }));
|
||||||
}}
|
}}
|
||||||
onBlur={() => setTouched((t) => ({ ...t, email: true }))}
|
onBlur={() => setTouched((t) => ({ ...t, email: true }))}
|
||||||
disabled={pending || formState.success}
|
disabled={pending || formState.success}
|
||||||
@@ -88,6 +101,7 @@ const ContactForm = () => {
|
|||||||
value={formFields.message}
|
value={formFields.message}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setFormFields({ ...formFields, message: e.target.value });
|
setFormFields({ ...formFields, message: e.target.value });
|
||||||
|
setTouched((t) => ({ ...t, message: true }));
|
||||||
}}
|
}}
|
||||||
onBlur={() => setTouched((t) => ({ ...t, message: true }))}
|
onBlur={() => setTouched((t) => ({ ...t, message: true }))}
|
||||||
disabled={pending || formState.success}
|
disabled={pending || formState.success}
|
||||||
|
|||||||
File renamed without changes.
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { env } from "@/lib/env";
|
import { env } from "@/lib/env";
|
||||||
import { Resend } from "resend";
|
import { Resend } from "resend";
|
||||||
import { ContactSchema } from "@/lib/validation/contact";
|
import { ContactSchema } from "@/lib/schemas/contact";
|
||||||
import siteConfig from "@/lib/config/site";
|
import siteConfig from "@/lib/config/site";
|
||||||
import { checkBotId } from "botid/server";
|
import { checkBotId } from "botid/server";
|
||||||
|
|
||||||
Reference in new issue
Block a user