feat(contact us): Send email function
Send email to admin and get the confirmation message from admin
This commit is contained in:
parent
8137c58cd8
commit
55f295cee3
|
@ -0,0 +1,123 @@
|
|||
"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",
|
||||
};
|
||||
|
||||
export async function sendContactEmail(formData: {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
typeMessage: string;
|
||||
message: string;
|
||||
}) {
|
||||
try {
|
||||
// Initialize Supabase
|
||||
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";
|
||||
|
||||
// Save to Supabase
|
||||
const { data: contactData, error: contactError } = await supabase
|
||||
.from("contact_messages")
|
||||
.insert([
|
||||
{
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
message_type: formData.typeMessage,
|
||||
message_type_label: messageTypeLabel,
|
||||
message: formData.message,
|
||||
status: "new",
|
||||
},
|
||||
])
|
||||
.select();
|
||||
|
||||
if (contactError) {
|
||||
console.error("Error saving contact message to Supabase:", contactError);
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to save your message. Please try again later.",
|
||||
};
|
||||
}
|
||||
|
||||
// Render admin email template
|
||||
const adminEmailHtml = await render(
|
||||
AdminNotification({
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
messageType: messageTypeLabel,
|
||||
message: formData.message,
|
||||
})
|
||||
);
|
||||
|
||||
// Send email to admin
|
||||
const { data: emailData, error: emailError } = await resend.emails.send({
|
||||
from: "Contact Form <noreply@backspacex.tech>",
|
||||
to: ["xdamazon17@gmail.com"],
|
||||
subject: `New Contact Form Submission: ${messageTypeLabel}`,
|
||||
html: adminEmailHtml,
|
||||
});
|
||||
|
||||
if (emailError) {
|
||||
console.error("Error sending email via Resend:", emailError);
|
||||
// Note: We don't return error here since the data is already saved to Supabase
|
||||
}
|
||||
|
||||
const userEmailHtml = await render(
|
||||
UserConfirmation({
|
||||
name: formData.name,
|
||||
messageType: messageTypeLabel,
|
||||
message: formData.message,
|
||||
})
|
||||
);
|
||||
|
||||
// Send confirmation email to user
|
||||
const { data: confirmationData, error: confirmationError } =
|
||||
await resend.emails.send({
|
||||
from: "Your Company <noreply@backspacex.tech>",
|
||||
to: [formData.email],
|
||||
subject: "Thank you for contacting us",
|
||||
html: userEmailHtml,
|
||||
});
|
||||
|
||||
if (confirmationError) {
|
||||
console.error("Error sending confirmation email:", confirmationError);
|
||||
// Note: We don't return error here either
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Your message has been sent successfully!",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Unexpected error in sendContactEmail:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: "An unexpected error occurred. Please try again later.",
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
import * as React from "react";
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
@ -21,6 +23,9 @@ 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";
|
||||
|
||||
export function ContactUsForm() {
|
||||
const typeMessage = [
|
||||
|
@ -30,6 +35,112 @@ export function ContactUsForm() {
|
|||
{ 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);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-[500px] bg-[#171717] border-none text-white">
|
||||
<CardHeader>
|
||||
|
@ -39,7 +150,7 @@ export function ContactUsForm() {
|
|||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="text-sm text-gray-300">
|
||||
Name
|
||||
|
@ -47,8 +158,16 @@ export function ContactUsForm() {
|
|||
<Input
|
||||
id="name"
|
||||
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" : ""
|
||||
}`}
|
||||
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">
|
||||
|
@ -57,8 +176,16 @@ export function ContactUsForm() {
|
|||
<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"
|
||||
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">
|
||||
|
@ -66,18 +193,32 @@ export function ContactUsForm() {
|
|||
</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
placeholder="085255xxx"
|
||||
className="bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600"
|
||||
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>
|
||||
<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"
|
||||
className={`bg-[#1C1C1C] border-gray-800 text-white focus:border-emerald-600 focus:ring-emerald-600 ${
|
||||
errors.typeMessage ? "border-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
|
@ -93,6 +234,9 @@ 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">
|
||||
|
@ -100,25 +244,38 @@ export function ContactUsForm() {
|
|||
</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
name="message"
|
||||
placeholder="Your message here..."
|
||||
className="resize-none h-24 bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600"
|
||||
required
|
||||
className={`resize-none h-24 bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
|
||||
errors.message ? "border-red-500" : ""
|
||||
}`}
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
{errors.message && (
|
||||
<p className="text-red-500 text-xs mt-1">{errors.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<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}
|
||||
>
|
||||
{isSubmitting ? "Sending..." : "Send"}
|
||||
</SubmitButton>
|
||||
<div className="text-center text-lg space-x-2">
|
||||
<span className="text-gray-400">Already have an account?</span>
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="text-white hover:text-emerald-500"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col items-center space-y-4">
|
||||
<SubmitButton className="w-full bg-emerald-600 hover:bg-emerald-700 text-white">
|
||||
Send
|
||||
</SubmitButton>
|
||||
<div className="text-center text-lg space-x-2">
|
||||
<span className="text-gray-400">Already have an account?</span>
|
||||
<Link href="/sign-in" className="text-white hover:text-emerald-500">
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
import {
|
||||
Body,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Section,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
interface AdminNotificationProps {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
messageType: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const AdminNotification = ({
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
messageType,
|
||||
message,
|
||||
}: AdminNotificationProps) => {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>New Contact Form Submission: {messageType}</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
<Heading style={h1}>New Contact Form Submission</Heading>
|
||||
|
||||
<Section style={section}>
|
||||
<Text style={detailLabel}>Name:</Text>
|
||||
<Text style={detailValue}>{name}</Text>
|
||||
|
||||
<Text style={detailLabel}>Email:</Text>
|
||||
<Text style={detailValue}>{email}</Text>
|
||||
|
||||
<Text style={detailLabel}>Phone:</Text>
|
||||
<Text style={detailValue}>{phone}</Text>
|
||||
|
||||
<Text style={detailLabel}>Type:</Text>
|
||||
<Text style={detailValue}>{messageType}</Text>
|
||||
</Section>
|
||||
|
||||
<Section style={section}>
|
||||
<Heading as="h2" style={h2}>
|
||||
Message:
|
||||
</Heading>
|
||||
<Text style={messageBox}>{message}</Text>
|
||||
</Section>
|
||||
|
||||
<Hr style={hr} />
|
||||
<Text style={footer}>
|
||||
This message was submitted through the contact form.
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
const main = {
|
||||
backgroundColor: "#1C1C1C",
|
||||
fontFamily:
|
||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
|
||||
};
|
||||
|
||||
const container = {
|
||||
backgroundColor: "#2E2E2E",
|
||||
margin: "0 auto",
|
||||
padding: "20px 0 48px",
|
||||
marginBottom: "64px",
|
||||
borderRadius: "8px",
|
||||
};
|
||||
|
||||
const section = {
|
||||
padding: "0 48px",
|
||||
};
|
||||
|
||||
const h1 = {
|
||||
color: "#10B981", // Emerald-500
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
margin: "40px 0",
|
||||
padding: "0",
|
||||
textAlign: "center" as const,
|
||||
};
|
||||
|
||||
const h2 = {
|
||||
color: "#10B981", // Emerald-500
|
||||
fontSize: "20px",
|
||||
fontWeight: "bold",
|
||||
margin: "24px 0",
|
||||
padding: "0",
|
||||
};
|
||||
|
||||
const detailLabel = {
|
||||
color: "#A1A1AA", // Zinc-400
|
||||
fontSize: "14px",
|
||||
margin: "8px 0 4px",
|
||||
};
|
||||
|
||||
const detailValue = {
|
||||
color: "#FFFFFF",
|
||||
fontSize: "14px",
|
||||
margin: "0 0 16px",
|
||||
};
|
||||
|
||||
const messageBox = {
|
||||
backgroundColor: "#3F3F46", // Zinc-700
|
||||
padding: "16px",
|
||||
borderRadius: "4px",
|
||||
color: "#FFFFFF",
|
||||
fontSize: "14px",
|
||||
lineHeight: "24px",
|
||||
margin: "0",
|
||||
};
|
||||
|
||||
const hr = {
|
||||
borderColor: "#3F3F46", // Zinc-700
|
||||
margin: "20px 0",
|
||||
};
|
||||
|
||||
const footer = {
|
||||
color: "#71717A", // Zinc-500
|
||||
fontSize: "12px",
|
||||
marginTop: "12px",
|
||||
textAlign: "center" as const,
|
||||
};
|
||||
|
||||
export default AdminNotification;
|
|
@ -0,0 +1,126 @@
|
|||
import {
|
||||
Body,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Section,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
interface UserConfirmationProps {
|
||||
name: string;
|
||||
messageType: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const UserConfirmation = ({
|
||||
name,
|
||||
messageType,
|
||||
message,
|
||||
}: UserConfirmationProps) => {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>Thank you for contacting us</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
<Heading style={h1}>Thank You for Contacting Us</Heading>
|
||||
|
||||
<Section style={section}>
|
||||
<Text style={text}>Dear {name},</Text>
|
||||
|
||||
<Text style={text}>
|
||||
We have received your message regarding "{messageType}" and will
|
||||
get back to you as soon as possible.
|
||||
</Text>
|
||||
|
||||
<Text style={messageLabel}>Here's a copy of your message:</Text>
|
||||
<Text style={messageBox}>{message}</Text>
|
||||
|
||||
<Text style={text}>
|
||||
If you have any further questions, please don't hesitate to
|
||||
contact us.
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
<Hr style={hr} />
|
||||
|
||||
<Text style={signature}>
|
||||
Best regards,
|
||||
<br />
|
||||
The Support Team
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
const main = {
|
||||
backgroundColor: "#1C1C1C",
|
||||
fontFamily:
|
||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
|
||||
};
|
||||
|
||||
const container = {
|
||||
backgroundColor: "#2E2E2E",
|
||||
margin: "0 auto",
|
||||
padding: "20px 0 48px",
|
||||
marginBottom: "64px",
|
||||
borderRadius: "8px",
|
||||
};
|
||||
|
||||
const section = {
|
||||
padding: "0 48px",
|
||||
};
|
||||
|
||||
const h1 = {
|
||||
color: "#10B981", // Emerald-500
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
margin: "40px 0",
|
||||
padding: "0",
|
||||
textAlign: "center" as const,
|
||||
};
|
||||
|
||||
const text = {
|
||||
color: "#FFFFFF",
|
||||
fontSize: "14px",
|
||||
lineHeight: "24px",
|
||||
margin: "16px 0",
|
||||
};
|
||||
|
||||
const messageLabel = {
|
||||
color: "#A1A1AA", // Zinc-400
|
||||
fontSize: "14px",
|
||||
margin: "24px 0 8px",
|
||||
};
|
||||
|
||||
const messageBox = {
|
||||
backgroundColor: "#3F3F46", // Zinc-700
|
||||
padding: "16px",
|
||||
borderRadius: "4px",
|
||||
color: "#FFFFFF",
|
||||
fontSize: "14px",
|
||||
lineHeight: "24px",
|
||||
margin: "0 0 24px",
|
||||
};
|
||||
|
||||
const hr = {
|
||||
borderColor: "#3F3F46", // Zinc-700
|
||||
margin: "20px 0",
|
||||
};
|
||||
|
||||
const signature = {
|
||||
color: "#FFFFFF",
|
||||
fontSize: "14px",
|
||||
lineHeight: "24px",
|
||||
margin: "16px 0",
|
||||
textAlign: "center" as const,
|
||||
};
|
||||
|
||||
export default UserConfirmation;
|
|
@ -0,0 +1,9 @@
|
|||
import { Resend } from "resend";
|
||||
|
||||
export const useResend = () => {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
return {
|
||||
resend,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "status_contact_messages" AS ENUM ('new', 'read', 'replied', 'resolved');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "contact_messages" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"email" TEXT,
|
||||
"phone" TEXT,
|
||||
"message_type" TEXT,
|
||||
"message_type_label" TEXT,
|
||||
"message" TEXT,
|
||||
"status" "status_contact_messages" NOT NULL DEFAULT 'new',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "contact_messages_pkey" PRIMARY KEY ("id")
|
||||
);
|
|
@ -14,14 +14,6 @@ datasource db {
|
|||
directUrl = env("DIRECT_URL")
|
||||
}
|
||||
|
||||
enum Role {
|
||||
admin
|
||||
staff
|
||||
user
|
||||
|
||||
@@map("roles")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id
|
||||
email String @unique
|
||||
|
@ -55,3 +47,35 @@ model Profile {
|
|||
|
||||
@@map("profiles") // Maps to Supabase's 'profiles' table
|
||||
}
|
||||
|
||||
model ContactMessages {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()"))
|
||||
name String?
|
||||
email String?
|
||||
phone String?
|
||||
message_type String?
|
||||
message_type_label String?
|
||||
message String?
|
||||
status StatusContactMessages @default(new)
|
||||
createdAt DateTime @default(dbgenerated("now()")) @db.Timestamptz(6)
|
||||
updatedAt DateTime @default(dbgenerated("now()")) @updatedAt @db.Timestamptz(6)
|
||||
|
||||
@@map("contact_messages") // Maps to Supabase's 'contact_messages' table
|
||||
}
|
||||
|
||||
enum Role {
|
||||
admin
|
||||
staff
|
||||
user
|
||||
|
||||
@@map("roles")
|
||||
}
|
||||
|
||||
enum StatusContactMessages {
|
||||
new
|
||||
read
|
||||
replied
|
||||
resolved
|
||||
|
||||
@@map("status_contact_messages")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export class TValidator {
|
||||
/**
|
||||
* Validate an empty value
|
||||
* @param {string} value - The value to validate
|
||||
* @param {string} fieldName - The name of the field for error message
|
||||
* @returns {Object} - Validation result object with success and optional error message
|
||||
*/
|
||||
static validateEmptyValue(value: string, fieldName: string) {
|
||||
const schema = z.string().min(1, `${fieldName} cannot be empty`);
|
||||
const result = schema.safeParse(value);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `${fieldName} cannot be empty`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an email address
|
||||
* @param {string} email - The email address to validate
|
||||
* @returns {Object} - Validation result object with success and optional error message
|
||||
*/
|
||||
static validateEmail(email: string) {
|
||||
const emptyCheck = this.validateEmptyValue(email, "Email");
|
||||
if (!emptyCheck.success) return emptyCheck;
|
||||
|
||||
const schema = z.string().email("Invalid email address");
|
||||
const result = schema.safeParse(email);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Invalid email address",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a phone number for Indonesia
|
||||
* @param {string} phone - The phone number to validate
|
||||
* @returns {Object} - Validation result object with success and optional error message
|
||||
*/
|
||||
static validatePhone(phone: string) {
|
||||
const emptyCheck = this.validateEmptyValue(phone, "Phone");
|
||||
if (!emptyCheck.success) return emptyCheck;
|
||||
|
||||
// Regex for Indonesian phone numbers:
|
||||
// - Allows format starting with +62 or 0
|
||||
// - For +62 format: +62 followed by 8-12 digits
|
||||
// - For 0 format: 0 followed by 9-12 digits (usually starts with 08)
|
||||
// - Handles common mobile prefixes (8xx) and landline prefixes
|
||||
const schema = z
|
||||
.string()
|
||||
.regex(
|
||||
/^(?:\+62[0-9]{8,12}|0[0-9]{9,12})$/,
|
||||
"Invalid Indonesian phone number format"
|
||||
);
|
||||
const result = schema.safeParse(phone);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"Invalid Indonesian phone number format. Use format +62xxxxxxxxxx or 08xxxxxxxxx",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
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