-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Create contact form to report issues and integrate Nodemailer f…
…or email notifications
- Loading branch information
1 parent
c5ce474
commit a88f187
Showing
7 changed files
with
307 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { NextResponse } from "next/server"; | ||
import { sendMail } from "@/lib/nodemailer"; | ||
|
||
export async function POST(req: Request) { | ||
const body = await req.json(); | ||
const { name, email, message } = body; | ||
|
||
if (!name) { | ||
return NextResponse.json({ error: "Name is required." }, { status: 400 }); | ||
} | ||
|
||
if (!email) { | ||
return NextResponse.json({ error: "Email is required." }, { status: 400 }); | ||
} | ||
|
||
if (!message) { | ||
return NextResponse.json({ error: "Message is required." }, { status: 400 }); | ||
} | ||
|
||
try { | ||
await sendMail(name, email, message); | ||
|
||
return NextResponse.json({ message: "Email sent successfully." }, { status: 200 }); | ||
} catch (error) { | ||
console.error("Error sending email:", error); | ||
return NextResponse.json({ error: "Failed to send email." }, { status: 500 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
"use client"; | ||
|
||
import { Input } from "@/components/ui/input"; | ||
import { Textarea } from "@/components/ui/textarea"; | ||
import { Button } from "@/components/ui/button"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { useForm } from "react-hook-form"; | ||
import { | ||
Form, | ||
FormControl, | ||
FormField, | ||
FormItem, | ||
FormLabel, | ||
FormMessage, | ||
} from "@/components/ui/form"; | ||
import { toast } from "sonner"; | ||
import { motion } from "framer-motion"; | ||
import { z } from "zod"; | ||
import { Toaster } from "sonner"; | ||
import { Loader2 } from "lucide-react"; | ||
|
||
const formSchema = z.object({ | ||
name: z | ||
.string() | ||
.min(1, "First name is required") | ||
.max(50, "First name must be less than 50 characters"), | ||
lastName: z | ||
.string() | ||
.min(1, "Last name is required") | ||
.max(50, "Last name must be less than 50 characters"), | ||
email: z.string().email("Invalid email address"), | ||
message: z | ||
.string() | ||
.min(10, "Message must be at least 10 characters") | ||
.max(500, "Message must be less than 500 characters"), | ||
}); | ||
|
||
type FormValues = z.infer<typeof formSchema>; | ||
|
||
export default function ContactForm() { | ||
const containerVariants = { | ||
hidden: { opacity: 0, y: 20 }, | ||
visible: { | ||
opacity: 1, | ||
y: 0, | ||
transition: { | ||
staggerChildren: 0.3, | ||
when: "beforeChildren", | ||
}, | ||
}, | ||
}; | ||
|
||
const boxVariants = { | ||
hidden: { opacity: 0, y: 20 }, | ||
visible: { | ||
opacity: 1, | ||
y: 0, | ||
transition: { | ||
ease: "easeOut", | ||
}, | ||
}, | ||
}; | ||
|
||
const form = useForm<FormValues>({ | ||
resolver: zodResolver(formSchema), | ||
defaultValues: { | ||
name: "", | ||
lastName: "", | ||
email: "", | ||
message: "", | ||
}, | ||
}); | ||
|
||
const onSubmit = async (data: FormValues) => { | ||
try { | ||
const res = await fetch("/api/send-email", { | ||
method: "POST", | ||
headers: { "Content-Type": "application/json" }, | ||
body: JSON.stringify(data), | ||
}); | ||
|
||
if (res.ok) { | ||
toast("Your message has been sent successfully!"); | ||
form.reset(); | ||
} else { | ||
throw new Error("Failed to send message"); | ||
} | ||
} catch (err) { | ||
toast("There was a problem sending your message. Please try again."); | ||
} | ||
}; | ||
|
||
return ( | ||
<> | ||
<div className="container flex justify-center items-center h-screen md:pt-[90px] pb-[90px]"> | ||
<motion.div | ||
className="bg-cta-gradient rounded-3xl py-[60px] px-4 w-full max-w-3xl shadow-lg" | ||
variants={containerVariants} | ||
initial="hidden" | ||
whileInView="visible" | ||
viewport={{ once: true, amount: 0.2 }} | ||
layout="position" | ||
> | ||
<motion.h2 | ||
className="bg-gradient-to-br from-neutral-300 to-neutral-500 bg-clip-text text-left text-4xl font-bold tracking-tight text-transparent md:text-5xl mb-6" | ||
variants={boxVariants} | ||
> | ||
Contact Us | ||
</motion.h2> | ||
<motion.p | ||
className="mt-2 text-left text-[#7D7F78] max-w-[450px] mb-4" | ||
variants={boxVariants} | ||
> | ||
Fill out the form below, and we'll get back to you as soon as possible. | ||
</motion.p> | ||
<Form {...form}> | ||
<form | ||
onSubmit={form.handleSubmit(onSubmit)} | ||
className="space-y-6 md:space-y-8" | ||
> | ||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> | ||
<FormField | ||
control={form.control} | ||
name="name" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel>First Name</FormLabel> | ||
<FormControl> | ||
<Input | ||
{...field} | ||
placeholder="Name" | ||
aria-label="First Name" | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
<FormField | ||
control={form.control} | ||
name="lastName" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel>Last Name</FormLabel> | ||
<FormControl> | ||
<Input | ||
{...field} | ||
placeholder="Last Name" | ||
aria-label="Last Name" | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
</div> | ||
|
||
<FormField | ||
control={form.control} | ||
name="email" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel>Email</FormLabel> | ||
<FormControl> | ||
<Input | ||
{...field} | ||
type="email" | ||
placeholder="[email protected]" | ||
aria-label="Email" | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
|
||
<FormField | ||
control={form.control} | ||
name="message" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel>Message</FormLabel> | ||
<FormControl> | ||
<Textarea | ||
{...field} | ||
placeholder="Write your message here..." | ||
className="min-h-[120px]" | ||
aria-label="Message" | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
|
||
<div className="flex justify-end"> | ||
<Button type="submit" className="w-full sm:w-auto"> | ||
{form.formState.isSubmitting ? ( | ||
<> | ||
Submitting | ||
<Loader2 className="ml-2 h-4 w-4 animate-spin" /> | ||
</> | ||
) : ( | ||
"Submit" | ||
)} | ||
</Button> | ||
</div> | ||
</form> | ||
</Form> | ||
</motion.div> | ||
</div> | ||
<Toaster /> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,6 @@ import { Button } from './ui/button'; | |
import { refreshDb } from '@/actions/refresh-db'; | ||
import { useSession } from 'next-auth/react'; | ||
import { toast } from 'sonner'; | ||
import Link from 'next/link'; | ||
|
||
export const Courses = ({ courses }: { courses: Course[] }) => { | ||
const session = useSession(); | ||
|
@@ -50,13 +49,12 @@ export const Courses = ({ courses }: { courses: Course[] }) => { | |
</h3> | ||
<p className="text-primary/80"> | ||
Try refreshing the database. If you are still facing issues?{' '} | ||
<Link | ||
href="mailto:[email protected]" | ||
target="_blank" | ||
className="text-primary underline underline-offset-4" | ||
<span | ||
onClick={() => router.push('/contact')} | ||
className="text-primary underline underline-offset-4 cursor-pointer" | ||
> | ||
Contact us | ||
</Link> | ||
</span> | ||
</p> | ||
</div> | ||
<Button size={'lg'} onClick={handleClick}> | ||
|
Oops, something went wrong.