From 4ade1e976891a4b7306a48868f434dab3dde1a04 Mon Sep 17 00:00:00 2001 From: Yassin Soliman Date: Sat, 24 Jan 2026 12:13:59 -0700 Subject: [PATCH 1/2] Added SMTP through .env via nodemailer v7.0.12 --- waybionic/.gitignore | 2 +- waybionic/package-lock.json | 13 +- waybionic/package.json | 1 + waybionic/src/app/api/contact/route.ts | 178 ++++++++++++++++++ .../src/app/contact/components/contact.tsx | 70 +++++-- 5 files changed, 243 insertions(+), 21 deletions(-) create mode 100644 waybionic/src/app/api/contact/route.ts diff --git a/waybionic/.gitignore b/waybionic/.gitignore index 5ef6a52..2d47273 100644 --- a/waybionic/.gitignore +++ b/waybionic/.gitignore @@ -30,7 +30,7 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) +# env files .env* # vercel diff --git a/waybionic/package-lock.json b/waybionic/package-lock.json index 3e0ae6c..3db9771 100644 --- a/waybionic/package-lock.json +++ b/waybionic/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "next": "^15.4.10", + "nodemailer": "^7.0.12", "react": "^19.0.0", "react-dom": "^19.0.0", "react-responsive-carousel": "^3.2.23" @@ -907,7 +908,6 @@ "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1357,6 +1357,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/nodemailer": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz", + "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1417,7 +1426,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -1427,7 +1435,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.25.0" }, diff --git a/waybionic/package.json b/waybionic/package.json index 42acacf..c4e1683 100644 --- a/waybionic/package.json +++ b/waybionic/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "next": "^15.4.10", + "nodemailer": "^7.0.12", "react": "^19.0.0", "react-dom": "^19.0.0", "react-responsive-carousel": "^3.2.23" diff --git a/waybionic/src/app/api/contact/route.ts b/waybionic/src/app/api/contact/route.ts new file mode 100644 index 0000000..cbaeabc --- /dev/null +++ b/waybionic/src/app/api/contact/route.ts @@ -0,0 +1,178 @@ +import { NextResponse } from "next/server"; +import nodemailer from "nodemailer"; + +export const runtime = "nodejs"; + +type ContactPayload = { + firstName?: string; + lastName?: string; + fullName?: string; + subject?: string; + email?: string; + message?: string; +}; + +const emailPattern = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; + +function escapeHtml(value: string) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function normalize(value?: string) { + return typeof value === "string" ? value.trim() : ""; +} + +export async function POST(request: Request) { + let payload: ContactPayload; + + try { + payload = (await request.json()) as ContactPayload; + } catch (error) { + return NextResponse.json( + { error: "Invalid request payload." }, + { status: 400 } + ); + } + + const firstName = normalize(payload.firstName); + const lastName = normalize(payload.lastName); + const fullName = normalize(payload.fullName) || `${firstName} ${lastName}`.trim(); + const subject = normalize(payload.subject); + const email = normalize(payload.email); + const message = normalize(payload.message); + + if (!firstName || !lastName || !subject || !email || !message) { + return NextResponse.json( + { error: "Please complete all required fields." }, + { status: 400 } + ); + } + + if (!emailPattern.test(email)) { + return NextResponse.json( + { error: "Please enter a valid email address." }, + { status: 400 } + ); + } + + const smtpHost = process.env.SMTP_HOST; + const smtpPort = Number(process.env.SMTP_PORT || ""); + const smtpUser = process.env.SMTP_USER; + const smtpPass = process.env.SMTP_PASS; + const smtpSecure = + process.env.SMTP_SECURE === "true" || smtpPort === 465; + + if (!smtpHost || !smtpPort || !smtpUser || !smtpPass) { + console.error("Contact email missing SMTP config.", { + hasHost: Boolean(smtpHost), + hasPort: Number.isFinite(smtpPort) && smtpPort > 0, + hasUser: Boolean(smtpUser), + hasPass: Boolean(smtpPass) + }); + return NextResponse.json( + { error: "Email service is not configured yet." }, + { status: 500 } + ); + } + + const toAddress = process.env.CONTACT_TO || "waybionics@gmail.com"; + const fromAddress = + process.env.CONTACT_FROM || `Waybionic Website <${smtpUser}>`; + + const submittedAt = new Date().toISOString(); + const safeName = escapeHtml(fullName || "Website Visitor"); + const safeSubject = escapeHtml(subject); + const safeEmail = escapeHtml(email); + const safeMessage = escapeHtml(message); + + const textBody = [ + "New Contact Request", + "", + `Name: ${fullName || "Website Visitor"}`, + `Email: ${email}`, + `Subject: ${subject}`, + `Submitted: ${submittedAt}`, + "", + "Message:", + message + ].join("\n"); + + const htmlBody = ` +
+

