diff --git a/README.md b/README.md index 30fe18576..2dc2df070 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ To run the project locally: 2. Visit `http://localhost:3000` in your browser. ### Recommended Extensions + - Prettier - Open your command palette, choose your default formatter to be Prettier, and enable format on save. - ESLint diff --git a/app/(auth)/actions.ts b/app/(auth)/actions.ts index 3ebb98f20..bb766821b 100644 --- a/app/(auth)/actions.ts +++ b/app/(auth)/actions.ts @@ -91,6 +91,30 @@ export async function signup(formData: FormData) { redirect("/verification?email=" + data.email); } +// Contact form submission +export async function contactSubmit(formData: FormData) { + const supabase = createClient(); + + const { error } = await supabase.from("contact").insert([ + { + first_name: formData.get("first-name") as string, + last_name: formData.get("last-name") as string, + email: formData.get("email") as string, + company_name: formData.get("company-name") as string, + organisation_size: formData.get("organisation-size") as string, + job_title: formData.get("job-title") as string, + phone_number: formData.get("phone-number") as string, + message: formData.get("message") as string, + }, + ]); + + if (error) { + return { error: error.message }; + } + + revalidatePath("/", "layout"); +} + // OAuth sign-in with Google or GitHub export async function signinWithOAuth( provider: Provider, diff --git a/app/contact/page.tsx b/app/contact/page.tsx new file mode 100644 index 000000000..90ab64f9a --- /dev/null +++ b/app/contact/page.tsx @@ -0,0 +1,17 @@ +import ContactUs from "@/components/contact"; +import { constructMetadata } from "@/lib/utils"; +import { Metadata } from "next/types"; + +export const metadata: Metadata = constructMetadata({ + title: "Contact Us", + description: "Contact PearAI.", + canonical: "/contact", +}); + +export default function Pricing() { + return ( + <> + + > + ); +} diff --git a/components/contact.tsx b/components/contact.tsx new file mode 100644 index 000000000..a998865bb --- /dev/null +++ b/components/contact.tsx @@ -0,0 +1,297 @@ +"use client"; +import Link from "next/link"; +import { useState } from "react"; +import { signup, signinWithOAuth, contactSubmit } from "@/app/(auth)/actions"; +import { Provider } from "@supabase/supabase-js"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Select } from "@/components/ui/select"; +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from "@/components/ui/form"; +import { contactSchema, ContactFormData } from "@/utils/form-schema"; +import { toast } from "sonner"; +import { useRouter } from "next/navigation"; + +const organisationSizes = [ + { value: "1-10", label: "1-10 employees" }, + { value: "11-50", label: "11-50 employees" }, + { value: "51-200", label: "51-200 employees" }, + { value: "201-500", label: "201-500 employees" }, + { value: "501-1000", label: "501-1000 employees" }, + { value: "1001+", label: "1001+ employees" }, +]; + +export default function ContactUs() { + const [isSubmitting, setIsSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const form = useForm({ + resolver: zodResolver(contactSchema), + defaultValues: { + first_name: "", + last_name: "", + email: "", + job_title: "", + company_name: "", + organisation_size: "", + phone_number: "", + message: "", + }, + }); + const router = useRouter(); + + const handleContactSubmit = async (data: ContactFormData) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMessage(null); + + try { + const formData = new FormData(); + formData.append("first-name", data.first_name); + formData.append("last-name", data.last_name); + formData.append("email", data.email); + formData.append("job-title", data.job_title); + formData.append("company-name", data.company_name || ""); + formData.append("organisation-size", data.organisation_size); + formData.append("phone-number", data.phone_number || ""); + formData.append("message", data.message); + + const response = await contactSubmit(formData); + if (response?.error) { + setErrorMessage(response.error); + } else { + toast.success("Successfully submitted! We will get back to you soon."); + router.push("/about"); + } + } catch (error) { + setErrorMessage("An unexpected error occurred. Please try again."); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + + + Looking for a custom{" "} + pricing option? Get in touch! + + + + + + + + Please contact us using the form below + + + + + + + ( + + + First Name * + + + + + + + )} + /> + + ( + + + Last Name * + + + + + + + )} + /> + + ( + + + Work Email * + + + + + + + )} + /> + + ( + + + Job Title * + + + + + + + )} + /> + + ( + + + Company Name * + + + + + + + )} + /> + + ( + + + Organisation Size{" "} + * + + + + + Select your organisation size + + {organisationSizes.map((size) => ( + + {size.label} + + ))} + + + + + )} + /> + + ( + + Phone Number + + + + + + )} + /> + + ( + + + Message * + + + + + + + )} + /> + + + {isSubmitting ? "Submitting..." : "Submit"} + + + {errorMessage && ( + {errorMessage} + )} + + + + + + + ); +} diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 000000000..3ba16d38e --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export interface SelectProps + extends React.SelectHTMLAttributes {} + +const Select = React.forwardRef( + ({ className, ...props }, ref) => { + return ( + + {props.children} + + ); + }, +); + +Select.displayName = "Select"; + +export { Select }; diff --git a/utils/form-schema.ts b/utils/form-schema.ts index 48f375684..7666058d9 100644 --- a/utils/form-schema.ts +++ b/utils/form-schema.ts @@ -28,6 +28,37 @@ export const signInSchema = z.object({ password: passwordSchema.shape.password, }); +// New schema for contact form + +export const contactSchema = z.object({ + first_name: z + .string() + .min(1, { message: "First name is required." }) + .max(100, { message: "First name is too long." }), + last_name: z + .string() + .min(1, { message: "Last name is required." }) + .max(100, { message: "Last name is too long." }), + email: emailSchema.shape.email, + company_name: z + .string() + .min(1, { message: "Company name is required." }) + .max(100, { message: "Company name is too long." }), + organisation_size: z + .string() + .min(1, { message: "Organisation size is required." }), + job_title: z + .string() + .min(1, { message: "Job title is required." }) + .max(100, { message: "Job title is too long." }), + phone_number: z + .string() + .max(20, { message: "Phone number is too long." }) + .optional(), // Phone number is an optional field to fill in for now + message: z.string().min(1, { message: "Message is required." }), + // Do we need a max length for the message? +}); + export const resetPasswordSchema = z.object({ email: emailSchema.shape.email, }); @@ -44,5 +75,6 @@ export const updatePasswordSchema = z export type SignUpFormData = z.infer; export type SignInFormData = z.infer; +export type ContactFormData = z.infer; export type ResetPasswordFormData = z.infer; export type UpdatePasswordFormData = z.infer;
{errorMessage}