diff --git a/.env.example b/.env.example index 1a8fe7f37..70ffe533c 100644 --- a/.env.example +++ b/.env.example @@ -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 = diff --git a/package.json b/package.json index 31bdc5ddf..5584f50c0 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d65c62289..ac5d24817 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2 + '@types/nodemailer': + specifier: ^6.4.17 + version: 6.4.17 '@uiw/react-markdown-preview': specifier: ^5.1.3 version: 5.1.3(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -124,13 +127,16 @@ importers: version: 14.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-auth: specifier: ^4.24.5 - version: 4.24.7(next@14.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.24.7(next@14.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.2.1 version: 0.2.1(next@14.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nextjs-toploader: specifier: ^1.6.11 version: 1.6.12(next@14.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nodemailer: + specifier: ^6.9.16 + version: 6.9.16 notion-client: specifier: ^6.16.0 version: 6.16.0 @@ -1448,6 +1454,9 @@ packages: '@types/node@20.16.0': resolution: {integrity: sha512-vDxceJcoZhIVh67S568bm1UGZO0DX0hpplJZxzeXMKwIPLn190ec5RRxQ69BKhX44SUGIxxgMdDY557lGLKprQ==} + '@types/nodemailer@6.4.17': + resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} + '@types/prismjs@1.26.4': resolution: {integrity: sha512-rlAnzkW2sZOjbqZ743IHUhFcvzaGbqijwOu8QZnZCjfQzBqFE3s4lOTJEsxikImav9uzz/42I+O7YUs1mWgMlg==} @@ -3138,6 +3147,10 @@ packages: node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + nodemailer@6.9.16: + resolution: {integrity: sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==} + engines: {node: '>=6.0.0'} + nopt@5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} engines: {node: '>=6'} @@ -5546,6 +5559,10 @@ snapshots: dependencies: undici-types: 6.19.6 + '@types/nodemailer@6.4.17': + dependencies: + '@types/node': 20.16.0 + '@types/prismjs@1.26.4': {} '@types/prop-types@15.7.12': {} @@ -7574,7 +7591,7 @@ snapshots: neo-async@2.6.2: {} - next-auth@4.24.7(next@14.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-auth@4.24.7(next@14.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.0 '@panva/hkdf': 1.2.1 @@ -7588,6 +7605,8 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) uuid: 8.3.2 + optionalDependencies: + nodemailer: 6.9.16 next-themes@0.2.1(next@14.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -7636,6 +7655,8 @@ snapshots: node-releases@2.0.18: {} + nodemailer@6.9.16: {} + nopt@5.0.0: dependencies: abbrev: 1.1.1 diff --git a/src/app/api/send-email/route.ts b/src/app/api/send-email/route.ts new file mode 100644 index 000000000..be4d51e90 --- /dev/null +++ b/src/app/api/send-email/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from "next/server"; +import { sendMail } from "@/lib/nodemailer"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; + +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions); + + if (!session || !session?.user) { + return NextResponse.json({error: "Unauthorized"}, { status: 401 }); + } + + 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 }); + } +} diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx new file mode 100644 index 000000000..518abe049 --- /dev/null +++ b/src/app/contact/page.tsx @@ -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; + +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({ + 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 ( + <> +
+ + + Contact Us + + + Fill out the form below, and we'll get back to you as soon as possible. + +
+ +
+ ( + + First Name + + + + + + )} + /> + ( + + Last Name + + + + + + )} + /> +
+ + ( + + Email + + + + + + )} + /> + + ( + + Message + +