mirror of
https://github.com/OwethuManagedServices/oms-website-nextjs.git
synced 2025-12-17 18:58:10 +00:00
246 lines
7.8 KiB
TypeScript
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>
|
|
);
|
|
}
|