New Contact Request

+ + + + + + + + + + + + + + + + + + + +
Name${safeName}
Email${safeEmail}
Subject${safeSubject}
Submitted${submittedAt}
+
Message
+
+ ${safeMessage} +
+

+ Submitted from the Waybionic website contact form. +

+
+ `; + + try { + const transporter = nodemailer.createTransport({ + host: smtpHost, + port: smtpPort, + secure: smtpSecure, + auth: { + user: smtpUser, + pass: smtpPass + } + }); + + await transporter.sendMail({ + to: toAddress, + from: fromAddress, + replyTo: email, + subject: `Waybionic Contact: ${subject}`, + text: textBody, + html: htmlBody + }); + } catch (error) { + const err = error as Error & { + code?: string; + responseCode?: number; + response?: string; + command?: string; + }; + console.error("Contact email send failed.", { + message: err.message, + code: err.code, + responseCode: err.responseCode, + response: err.response, + command: err.command + }); + return NextResponse.json( + { error: "Unable to send your message right now." }, + { status: 500 } + ); + } + + return NextResponse.json({ ok: true }); +} diff --git a/waybionic/src/app/contact/components/contact.tsx b/waybionic/src/app/contact/components/contact.tsx index 8c982f1..d0aa0df 100644 --- a/waybionic/src/app/contact/components/contact.tsx +++ b/waybionic/src/app/contact/components/contact.tsx @@ -9,6 +9,8 @@ export default function Contact() { const [email, setEmail] = useState(""); const [message, setMessage] = useState(""); const [status, setStatus] = useState(""); + const [statusType, setStatusType] = useState<"success" | "error" | "">(""); + const [isSubmitting, setIsSubmitting] = useState(false); const link: string = @@ -16,26 +18,52 @@ export default function Contact() { const hiring: boolean = true; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const fullName = `${firstName} ${lastName}`.trim(); + setIsSubmitting(true); + setStatus(""); + setStatusType(""); + try { + const response = await fetch("/api/contact", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + firstName, + lastName, + fullName, + subject, + email, + message + }) + }); - const mailtoLink = `mailto:waybionics@gmail.com?subject=${encodeURIComponent( - subject || "Contact Form Submission" - )}&body=${encodeURIComponent(message)}%0D%0A%0D%0AFrom: ${encodeURIComponent( - fullName - )} (${encodeURIComponent(email)})`; + if (!response.ok) { + const payload = await response.json().catch(() => null); + setStatusType("error"); + setStatus(payload?.error || "Something went wrong. Please try again."); + return; + } - window.location.href = mailtoLink; - - setFirstName(""); - setLastName(""); - setSubject(""); - setEmail(""); - setMessage(""); - setStatus("Message opened in your default email client!"); + setFirstName(""); + setLastName(""); + setSubject(""); + setEmail(""); + setMessage(""); + setStatusType("success"); + setStatus( + "Thanks for reaching out. Your message is on its way to the Waybionic team." + ); + } catch (error) { + setStatusType("error"); + setStatus("Unable to send right now. Please try again in a moment."); + } finally { + setIsSubmitting(false); + } }; return ( @@ -142,12 +170,20 @@ const hiring: boolean = true; {status && ( -

{status}

+

+ {status} +

)} From 441ab685e199f236d43d06922225ce54ecbb41cd Mon Sep 17 00:00:00 2001 From: "richard.nguyen1" Date: Sat, 24 Jan 2026 15:01:06 -0700 Subject: [PATCH 2/2] Finished forms and sending emails using SMTP --- waybionic/package-lock.json | 3 + waybionic/src/app/api/contact/route.ts | 99 ++++++++++++- .../src/app/contact/components/NonStudent.tsx | 65 +++++++++ .../contact/components/NonStudentInput.tsx | 19 +++ .../contact/components/NonStudentMessage.tsx | 29 ++++ .../src/app/contact/components/Student.tsx | 54 ++++++++ .../src/app/contact/components/contact.tsx | 130 ++++++++++++++---- 7 files changed, 369 insertions(+), 30 deletions(-) create mode 100644 waybionic/src/app/contact/components/NonStudent.tsx create mode 100644 waybionic/src/app/contact/components/NonStudentInput.tsx create mode 100644 waybionic/src/app/contact/components/NonStudentMessage.tsx create mode 100644 waybionic/src/app/contact/components/Student.tsx diff --git a/waybionic/package-lock.json b/waybionic/package-lock.json index 3db9771..605be6f 100644 --- a/waybionic/package-lock.json +++ b/waybionic/package-lock.json @@ -908,6 +908,7 @@ "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1426,6 +1427,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -1435,6 +1437,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.25.0" }, diff --git a/waybionic/src/app/api/contact/route.ts b/waybionic/src/app/api/contact/route.ts index cbaeabc..d1eda41 100644 --- a/waybionic/src/app/api/contact/route.ts +++ b/waybionic/src/app/api/contact/route.ts @@ -3,6 +3,7 @@ import nodemailer from "nodemailer"; export const runtime = "nodejs"; +// Gets all the information from the form, and puts it in a pyaload type ContactPayload = { firstName?: string; lastName?: string; @@ -10,10 +11,22 @@ type ContactPayload = { subject?: string; email?: string; message?: string; + typeOfContact?: string; + yearOfStudy?: string; + fieldOfStudy?: string; + clubInfo?: string; + businessInfo?: string; + professionInfo?: string; + sponsorshipInfo?: string; + partnershipInfo?: string; + professorInfo?: string; + otherInfo?: string; }; +// Regex for checking email const emailPattern = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; +// Remove standard characters w/ internet chracters so that it appears normal in the email function escapeHtml(value: string) { return value .replace(/&/g, "&") @@ -23,13 +36,16 @@ function escapeHtml(value: string) { .replace(/'/g, "'"); } +// Removes leading and trailing spaces function normalize(value?: string) { return typeof value === "string" ? value.trim() : ""; } export async function POST(request: Request) { + // creates an instance of ContactPayload let payload: ContactPayload; + // Next.js AI route, read the incoming HTTP body as JSON and convert to ContactPayload try { payload = (await request.json()) as ContactPayload; } catch (error) { @@ -39,20 +55,62 @@ export async function POST(request: Request) { ); } + // Getting the values from the form const firstName = normalize(payload.firstName); const lastName = normalize(payload.lastName); const fullName = normalize(payload.fullName) || `${firstName} ${lastName}`.trim(); const subject = normalize(payload.subject); const email = normalize(payload.email); const message = normalize(payload.message); + const typeOfContact = normalize(payload.typeOfContact); + const yearOfStudy = normalize(payload.yearOfStudy); + const fieldOfStudy = normalize(payload.fieldOfStudy); + const clubInfo = normalize(payload.clubInfo); + const businessInfo = normalize(payload.businessInfo); + const professionInfo = normalize(payload.professionInfo); + const sponsorshipInfo = normalize(payload.sponsorshipInfo); + const partnershipInfo = normalize(payload.partnershipInfo); + const professorInfo = normalize(payload.professorInfo); + const otherInfo = normalize(payload.otherInfo); - if (!firstName || !lastName || !subject || !email || !message) { + // Error checking / form parsing + + if (!firstName || !lastName || !subject || !email) { return NextResponse.json( { error: "Please complete all required fields." }, { status: 400 } ); } + console.log(typeOfContact); + if (typeOfContact === "Student" && (!yearOfStudy || !fieldOfStudy)) { + return NextResponse.json( + { error: "Please complete all required student fields." }, + { status: 400 } + ); + } + + // Error filtering for non-student + const rules: Record = { + "Club": { value: clubInfo, error: "Please complete all required club fields." }, + "Business": { value: businessInfo, error: "Please complete all required business fields." }, + "Profession": { value: professionInfo, error: "Please complete all required profession fields." }, + "Sponsorship": { value: sponsorshipInfo, error: "Please complete all required sponsorship fields." }, + "Partnership": { value: partnershipInfo, error: "Please complete all required partnership fields." }, + "Professor": { value: professorInfo, error: "Please complete all required professor fields." }, + "Other": { value: otherInfo, error: "Please complete all fields." } + }; + + if (typeOfContact !== "Student" && rules[typeOfContact]) { + const { value, error } = rules[typeOfContact]; + + if (!value) { + return NextResponse.json({ error }, { status: 400 }); + } + } + + + /// Email error filtering if (!emailPattern.test(email)) { return NextResponse.json( { error: "Please enter a valid email address." }, @@ -89,17 +147,37 @@ export async function POST(request: Request) { const safeSubject = escapeHtml(subject); const safeEmail = escapeHtml(email); const safeMessage = escapeHtml(message); + const safeClubInfo = escapeHtml(clubInfo); + const safeBusinessInfo = escapeHtml(businessInfo); + const safeProfessionInfo = escapeHtml(professionInfo); + const safeSponsorshipInfo = escapeHtml(sponsorshipInfo); + const safePartnershipInfo = escapeHtml(partnershipInfo); + const safeProfessorInfo = escapeHtml(professorInfo); + const safeOtherInfo = escapeHtml(otherInfo); + + const messageLinks = { + "Student": safeMessage, + "Club": safeClubInfo, + "Business": safeBusinessInfo, + "Industry Professional": safeProfessionInfo, + "Professor": safeProfessorInfo, + "Partnership": safePartnershipInfo, + "Sponsorship": safeSponsorshipInfo, + "Other": safeOtherInfo + } const textBody = [ "New Contact Request", "", `Name: ${fullName || "Website Visitor"}`, + `Type of Contact: ${typeOfContact}`, + ...(typeOfContact === "Student" ? [`Year: ${yearOfStudy}`] : []), `Email: ${email}`, `Subject: ${subject}`, `Submitted: ${submittedAt}`, "", "Message:", - message + messageLinks[typeOfContact] ].join("\n"); const htmlBody = ` @@ -111,6 +189,21 @@ export async function POST(request: Request) { Name ${safeName} + + Type of Contact + ${typeOfContact} + + ${typeOfContact === "Student" ? + ` + + School Year + ${yearOfStudy} + + + Major + ${fieldOfStudy} + + ` : ""} Email ${safeEmail} @@ -127,7 +220,7 @@ export async function POST(request: Request) {
Message
- ${safeMessage} + ${messageLinks[typeOfContact]}

Submitted from the Waybionic website contact form. diff --git a/waybionic/src/app/contact/components/NonStudent.tsx b/waybionic/src/app/contact/components/NonStudent.tsx new file mode 100644 index 0000000..1a8bfcb --- /dev/null +++ b/waybionic/src/app/contact/components/NonStudent.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import NonStudentMessage from './NonStudentMessage' +import NonStudentInput from './NonStudentInput' +import emailjs from "@emailjs/browser"; + +const NonStudent = ({ typeOfContact, setClubInfo, setBusinessInfo, setProfessionInfo, setSponsorshipInfo, setPartnershipInfo, setOtherInfo, clubInfo, businessInfo, professionInfo, sponsorshipInfo, partnershipInfo, otherInfo, professorInfo, setProfessorInfo }) => { + const getStateValue = () => { + switch(typeOfContact) { + case "Club": + return clubInfo + case "Business": + return businessInfo + case "Industry Professional": + return professionInfo + case "Sponsorship": + return sponsorshipInfo + case "Partnership": + return partnershipInfo + case "Professor": + return professorInfo + default: + return otherInfo + } + } + + const setStateValue = (value: string) => { + switch(typeOfContact) { + case "Club": + setClubInfo(value) + break + case "Business": + setBusinessInfo(value) + break + case "Industry Professional": + setProfessionInfo(value) + break + case "Sponsorship": + setSponsorshipInfo(value) + break + case "Partnership": + setPartnershipInfo(value) + break + case "Professor": + setProfessorInfo(value) + break + default: + setOtherInfo(value) + } + } + + if (typeOfContact && typeOfContact !== "Student") { + return ( + <> +

+ + +
+ + ) + } + + return null +} + +export default NonStudent \ No newline at end of file diff --git a/waybionic/src/app/contact/components/NonStudentInput.tsx b/waybionic/src/app/contact/components/NonStudentInput.tsx new file mode 100644 index 0000000..d4a193f --- /dev/null +++ b/waybionic/src/app/contact/components/NonStudentInput.tsx @@ -0,0 +1,19 @@ +import React from 'react' + +const NonStudentInput = ({ getStateValue, setStateValue}) => { + return ( + <> +