Files
oms-website-nextjs/components/ContactForm.tsx
2025-04-27 12:47:08 +02:00

246 lines
7.8 KiB
TypeScript

"use client";
import React, { useEffect, useActionState } from "react"; // Removed useRef
import { useForm } from "react-hook-form"; // Removed SubmitHandler
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { submitContactForm, ContactFormState } from "@/actions/contact";
import Button from "@/components/ui/Button";
// Zod schema for contact form validation
const contactSchema = z.object({
name: z.string().min(2, "Full name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
subject: z.string().min(3, "Subject must be at least 3 characters"),
message: z.string().min(10, "Message must be at least 10 characters"),
});
type ContactFormData = z.infer<typeof contactSchema>;
export default function ContactForm() {
// React Hook Form setup
const {
register,
formState: { errors, isValid },
reset,
} = useForm<ContactFormData>({
resolver: zodResolver(contactSchema),
mode: "onChange",
});
const initialState: ContactFormState = {
message: null,
errors: {},
success: false,
};
const [state, formAction] = useActionState(submitContactForm, initialState); // Renamed dispatch to formAction for clarity
// Reset form when server action reports success
useEffect(() => {
if (state.success) {
reset();
}
}, [state.success, reset]);
// Removed onSubmit handler
// Pass formAction directly to the form's action prop
// Remove onSubmit={handleSubmit(onSubmit)}
return (
<form action={formAction} className="space-y-6">
{/* Name Field */}
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-foreground mb-1"
>
Full Name
</label>
<input
id="name"
type="text"
{...register("name")}
aria-invalid={errors.name ? "true" : "false"}
aria-describedby="name-error"
className={`block w-full px-4 py-2 border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:placeholder-gray-400 ${
errors.name
? "border-destructive"
: "border-border dark:border-gray-600"
}`}
/>
{/* Client-side error */}
{errors.name && (
<p
id="name-error"
role="alert"
className="mt-1 text-sm text-destructive dark:text-red-400"
>
{errors.name.message}
</p>
)}
{/* Server-side error */}
{state.errors?.name &&
state.errors.name.map((err: string) => (
<p
key={err}
className="mt-1 text-sm text-destructive dark:text-red-400"
>
{err}
</p>
))}
</div>
{/* Email Field */}
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-foreground mb-1"
>
Email Address
</label>
<input
id="email"
type="email"
{...register("email")}
aria-invalid={errors.email ? "true" : "false"}
aria-describedby="email-error"
className={`block w-full px-4 py-2 border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:placeholder-gray-400 ${
errors.email
? "border-destructive"
: "border-border dark:border-gray-600"
}`}
/>
{errors.email && (
<p
id="email-error"
role="alert"
className="mt-1 text-sm text-destructive dark:text-red-400"
>
{errors.email.message}
</p>
)}
{state.errors?.email &&
state.errors.email.map((err: string) => (
<p
key={err}
className="mt-1 text-sm text-destructive dark:text-red-400"
>
{err}
</p>
))}
</div>
{/* Subject Field */}
<div>
<label
htmlFor="subject"
className="block text-sm font-medium text-foreground mb-1"
>
Subject
</label>
<input
id="subject"
type="text"
{...register("subject")}
aria-invalid={errors.subject ? "true" : "false"}
aria-describedby="subject-error"
className={`block w-full px-4 py-2 border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:placeholder-gray-400 ${
errors.subject
? "border-destructive"
: "border-border dark:border-gray-600"
}`}
/>
{errors.subject && (
<p
id="subject-error"
role="alert"
className="mt-1 text-sm text-destructive dark:text-red-400"
>
{errors.subject.message}
</p>
)}
{state.errors?.subject &&
state.errors.subject.map((err: string) => (
<p
key={err}
className="mt-1 text-sm text-destructive dark:text-red-400"
>
{err}
</p>
))}
</div>
{/* Message Field */}
<div>
<label
htmlFor="message"
className="block text-sm font-medium text-foreground mb-1"
>
Your Message
</label>
<textarea
id="message"
rows={5}
{...register("message")}
aria-invalid={errors.message ? "true" : "false"}
aria-describedby="message-error"
className={`block w-full px-4 py-2 border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground resize-vertical dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:placeholder-gray-400 ${
errors.message
? "border-destructive"
: "border-border dark:border-gray-600"
}`}
/>
{errors.message && (
<p
id="message-error"
role="alert"
className="mt-1 text-sm text-destructive dark:text-red-400"
>
{errors.message.message}
</p>
)}
{state.errors?.message &&
state.errors.message.map((err: string) => (
<p
key={err}
className="mt-1 text-sm text-destructive dark:text-red-400"
>
{err}
</p>
))}
</div>
{/* General Form Response */}
{state.message && (
<p
className={`text-sm ${
state.success
? "text-green-600 dark:text-green-400"
: "text-destructive dark:text-red-400"
}`}
>
{state.message}
</p>
)}
{/* Submit Button */}
{/* isSubmitting from useFormState is now implicitly handled by the form action */}
<div className="pt-2">
<Button
type="submit"
variant="primary"
size="lg"
// Disable based on client-side validation only before submission starts
// The form handles disabling during submission automatically
disabled={!isValid}
// aria-disabled={pending} // You might need to import useFormStatus for pending state
>
{/* {pending ? "Sending..." : "Send Message"} // Use useFormStatus for pending state */}
Send Message{" "}
{/* Simplified button text, pending state handled by browser/React */}
</Button>
</div>
</form>
);
}