From f78b1ce9f6bbc8708f0c2628b246d94d199666d6 Mon Sep 17 00:00:00 2001 From: oree Date: Thu, 25 Jul 2024 14:45:02 +0100 Subject: [PATCH] feat: Contact Us Page Added --- .../(landing-routes)/contact-us/constants.tsx | 26 +++ src/app/(landing-routes)/contact-us/page.tsx | 59 +++++- .../common/contact-us-form/index.tsx | 200 ++++++++++++++++++ .../common/contact-us-form/inputfield.tsx | 38 ++++ src/test/contact.test.tsx | 95 +++++++++ 5 files changed, 415 insertions(+), 3 deletions(-) create mode 100644 src/app/(landing-routes)/contact-us/constants.tsx create mode 100644 src/components/common/contact-us-form/index.tsx create mode 100644 src/components/common/contact-us-form/inputfield.tsx create mode 100644 src/test/contact.test.tsx diff --git a/src/app/(landing-routes)/contact-us/constants.tsx b/src/app/(landing-routes)/contact-us/constants.tsx new file mode 100644 index 000000000..ea0baf1f4 --- /dev/null +++ b/src/app/(landing-routes)/contact-us/constants.tsx @@ -0,0 +1,26 @@ +import { + Instagram, + Linkedin, + Mail, + MapPin, + Phone, + Twitter, +} from "lucide-react"; + +export const bizTime = ["8am", "6pm"]; +export const contactInfo = [ + { + alt: "map icon", + Icon: MapPin, + text: "10111, hornchurch, london, United kingdom", + }, + { alt: "phone icon", Icon: Phone, text: "+4403989898787" }, + { alt: "mail icon", Icon: Mail, text: "Email: supportteam@gmail.com" }, + { alt: "twitter icon", Icon: Twitter, text: "Twitter @boilerplate23" }, + { + alt: "instagram icon", + Icon: Instagram, + text: "Instagram @boilerplate234", + }, + { alt: "linkedin icon", Icon: Linkedin, text: "Linkedin @boilerplate34" }, +]; diff --git a/src/app/(landing-routes)/contact-us/page.tsx b/src/app/(landing-routes)/contact-us/page.tsx index db4d38750..7ca45b82a 100644 --- a/src/app/(landing-routes)/contact-us/page.tsx +++ b/src/app/(landing-routes)/contact-us/page.tsx @@ -1,5 +1,58 @@ -const page = () => { - return
page
; +import { ArrowRight } from "lucide-react"; + +import ContactForm from "~/components/common/contact-us-form"; +import { bizTime, contactInfo } from "./constants"; + +const Contact = () => { + return ( +
+
+
+

Contact Our Team

+

+ Let's Build Your Product Together +

+

+ Achieve your dreams with us today +

+
+
+ +
+
+

United Kingdom

+

+ Business hours : {bizTime[0]} - {bizTime[1]} +

+
+ {contactInfo.map((info) => ( +
+
+ +
+

{info.text}

+
+ ))} +
+
+
+
+

+ FAQ +

+
+ +
+
+

+ See and get answers to the most frequent asked questions +

+
+
+
+
+
+ ); }; -export default page; +export default Contact; diff --git a/src/components/common/contact-us-form/index.tsx b/src/components/common/contact-us-form/index.tsx new file mode 100644 index 000000000..dd2415154 --- /dev/null +++ b/src/components/common/contact-us-form/index.tsx @@ -0,0 +1,200 @@ +"use client"; + +import { Mail } from "lucide-react"; +import { useEffect, useState } from "react"; +import { z, ZodError } from "zod"; + +import CustomButton from "../common-button/common-button"; +import InputField from "./inputfield"; + +const schema = z.object({ + name: z.string().min(5, "Name is required"), + email: z.string().email("Email is invalid"), + phone: z + .string() + .min(8, "Phone number is required") + .regex(/^\+?\d{10,15}$/, "Phone number is invalid"), + message: z.string().min(1, "Message is required"), +}); + +type FormData = z.infer; + +const initialFormData: FormData = { + name: "", + email: "", + phone: "", + message: "", +}; + +const ContactForm: React.FC = () => { + const [formData, setFormData] = useState({ ...initialFormData }); + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + const [status, setStatus] = useState(); + const [message, setMessage] = useState(); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (status !== undefined) { + const timer = setTimeout(() => { + setStatus(undefined); + }, 3000); + return () => clearTimeout(timer); + } + }, [status]); + + const validate = () => { + try { + schema.parse(formData); + return {}; + } catch (error) { + if (error instanceof ZodError) { + const errorMessages: { [key: string]: string } = {}; + for (const { path, message } of error.errors) { + errorMessages[path[0]] = message; + } + return errorMessages; + } + return {}; + } + }; + + const handleChange = ( + event: React.ChangeEvent, + ) => { + const { name, value } = event.target; + setFormData({ ...formData, [name]: value }); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const validationErrors = validate(); + if (Object.keys(validationErrors).length > 0) { + setErrors(validationErrors); + return; + } + try { + setLoading(true); + const response = await fetch( + "https://test.gracefilledcollege.com/public/api/v1/contact", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }, + ); + + const responseData = await response.json(); + + if (!response.ok) { + throw new Error(responseData.message || "Failed to submit the form."); + } + + setStatus(true); + setMessage(responseData?.message || "Form submitted successfully!"); + setFormData({ ...initialFormData }); + setErrors({}); + } catch (error) { + setStatus(false); + setMessage( + (error as Error).message || + "Failed to submit the form. Please try again.", + ); + } finally { + setLoading(false); + } + }; + + const inputFields = [ + { + label: "Name", + name: "name", + type: "text", + placeholder: "Enter full name", + required: true, + }, + { + label: "Email", + name: "email", + type: "email", + placeholder: "Enter email address", + required: true, + }, + { + label: "Phone Number", + name: "phone", + type: "tel", + placeholder: "Enter phone number", + required: true, + }, + ]; + + return ( + <> +
+
+ {inputFields.map((field) => ( +
+ + {errors[field.name] && ( +

+ {errors[field.name]} +

+ )} +
+ ))} +
+ + + {errors.message && ( +

+ {errors.message} +

+ )} +
+ + + Send + + + {status !== undefined && ( +

+ {message} +

+ )} +
+
+ + ); +}; + +export default ContactForm; diff --git a/src/components/common/contact-us-form/inputfield.tsx b/src/components/common/contact-us-form/inputfield.tsx new file mode 100644 index 000000000..5e90ddf79 --- /dev/null +++ b/src/components/common/contact-us-form/inputfield.tsx @@ -0,0 +1,38 @@ +import React from "react"; + +interface InputFieldProperties { + value: string; + type: string; + onChange: (event: React.ChangeEvent) => void; + placeholder: string; + id: string; + name: string; + label: string; +} + +const InputField: React.FC = ({ + value, + type, + onChange, + placeholder, + id, + name, + label, +}) => ( +
+ + +
+); + +export default InputField; diff --git a/src/test/contact.test.tsx b/src/test/contact.test.tsx new file mode 100644 index 000000000..cd7bfa96c --- /dev/null +++ b/src/test/contact.test.tsx @@ -0,0 +1,95 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; + +import Contact from "~/app/(landing-routes)/contact-us/page"; + +describe("contact Page tests", () => { + it("should render the Contact Us form and content card correctly", () => { + expect.assertions(4); + render(); + + expect(screen.getByRole("form")).toBeInTheDocument(); + + expect(screen.getByText("Contact Our Team")).toBeInTheDocument(); + + expect(screen.getByText(/business hours/i)).toBeInTheDocument(); + + expect(screen.getByRole("heading", { name: /faq/i })).toBeInTheDocument(); + }); + + it("should validate the form inputs correctly", async () => { + expect.assertions(1); + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: "invalid-email" }, + }); + fireEvent.submit(screen.getByRole("form")); + + await waitFor(() => { + expect(screen.getByText(/email is invalid/i)).toBeInTheDocument(); + }); + }); + + it("should handle API integration correctly", async () => { + expect.assertions(1); + + const fetchMock = vi.fn(); + global.fetch = fetchMock; + + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ message: "Form submitted successfully!" }), + { + status: 200, + statusText: "OK", + headers: { "Content-Type": "application/json" }, + }, + ), + ); + + render(); + + fireEvent.change(screen.getByPlaceholderText("Enter full name"), { + target: { value: "John Doe" }, + }); + fireEvent.change(screen.getByPlaceholderText("Enter email address"), { + target: { value: "john@example.com" }, + }); + fireEvent.change(screen.getByPlaceholderText("Enter phone number"), { + target: { value: "+1234567890" }, + }); + fireEvent.change(screen.getByPlaceholderText("Message..."), { + target: { value: "Hello!" }, + }); + fireEvent.click(screen.getByText("Send")); + + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith( + "https://test.gracefilledcollege.com/public/api/v1/contact", + expect.objectContaining({ + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: "John Doe", + email: "john@example.com", + phone: "+1234567890", + message: "Hello!", + }), + }), + ); + }); + }); + + it("should be responsive", () => { + expect.assertions(2); + render(); + + window.innerWidth = 320; + window.dispatchEvent(new Event("resize")); + expect(screen.getByRole("form")).toBeInTheDocument(); + + window.innerWidth = 1024; + window.dispatchEvent(new Event("resize")); + expect(screen.getByRole("form")).toBeInTheDocument(); + }); +});