Skip to content

Commit

Permalink
feat: Create contact form to report issues and integrate Nodemailer f…
Browse files Browse the repository at this point in the history
…or email notifications
  • Loading branch information
ToseebNadaf committed Jan 14, 2025
1 parent c5ce474 commit a88f187
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 8 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ GITHUB_SECRET=
NEXT_PUBLIC_DISCORD_WEBHOOK_URL =
JOB_BOARD_AUTH_SECRET=

EMAIL_USER=
EMAIL_PASS=
EMAIL_RECEIVER=


COHORT3_DISCORD_ACCESS_KEY =
COHORT3_DISCORD_ACCESS_SECRET =
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@radix-ui/react-tooltip": "^1.1.2",
"@tabler/icons-react": "^3.14.0",
"@types/bcrypt": "^5.0.2",
"@types/nodemailer": "^6.4.17",
"@uiw/react-markdown-preview": "^5.1.3",
"@uiw/react-md-editor": "^4.0.4",
"autoprefixer": "^10.4.20",
Expand All @@ -65,6 +66,7 @@
"next-auth": "^4.24.5",
"next-themes": "^0.2.1",
"nextjs-toploader": "^1.6.11",
"nodemailer": "^6.9.16",
"notion-client": "^6.16.0",
"pdf-lib": "^1.17.1",
"prismjs": "^1.29.0",
Expand Down
25 changes: 23 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions src/app/api/send-email/route.ts
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 });
}
}
215 changes: 215 additions & 0 deletions src/app/contact/page.tsx
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 />
</>
);
}
10 changes: 4 additions & 6 deletions src/components/Courses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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}>
Expand Down
Loading

0 comments on commit a88f187

Please sign in to comment.