Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion waybionic/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions waybionic/package-lock.json

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

1 change: 1 addition & 0 deletions waybionic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
271 changes: 271 additions & 0 deletions waybionic/src/app/api/contact/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import { NextResponse } from "next/server";
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;
fullName?: string;
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, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}

// 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) {
return NextResponse.json(
{ error: "Invalid request payload." },
{ status: 400 }
);
}

// 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);

// 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<string, { value: string | null | undefined; error: string }> = {
"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." },
{ 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 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:",
messageLinks[typeOfContact]
].join("\n");

const htmlBody = `
<div style="font-family: Arial, Helvetica, sans-serif; color: #111827; line-height: 1.6;">
<h2 style="margin: 0 0 12px;">New Contact Request</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 16px;">
<tbody>
<tr>
<td style="padding: 6px 0; font-weight: 600; width: 110px;">Name</td>
<td style="padding: 6px 0;">${safeName}</td>
</tr>
<tr>
<td style="padding: 6px 0; font-weight: 600; width: 110px;">Type of Contact</td>
<td style="padding: 6px 0;">${typeOfContact}</td>
</tr>
${typeOfContact === "Student" ?
`
<tr>
<td style="padding: 6px 0; font-weight: 600; width: 110px;">School Year</td>
<td style="padding: 6px 0;">${yearOfStudy}</td>
</tr>
<tr>
<td style="padding: 6px 0; font-weight: 600; width: 110px;">Major</td>
<td style="padding: 6px 0;">${fieldOfStudy}</td>
</tr>
` : ""}
<tr>
<td style="padding: 6px 0; font-weight: 600;">Email</td>
<td style="padding: 6px 0;"><a href="mailto:${safeEmail}" style="color: #2563eb;">${safeEmail}</a></td>
</tr>
<tr>
<td style="padding: 6px 0; font-weight: 600;">Subject</td>
<td style="padding: 6px 0;">${safeSubject}</td>
</tr>
<tr>
<td style="padding: 6px 0; font-weight: 600;">Submitted</td>
<td style="padding: 6px 0;">${submittedAt}</td>
</tr>
</tbody>
</table>
<div style="font-weight: 600; margin-bottom: 6px;">Message</div>
<div style="white-space: pre-wrap; border: 1px solid #e5e7eb; padding: 12px; border-radius: 6px; background: #f9fafb;">
${messageLinks[typeOfContact]}
</div>
<p style="margin-top: 16px; font-size: 12px; color: #6b7280;">
Submitted from the Waybionic website contact form.
</p>
</div>
`;

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 });
}
65 changes: 65 additions & 0 deletions waybionic/src/app/contact/components/NonStudent.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div>
<NonStudentMessage typeOfContact={typeOfContact} />
<NonStudentInput getStateValue={getStateValue} setStateValue={setStateValue} />
</div>
</>
)
}

return null
}

export default NonStudent
Loading