Refactor(contact us): make code more clean

This commit is contained in:
vergiLgood1 2025-02-19 23:28:15 +07:00
parent 55f295cee3
commit 2b61c97cb4
12 changed files with 442 additions and 301 deletions

View File

@ -1,19 +1,11 @@
"use server";
import { Resend } from "resend";
import { TValidator } from "@/utils/validator";
import AdminNotification from "@/components/email-templates/admin-notification";
import { render } from "@react-email/components";
import UserConfirmation from "@/components/email-templates/user-confirmation";
import { createClient } from "@/utils/supabase/server";
import { useResend } from "@/hooks/use-resend";
const typeMessageMap: Record<string, string> = {
"1": "Request to become a user",
"2": "OTP problem",
"3": "Request for a feature",
"4": "Other",
};
import { typeMessageMap } from "@/src/applications/entities/models/contact-us.model";
export async function sendContactEmail(formData: {
name: string;
@ -27,17 +19,9 @@ export async function sendContactEmail(formData: {
const supabase = await createClient();
const { resend } = useResend();
// Validate form data
const validation = TValidator.validateContactForm(formData);
if (!validation.success) {
return {
success: false,
errors: validation.errors,
};
}
// Get message type label
const messageTypeLabel = typeMessageMap[formData.typeMessage] || "Unknown";
const messageTypeLabel =
typeMessageMap.get(formData.typeMessage) || "Unknown";
// Save to Supabase
const { data: contactData, error: contactError } = await supabase
@ -76,7 +60,7 @@ export async function sendContactEmail(formData: {
// Send email to admin
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"],
subject: `New Contact Form Submission: ${messageTypeLabel}`,
html: adminEmailHtml,
@ -98,7 +82,7 @@ export async function sendContactEmail(formData: {
// Send confirmation email to user
const { data: confirmationData, error: confirmationError } =
await resend.emails.send({
from: "Your Company <noreply@backspacex.tech>",
from: "Your Company <support@backspacex.tech>",
to: [formData.email],
subject: "Thank you for contacting us",
html: userEmailHtml,

View File

@ -1,8 +1,6 @@
"use client";
import * as React from "react";
import { useState } from "react";
import {
Card,
CardContent,
@ -12,7 +10,6 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
@ -23,123 +20,22 @@ import {
import { Textarea } from "../ui/textarea";
import { SubmitButton } from "../submit-button";
import Link from "next/link";
import { toast } from "@/hooks/use-toast";
import { sendContactEmail } from "@/actions/auth/contact-us";
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() {
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" },
];
// State untuk form data
const [formData, setFormData] = useState({
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);
}
};
const {
formData,
errors,
isSubmitting,
setFormData,
handleChange,
handleSelectChange,
handleSubmit,
} = useContactForm();
return (
<Card className="w-[500px] bg-[#171717] border-none text-white">
@ -150,11 +46,10 @@ export function ContactUsForm() {
</CardDescription>
</CardHeader>
<CardContent>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2">
<Label htmlFor="name" className="text-sm text-gray-300">
Name
</Label>
<form onSubmit={handleSubmit} className="space-y-2">
<FormField
label="Name"
input={
<Input
id="name"
placeholder="John doe"
@ -165,14 +60,12 @@ export function ContactUsForm() {
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>
}
error={errors.name}
/>
<FormField
label="Email"
input={
<Input
id="email"
placeholder="example@gmail.com"
@ -183,14 +76,12 @@ export function ContactUsForm() {
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>
}
error={errors.email}
/>
<FormField
label="Phone"
input={
<Input
id="phone"
placeholder="08123456789"
@ -201,14 +92,12 @@ export function ContactUsForm() {
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>
}
error={errors.phone}
/>
<FormField
label="Type message"
input={
<Select
value={formData.typeMessage}
onValueChange={handleSelectChange}
@ -234,14 +123,12 @@ export function ContactUsForm() {
))}
</SelectContent>
</Select>
{errors.typeMessage && (
<p className="text-red-500 text-xs mt-1">{errors.typeMessage}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="message" className="text-sm text-gray-300">
Message
</Label>
}
error={errors.typeMessage}
/>
<FormField
label="Message"
input={
<Textarea
id="message"
placeholder="Your message here..."
@ -252,17 +139,16 @@ export function ContactUsForm() {
onChange={handleChange}
disabled={isSubmitting}
/>
{errors.message && (
<p className="text-red-500 text-xs mt-1">{errors.message}</p>
)}
</div>
}
error={errors.message}
/>
<CardFooter className="flex flex-col items-center space-y-4 px-0">
<SubmitButton
type="submit"
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
disabled={isSubmitting}
pendingText="Sending..."
>
{isSubmitting ? "Sending..." : "Send"}
Send
</SubmitButton>
<div className="text-center text-lg space-x-2">
<span className="text-gray-400">Already have an account?</span>

View File

@ -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>
);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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>;
}

View File

@ -0,0 +1,9 @@
import {
ContactUs,
ContactUsInsert,
ContactUsResponse,
} from "../entities/models/contact-us.model";
export interface ContactUsRepository {
createContactUs(contact: ContactUsInsert): Promise<ContactUsResponse>;
}

View File

@ -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);
}
}

View File

@ -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,
};
};

View File

@ -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.",
};
}
}
}

View File

@ -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,
};
};

View File

@ -81,62 +81,5 @@ export class TValidator {
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,
};
}
}