Skip to content

Commit

Permalink
feat: Contact Us Page Added
Browse files Browse the repository at this point in the history
  • Loading branch information
oreoluwa212 committed Jul 25, 2024
1 parent 72233da commit f78b1ce
Show file tree
Hide file tree
Showing 5 changed files with 415 additions and 3 deletions.
26 changes: 26 additions & 0 deletions src/app/(landing-routes)/contact-us/constants.tsx
Original file line number Diff line number Diff line change
@@ -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: [email protected]" },
{ alt: "twitter icon", Icon: Twitter, text: "Twitter @boilerplate23" },
{
alt: "instagram icon",
Icon: Instagram,
text: "Instagram @boilerplate234",
},
{ alt: "linkedin icon", Icon: Linkedin, text: "Linkedin @boilerplate34" },
];
59 changes: 56 additions & 3 deletions src/app/(landing-routes)/contact-us/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,58 @@
const page = () => {
return <div>page</div>;
import { ArrowRight } from "lucide-react";

import ContactForm from "~/components/common/contact-us-form";
import { bizTime, contactInfo } from "./constants";

const Contact = () => {
return (
<main className="bg-white">
<section className="mx-auto max-w-[1200px] pb-20 text-neutral-dark-1 transition-all lg:container lg:pb-44 lg:pt-9">
<div className="grid justify-center gap-2.5 pt-24 *:text-center lg:gap-3 lg:pb-11 lg:pt-24">
<h1 className="text-3xl font-bold lg:text-6xl">Contact Our Team</h1>
<p className="text-lg max-sm:hidden lg:text-2xl">
Let&#39;s Build Your Product Together
</p>
<p className="text-lg font-medium sm:hidden">
Achieve your dreams with us today
</p>
</div>
<div className="items-start justify-between gap-4 lg:flex">
<ContactForm />
<div className="mx-auto grid gap-3 max-lg:container *:rounded-md *:border *:bg-background *:p-6 max-lg:mt-5 lg:w-1/2 lg:pr-8 lg:pt-8">
<div className="grid lg:gap-7">
<h2 className="text-lg font-bold lg:text-3xl">United Kingdom</h2>
<h3 className="text-sm font-bold max-lg:pb-6 max-lg:pt-2 lg:text-base">
Business hours : {bizTime[0]} - {bizTime[1]}
</h3>
<div className="grid gap-5 lg:gap-3">
{contactInfo.map((info) => (
<div key={info.alt} className="flex items-center gap-4">
<div className="rounded-sm bg-neutral-dark-1 p-2.5">
<info.Icon color="white" size={24} />
</div>
<p className="text-lg leading-5">{info.text}</p>
</div>
))}
</div>
</div>
<div className="gap-2 max-lg:grid">
<div className="flex justify-between">
<h2 className="text-xl font-semibold text-primary underline underline-offset-2">
FAQ
</h2>
<div>
<ArrowRight className="text-primary" />
</div>
</div>
<p className="texl-lg">
See and get answers to the most frequent asked questions
</p>
</div>
</div>
</div>
</section>
</main>
);
};

export default page;
export default Contact;
200 changes: 200 additions & 0 deletions src/components/common/contact-us-form/index.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof schema>;

const initialFormData: FormData = {
name: "",
email: "",
phone: "",
message: "",
};

const ContactForm: React.FC = () => {
const [formData, setFormData] = useState<FormData>({ ...initialFormData });
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const [status, setStatus] = useState<boolean | undefined>();
const [message, setMessage] = useState<string | undefined>();
const [loading, setLoading] = useState<boolean>(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<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = event.target;
setFormData({ ...formData, [name]: value });
};

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
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 (
<>
<div className="mx-auto w-full lg:max-w-[80%] lg:p-8">
<form
onSubmit={handleSubmit}
className="mb-4 w-full rounded-[8px] p-8 lg:max-w-[80%] lg:border lg:bg-background lg:shadow-sm"
role="form"
>
{inputFields.map((field) => (
<div key={field.name} className="mb-6">
<InputField
value={formData[field.name as keyof FormData]}
type={field.type}
onChange={handleChange}
placeholder={field.placeholder}
id={field.name}
name={field.name}
label={field.label}
/>
{errors[field.name] && (
<p className="text-xs italic text-destructive">
{errors[field.name]}
</p>
)}
</div>
))}
<div className="mb-6">
<label htmlFor="message" className="mb-2 block text-lg">
Message
</label>
<input
id="message"
name="message"
placeholder="Message..."
value={formData.message}
onChange={handleChange}
className="w-full appearance-none rounded-[8px] border bg-transparent px-3 py-2 pb-[112px] leading-tight outline-none"
/>
{errors.message && (
<p className="text-xs italic text-destructive">
{errors.message}
</p>
)}
</div>
<CustomButton
variant="primary"
size="lg"
isLoading={loading}
className="w-full px-4 py-7"
>
<Mail />
Send
</CustomButton>

{status !== undefined && (
<p
className={`text-xs italic ${status ? "text-default" : "text-destructive"}`}
>
{message}
</p>
)}
</form>
</div>
</>
);
};

export default ContactForm;
38 changes: 38 additions & 0 deletions src/components/common/contact-us-form/inputfield.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from "react";

interface InputFieldProperties {
value: string;
type: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
placeholder: string;
id: string;
name: string;
label: string;
}

const InputField: React.FC<InputFieldProperties> = ({
value,
type,
onChange,
placeholder,
id,
name,
label,
}) => (
<div className="mb-4">
<label htmlFor={id} className="mb-2 block text-sm">
{label}
</label>
<input
type={type}
id={id}
name={name}
value={value}
onChange={onChange}
placeholder={placeholder}
className="w-full appearance-none rounded-[8px] border px-3 py-4 leading-tight focus:shadow-outline focus:outline-none"
/>
</div>
);

export default InputField;
Loading

0 comments on commit f78b1ce

Please sign in to comment.