Refactor(contact us): make code more clean
This commit is contained in:
parent
55f295cee3
commit
2b61c97cb4
|
@ -1,19 +1,11 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { Resend } from "resend";
|
|
||||||
import { TValidator } from "@/utils/validator";
|
|
||||||
import AdminNotification from "@/components/email-templates/admin-notification";
|
import AdminNotification from "@/components/email-templates/admin-notification";
|
||||||
import { render } from "@react-email/components";
|
import { render } from "@react-email/components";
|
||||||
import UserConfirmation from "@/components/email-templates/user-confirmation";
|
import UserConfirmation from "@/components/email-templates/user-confirmation";
|
||||||
import { createClient } from "@/utils/supabase/server";
|
import { createClient } from "@/utils/supabase/server";
|
||||||
import { useResend } from "@/hooks/use-resend";
|
import { useResend } from "@/hooks/use-resend";
|
||||||
|
import { typeMessageMap } from "@/src/applications/entities/models/contact-us.model";
|
||||||
const typeMessageMap: Record<string, string> = {
|
|
||||||
"1": "Request to become a user",
|
|
||||||
"2": "OTP problem",
|
|
||||||
"3": "Request for a feature",
|
|
||||||
"4": "Other",
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function sendContactEmail(formData: {
|
export async function sendContactEmail(formData: {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -27,17 +19,9 @@ export async function sendContactEmail(formData: {
|
||||||
const supabase = await createClient();
|
const supabase = await createClient();
|
||||||
const { resend } = useResend();
|
const { resend } = useResend();
|
||||||
|
|
||||||
// Validate form data
|
|
||||||
const validation = TValidator.validateContactForm(formData);
|
|
||||||
if (!validation.success) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
errors: validation.errors,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get message type label
|
// Get message type label
|
||||||
const messageTypeLabel = typeMessageMap[formData.typeMessage] || "Unknown";
|
const messageTypeLabel =
|
||||||
|
typeMessageMap.get(formData.typeMessage) || "Unknown";
|
||||||
|
|
||||||
// Save to Supabase
|
// Save to Supabase
|
||||||
const { data: contactData, error: contactError } = await supabase
|
const { data: contactData, error: contactError } = await supabase
|
||||||
|
@ -76,7 +60,7 @@ export async function sendContactEmail(formData: {
|
||||||
|
|
||||||
// Send email to admin
|
// Send email to admin
|
||||||
const { data: emailData, error: emailError } = await resend.emails.send({
|
const { data: emailData, error: emailError } = await resend.emails.send({
|
||||||
from: "Contact Form <noreply@backspacex.tech>",
|
from: "Contact Form <contact@backspacex.tech>",
|
||||||
to: ["xdamazon17@gmail.com"],
|
to: ["xdamazon17@gmail.com"],
|
||||||
subject: `New Contact Form Submission: ${messageTypeLabel}`,
|
subject: `New Contact Form Submission: ${messageTypeLabel}`,
|
||||||
html: adminEmailHtml,
|
html: adminEmailHtml,
|
||||||
|
@ -98,7 +82,7 @@ export async function sendContactEmail(formData: {
|
||||||
// Send confirmation email to user
|
// Send confirmation email to user
|
||||||
const { data: confirmationData, error: confirmationError } =
|
const { data: confirmationData, error: confirmationError } =
|
||||||
await resend.emails.send({
|
await resend.emails.send({
|
||||||
from: "Your Company <noreply@backspacex.tech>",
|
from: "Your Company <support@backspacex.tech>",
|
||||||
to: [formData.email],
|
to: [formData.email],
|
||||||
subject: "Thank you for contacting us",
|
subject: "Thank you for contacting us",
|
||||||
html: userEmailHtml,
|
html: userEmailHtml,
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
@ -12,7 +10,6 @@ import {
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
@ -23,123 +20,22 @@ import {
|
||||||
import { Textarea } from "../ui/textarea";
|
import { Textarea } from "../ui/textarea";
|
||||||
import { SubmitButton } from "../submit-button";
|
import { SubmitButton } from "../submit-button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { toast } from "@/hooks/use-toast";
|
|
||||||
import { sendContactEmail } from "@/actions/auth/contact-us";
|
|
||||||
import { TValidator } from "@/utils/validator";
|
import { TValidator } from "@/utils/validator";
|
||||||
|
import { useContactForm } from "@/src/infrastructure/hooks/use-contact-us-form";
|
||||||
|
import { FormField } from "../form-field";
|
||||||
|
import { typeMessage } from "@/src/applications/entities/models/contact-us.model";
|
||||||
|
import { Form } from "../ui/form";
|
||||||
|
|
||||||
export function ContactUsForm() {
|
export function ContactUsForm() {
|
||||||
const typeMessage = [
|
const {
|
||||||
{ value: "1", label: "Request to become a user" },
|
formData,
|
||||||
{ value: "2", label: "OTP problem" },
|
errors,
|
||||||
{ value: "3", label: "Request for a feature" },
|
isSubmitting,
|
||||||
{ value: "4", label: "Other" },
|
setFormData,
|
||||||
];
|
handleChange,
|
||||||
|
handleSelectChange,
|
||||||
// State untuk form data
|
handleSubmit,
|
||||||
const [formData, setFormData] = useState({
|
} = useContactForm();
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
phone: "",
|
|
||||||
typeMessage: "",
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
// State untuk error messages
|
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
||||||
// Loading state
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
|
|
||||||
// Handle input change
|
|
||||||
const handleChange = (
|
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
||||||
) => {
|
|
||||||
const { id, value } = e.target;
|
|
||||||
setFormData((prev) => ({ ...prev, [id]: value }));
|
|
||||||
|
|
||||||
// Clear error when typing
|
|
||||||
if (errors[id]) {
|
|
||||||
setErrors((prev) => {
|
|
||||||
const newErrors = { ...prev };
|
|
||||||
delete newErrors[id];
|
|
||||||
return newErrors;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle select change
|
|
||||||
const handleSelectChange = (value: string) => {
|
|
||||||
setFormData((prev) => ({ ...prev, typeMessage: value }));
|
|
||||||
|
|
||||||
// Clear error when selecting
|
|
||||||
if (errors.typeMessage) {
|
|
||||||
setErrors((prev) => {
|
|
||||||
const newErrors = { ...prev };
|
|
||||||
delete newErrors.typeMessage;
|
|
||||||
return newErrors;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Client-side validation
|
|
||||||
const validation = TValidator.validateContactForm(formData);
|
|
||||||
|
|
||||||
if (!validation.success) {
|
|
||||||
setErrors(validation.errors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
// Call server action to send email
|
|
||||||
const result = await sendContactEmail(formData);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
// Handle server-side validation errors
|
|
||||||
if (result.errors) {
|
|
||||||
setErrors(result.errors);
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description:
|
|
||||||
result.error || "Failed to send message. Please try again.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success!
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description:
|
|
||||||
result.message || "Your message has been sent successfully!",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset form and errors after successful submission
|
|
||||||
setFormData({
|
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
phone: "",
|
|
||||||
typeMessage: "",
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
setErrors({});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error submitting form:", error);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "An unexpected error occurred. Please try again later.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-[500px] bg-[#171717] border-none text-white">
|
<Card className="w-[500px] bg-[#171717] border-none text-white">
|
||||||
|
@ -150,119 +46,109 @@ export function ContactUsForm() {
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit} className="space-y-2">
|
||||||
<div className="space-y-2">
|
<FormField
|
||||||
<Label htmlFor="name" className="text-sm text-gray-300">
|
label="Name"
|
||||||
Name
|
input={
|
||||||
</Label>
|
<Input
|
||||||
<Input
|
id="name"
|
||||||
id="name"
|
placeholder="John doe"
|
||||||
placeholder="John doe"
|
className={`bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
|
||||||
className={`bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
|
errors.name ? "border-red-500" : ""
|
||||||
errors.name ? "border-red-500" : ""
|
|
||||||
}`}
|
|
||||||
value={formData.name}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
{errors.name && (
|
|
||||||
<p className="text-red-500 text-xs mt-1">{errors.name}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="email" className="text-sm text-gray-300">
|
|
||||||
Email
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
placeholder="example@gmail.com"
|
|
||||||
className={`bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
|
|
||||||
errors.email ? "border-red-500" : ""
|
|
||||||
}`}
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
{errors.email && (
|
|
||||||
<p className="text-red-500 text-xs mt-1">{errors.email}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="phone" className="text-sm text-gray-300">
|
|
||||||
Phone
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="phone"
|
|
||||||
placeholder="08123456789"
|
|
||||||
className={`bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
|
|
||||||
errors.phone ? "border-red-500" : ""
|
|
||||||
}`}
|
|
||||||
value={formData.phone}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
{errors.phone && (
|
|
||||||
<p className="text-red-500 text-xs mt-1">{errors.phone}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="typemessage" className="text-sm text-gray-300">
|
|
||||||
Type message
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.typeMessage}
|
|
||||||
onValueChange={handleSelectChange}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="typemessage"
|
|
||||||
className={`bg-[#1C1C1C] border-gray-800 text-white focus:border-emerald-600 focus:ring-emerald-600 ${
|
|
||||||
errors.typeMessage ? "border-red-500" : ""
|
|
||||||
}`}
|
}`}
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
error={errors.name}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
label="Email"
|
||||||
|
input={
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
placeholder="example@gmail.com"
|
||||||
|
className={`bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
|
||||||
|
errors.email ? "border-red-500" : ""
|
||||||
|
}`}
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
error={errors.email}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
label="Phone"
|
||||||
|
input={
|
||||||
|
<Input
|
||||||
|
id="phone"
|
||||||
|
placeholder="08123456789"
|
||||||
|
className={`bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
|
||||||
|
errors.phone ? "border-red-500" : ""
|
||||||
|
}`}
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
error={errors.phone}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
label="Type message"
|
||||||
|
input={
|
||||||
|
<Select
|
||||||
|
value={formData.typeMessage}
|
||||||
|
onValueChange={handleSelectChange}
|
||||||
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
<SelectValue placeholder="Select" />
|
<SelectTrigger
|
||||||
</SelectTrigger>
|
id="typemessage"
|
||||||
<SelectContent className="bg-[#1C1C1C] border-gray-800 text-white">
|
className={`bg-[#1C1C1C] border-gray-800 text-white focus:border-emerald-600 focus:ring-emerald-600 ${
|
||||||
{typeMessage.map((message) => (
|
errors.typeMessage ? "border-red-500" : ""
|
||||||
<SelectItem
|
}`}
|
||||||
key={message.value}
|
>
|
||||||
value={message.value}
|
<SelectValue placeholder="Select" />
|
||||||
className="focus:bg-emerald-600 focus:text-white"
|
</SelectTrigger>
|
||||||
>
|
<SelectContent className="bg-[#1C1C1C] border-gray-800 text-white">
|
||||||
{message.label}
|
{typeMessage.map((message) => (
|
||||||
</SelectItem>
|
<SelectItem
|
||||||
))}
|
key={message.value}
|
||||||
</SelectContent>
|
value={message.value}
|
||||||
</Select>
|
className="focus:bg-emerald-600 focus:text-white"
|
||||||
{errors.typeMessage && (
|
>
|
||||||
<p className="text-red-500 text-xs mt-1">{errors.typeMessage}</p>
|
{message.label}
|
||||||
)}
|
</SelectItem>
|
||||||
</div>
|
))}
|
||||||
<div className="space-y-2">
|
</SelectContent>
|
||||||
<Label htmlFor="message" className="text-sm text-gray-300">
|
</Select>
|
||||||
Message
|
}
|
||||||
</Label>
|
error={errors.typeMessage}
|
||||||
<Textarea
|
/>
|
||||||
id="message"
|
<FormField
|
||||||
placeholder="Your message here..."
|
label="Message"
|
||||||
className={`resize-none h-24 bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
|
input={
|
||||||
errors.message ? "border-red-500" : ""
|
<Textarea
|
||||||
}`}
|
id="message"
|
||||||
value={formData.message}
|
placeholder="Your message here..."
|
||||||
onChange={handleChange}
|
className={`resize-none h-24 bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
|
||||||
disabled={isSubmitting}
|
errors.message ? "border-red-500" : ""
|
||||||
/>
|
}`}
|
||||||
{errors.message && (
|
value={formData.message}
|
||||||
<p className="text-red-500 text-xs mt-1">{errors.message}</p>
|
onChange={handleChange}
|
||||||
)}
|
disabled={isSubmitting}
|
||||||
</div>
|
/>
|
||||||
|
}
|
||||||
|
error={errors.message}
|
||||||
|
/>
|
||||||
<CardFooter className="flex flex-col items-center space-y-4 px-0">
|
<CardFooter className="flex flex-col items-center space-y-4 px-0">
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
type="submit"
|
|
||||||
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
|
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
pendingText="Sending..."
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Sending..." : "Send"}
|
Send
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
<div className="text-center text-lg space-x-2">
|
<div className="text-center text-lg space-x-2">
|
||||||
<span className="text-gray-400">Already have an account?</span>
|
<span className="text-gray-400">Already have an account?</span>
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Label } from "./ui/label";
|
||||||
|
|
||||||
|
interface FormFieldProps {
|
||||||
|
label: string;
|
||||||
|
input: React.ReactNode;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormField({ label, input, error }: FormFieldProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm text-gray-300">{label}</Label>
|
||||||
|
{input}
|
||||||
|
{error && <p className="text-red-500 text-xs mt-1">{error}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
export class AuthenticationError extends Error {
|
||||||
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnauthenticatedError extends Error {
|
||||||
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnauthorizedError extends Error {
|
||||||
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
export class DatabaseOperationError extends Error {
|
||||||
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotFoundError extends Error {
|
||||||
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InputParseError extends Error {
|
||||||
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Define the message type mapping
|
||||||
|
export const typeMessage = [
|
||||||
|
{ value: "1", label: "Request to become a user" },
|
||||||
|
{ value: "2", label: "OTP problem" },
|
||||||
|
{ value: "3", label: "Request for a feature" },
|
||||||
|
{ value: "4", label: "Other" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const typeMessageMap = new Map(
|
||||||
|
typeMessage.map((item) => [item.value, item.label])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const statusEnum = {
|
||||||
|
NEW: "new",
|
||||||
|
READ: "read",
|
||||||
|
REPLIED: "replied",
|
||||||
|
RESOLVED: "resolved",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StatusEnum = typeof statusEnum;
|
||||||
|
|
||||||
|
// Schema for what's stored in Supabase
|
||||||
|
export const selectContactUsSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
phone: z.string(),
|
||||||
|
message_type: z.string(),
|
||||||
|
message_type_label: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
status: z.nativeEnum(statusEnum),
|
||||||
|
created_at: z.string(),
|
||||||
|
updated_at: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ContactUs = z.infer<typeof selectContactUsSchema>;
|
||||||
|
|
||||||
|
// Schema for form input
|
||||||
|
export const insertContactUsSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
phone: z.string(),
|
||||||
|
typeMessage: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ContactUsInsert = z.infer<typeof insertContactUsSchema>;
|
||||||
|
|
||||||
|
// Type for the response from the server action
|
||||||
|
export interface ContactUsResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
errors?: Record<string, string>;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {
|
||||||
|
ContactUs,
|
||||||
|
ContactUsInsert,
|
||||||
|
ContactUsResponse,
|
||||||
|
} from "../entities/models/contact-us.model";
|
||||||
|
|
||||||
|
export interface ContactUsRepository {
|
||||||
|
createContactUs(contact: ContactUsInsert): Promise<ContactUsResponse>;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import {
|
||||||
|
ContactUs,
|
||||||
|
ContactUsInsert,
|
||||||
|
ContactUsResponse,
|
||||||
|
} from "../../entities/models/contact-us.model";
|
||||||
|
import { ContactUsRepository } from "../../repositories/contact-us.repository";
|
||||||
|
|
||||||
|
export class CreateContactUseCase {
|
||||||
|
constructor(private contactRepository: ContactUsRepository) {}
|
||||||
|
|
||||||
|
async execute(contact: ContactUsInsert): Promise<ContactUsResponse> {
|
||||||
|
return this.contactRepository.createContactUs(contact);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import { ContactUsInsert } from "@/src/applications/entities/models/contact-us.model";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ContactRepositoryImpl } from "../repositories/contact-us.repository.impl";
|
||||||
|
import { validateContactForm } from "../validators/contact-us.validator";
|
||||||
|
|
||||||
|
export const useContactForm = () => {
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [formData, setFormData] = useState<ContactUsInsert>({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
typeMessage: "",
|
||||||
|
message: "",
|
||||||
|
});
|
||||||
|
const [errors, setErrors] = useState<
|
||||||
|
Partial<Record<keyof ContactUsInsert, string>>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const contactRepository = new ContactRepositoryImpl();
|
||||||
|
|
||||||
|
// Handle input change
|
||||||
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
|
const { id, value } = e.target;
|
||||||
|
setFormData((prev) => ({ ...prev, [id]: value }));
|
||||||
|
|
||||||
|
if (errors[id as keyof ContactUsInsert]) {
|
||||||
|
setErrors((prev) => {
|
||||||
|
const newErrors = { ...prev };
|
||||||
|
delete newErrors[id as keyof ContactUsInsert];
|
||||||
|
return newErrors;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle select change
|
||||||
|
const handleSelectChange = (value: string) => {
|
||||||
|
setFormData((prev) => ({ ...prev, typeMessage: value }));
|
||||||
|
|
||||||
|
// Clear error when selecting
|
||||||
|
if (errors.typeMessage) {
|
||||||
|
setErrors((prev) => {
|
||||||
|
const newErrors = { ...prev };
|
||||||
|
delete newErrors.typeMessage;
|
||||||
|
return newErrors;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Client-side validation
|
||||||
|
const validation = validateContactForm(formData);
|
||||||
|
|
||||||
|
if (!validation.success) {
|
||||||
|
setErrors(validation.errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await contactRepository.createContactUs(formData);
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
if (response.errors) {
|
||||||
|
setErrors(response.errors);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: response.error || "Failed to send message",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description:
|
||||||
|
response.message || "Your message has been sent successfully!",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setFormData({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
typeMessage: "",
|
||||||
|
message: "",
|
||||||
|
});
|
||||||
|
setErrors({});
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "An unexpected error occurred. Please try again later.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
formData,
|
||||||
|
errors,
|
||||||
|
isSubmitting,
|
||||||
|
setFormData,
|
||||||
|
handleChange,
|
||||||
|
handleSelectChange,
|
||||||
|
handleSubmit,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
// src/infrastructure/repositories/contact-us.repository.impl.ts
|
||||||
|
import { sendContactEmail } from "@/actions/auth/contact-us";
|
||||||
|
import {
|
||||||
|
ContactUsInsert,
|
||||||
|
ContactUsResponse,
|
||||||
|
} from "@/src/applications/entities/models/contact-us.model";
|
||||||
|
import { ContactUsRepository } from "@/src/applications/repositories/contact-us.repository";
|
||||||
|
|
||||||
|
export class ContactRepositoryImpl implements ContactUsRepository {
|
||||||
|
async createContactUs(contact: ContactUsInsert): Promise<ContactUsResponse> {
|
||||||
|
try {
|
||||||
|
return await sendContactEmail(contact);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in ContactRepositoryImpl:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "An unexpected error occurred. Please try again later.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { ContactUsInsert } from "@/src/applications/entities/models/contact-us.model";
|
||||||
|
import { TValidator } from "@/utils/validator";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the contact form
|
||||||
|
* @param {Object} formData - The form data to validate
|
||||||
|
* @returns {Object} - Validation result with success flag and errors object
|
||||||
|
*/
|
||||||
|
export const validateContactForm = (
|
||||||
|
formData: ContactUsInsert
|
||||||
|
): { success: boolean; errors: Record<string, string> } => {
|
||||||
|
const errors: Record<string, string> = {};
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
// Validate name
|
||||||
|
const nameResult = TValidator.validateEmptyValue(formData.name, "Name");
|
||||||
|
if (!nameResult.success) {
|
||||||
|
errors.name = nameResult.error!;
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email
|
||||||
|
const emailResult = TValidator.validateEmail(formData.email);
|
||||||
|
if (!emailResult.success) {
|
||||||
|
errors.email = emailResult.error!;
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate phone
|
||||||
|
const phoneResult = TValidator.validatePhone(formData.phone);
|
||||||
|
if (!phoneResult.success) {
|
||||||
|
errors.phone = phoneResult.error!;
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate type message
|
||||||
|
const typeMessageResult = TValidator.validateEmptyValue(
|
||||||
|
formData.typeMessage,
|
||||||
|
"Type message"
|
||||||
|
);
|
||||||
|
if (!typeMessageResult.success) {
|
||||||
|
errors.typeMessage = typeMessageResult.error!;
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate message
|
||||||
|
const messageResult = TValidator.validateEmptyValue(
|
||||||
|
formData.message,
|
||||||
|
"Message"
|
||||||
|
);
|
||||||
|
if (!messageResult.success) {
|
||||||
|
errors.message = messageResult.error!;
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: isValid,
|
||||||
|
errors: isValid ? {} : errors,
|
||||||
|
};
|
||||||
|
};
|
|
@ -81,62 +81,5 @@ export class TValidator {
|
||||||
success: true,
|
success: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Validate the contact form
|
|
||||||
* @param {Object} formData - The form data to validate
|
|
||||||
* @returns {Object} - Validation result with success flag and errors object
|
|
||||||
*/
|
|
||||||
static validateContactForm(formData: {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
typeMessage: string;
|
|
||||||
message: string;
|
|
||||||
}) {
|
|
||||||
const errors: Record<string, string> = {};
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
// Validate name
|
|
||||||
const nameResult = this.validateEmptyValue(formData.name, "Name");
|
|
||||||
if (!nameResult.success) {
|
|
||||||
errors.name = nameResult.error!;
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate email
|
|
||||||
const emailResult = this.validateEmail(formData.email);
|
|
||||||
if (!emailResult.success) {
|
|
||||||
errors.email = emailResult.error!;
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate phone
|
|
||||||
const phoneResult = this.validatePhone(formData.phone);
|
|
||||||
if (!phoneResult.success) {
|
|
||||||
errors.phone = phoneResult.error!;
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate type message
|
|
||||||
const typeMessageResult = this.validateEmptyValue(
|
|
||||||
formData.typeMessage,
|
|
||||||
"Type message"
|
|
||||||
);
|
|
||||||
if (!typeMessageResult.success) {
|
|
||||||
errors.typeMessage = typeMessageResult.error!;
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate message
|
|
||||||
const messageResult = this.validateEmptyValue(formData.message, "Message");
|
|
||||||
if (!messageResult.success) {
|
|
||||||
errors.message = messageResult.error!;
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: isValid,
|
|
||||||
errors: isValid ? {} : errors,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue