diff --git a/.env.example b/.env.example index 8b28778..ee82653 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_id NEXT_PUBLIC_SANITY_DATASET=production NEXT_PUBLIC_SANITY_API_VERSION=2024-01-18 -# Sanity CMS Configuration (Sanity CLI: dev, deploy) -SANITY_STUDIO_PROJECT_ID=your_project_id -SANITY_STUDIO_DATASET=production -SANITY_STUDIO_API_VERSION=2024-01-18 +# Resend Configuration +# Required: used to send contact form emails via Resend +RESEND_API_KEY=your_resend_api_key + diff --git a/app/api/send/route.ts b/app/api/send/route.ts new file mode 100644 index 0000000..7f990ea --- /dev/null +++ b/app/api/send/route.ts @@ -0,0 +1,100 @@ +import { EmailTemplate } from '../../../components/EmailTemplate'; +import { Resend } from 'resend'; + +// Validate RESEND_API_KEY is configured +const apiKey = process.env.RESEND_API_KEY; +if (!apiKey || apiKey.trim().length === 0) { + throw new Error( + 'RESEND_API_KEY is not configured. Please set the RESEND_API_KEY environment variable.' + ); +} + +const resend = new Resend(apiKey); + +function isNonEmptyString(value: unknown): value is string { + return typeof value === 'string' && value.trim().length > 0; +} + +function isValidEmail(value: unknown): value is string { + if (typeof value !== 'string') return false; + const email = value.trim(); + // Basic email pattern; not exhaustive but sufficient for simple validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return email.length > 0 && emailRegex.test(email); +} + +export async function POST(req: Request) { + try { + // Get the form data from the request + const body = await req.json(); + + if (!body || typeof body !== 'object') { + return Response.json( + { error: 'Invalid request body; expected JSON object.' }, + { status: 400 }, + ); + } + + const { + name, + emailAddress, + subject, + message, + } = body as { + name?: unknown; + emailAddress?: unknown; + subject?: unknown; + message?: unknown; + }; + + if (!isNonEmptyString(name)) { + return Response.json( + { error: 'Field "name" is required and must be a non-empty string.' }, + { status: 400 }, + ); + } + + if (!isValidEmail(emailAddress)) { + return Response.json( + { error: 'Field "emailAddress" is required and must be a valid email address.' }, + { status: 400 }, + ); + } + + if (!isNonEmptyString(message)) { + return Response.json( + { error: 'Field "message" is required and must be a non-empty string.' }, + { status: 400 }, + ); + } + + const normalizedSubject = + typeof subject === 'string' && subject.trim().length > 0 + ? subject + : 'New Message from Monash Coding Site'; + + const { data, error } = await resend.emails.send({ + from: 'noreply@monashcoding.com', + // to: 'coding@monashclubs.org', + to: 'projects@monashcoding.com', + replyTo: (emailAddress as string).trim(), // User's email will be set as reply-to + subject: normalizedSubject, + react: EmailTemplate({ + name: (name as string).trim(), + emailAddress: (emailAddress as string).trim(), + subject: normalizedSubject, + message: (message as string).trim(), + }), + }); + + if (error) { + console.error('Resend API error:', error); + return Response.json({ error }, { status: 500 }); + } + + return Response.json(data); + } catch (error) { + console.error('Catch error:', error); + return Response.json({ error: String(error) }, { status: 500 }); + } +} \ No newline at end of file diff --git a/components/ContactPageClient.tsx b/components/ContactPageClient.tsx index c70daab..e4ebc04 100644 --- a/components/ContactPageClient.tsx +++ b/components/ContactPageClient.tsx @@ -1,6 +1,7 @@ "use client"; -import { motion } from "framer-motion"; +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import { ContactPageData, ContactSocialLink } from "@/lib/sanity/types"; import { RibbonAwareSection } from "@/components/RibbonAwareSection"; @@ -66,6 +67,18 @@ interface ContactPageClientProps { } export default function ContactPageClient({ data }: ContactPageClientProps) { + // Form state + const [formData, setFormData] = useState({ + name: '', + email: '', + subject: '', + message: '', + }); + + // Error state + const [errors, setErrors] = useState>({}); + + // Use Sanity data or fallbacks const pageTitle = data?.pageTitle || "Get in Touch"; const pageSubtitle = data?.pageSubtitle || "Have a question or want to collaborate? We'd love to hear from you."; @@ -76,10 +89,80 @@ export default function ContactPageClient({ data }: ContactPageClientProps) { const locationMapLink = data?.locationMapLink || "https://maps.google.com/?q=Monash+University+Clayton"; const socialLinks = data?.socialLinks || defaultSocialLinks; + // Handle form input changes. Clears errors on change. + const handleInputChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + + setErrors((prev) => ({ + ...prev, + [name]: "", + })); + }; + + + // Validate form fields +const validate = () => { + const newErrors: Record = {}; + + if (!formData.name.trim()) newErrors.name = "Name is required"; + if (!formData.email.trim()) newErrors.email = "Email is required"; + if (!formData.subject.trim()) newErrors.subject = "Subject is required"; + if (!formData.message.trim()) newErrors.message = "Message is required"; + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; +}; + +const handleSendEmail = async ( + e: React.FormEvent +) => { + e.preventDefault(); + if (!validate()) return; + + try { + const response = await fetch("/api/send", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: formData.name, + emailAddress: formData.email, + subject: formData.subject, + message: formData.message, + }), + }); + + if (!response.ok) { + throw new Error("Failed to send email"); + } + + alert("Email sent successfully!"); + + setFormData({ + name: "", + email: "", + subject: "", + message: "", + }); + } catch (error) { + console.error("Error sending email:", error); + alert("Error sending email"); + } +}; + + return (
@@ -92,7 +175,7 @@ export default function ContactPageClient({ data }: ContactPageClientProps) { {pageTitle} + +
+ + {/* NAME */} +
+ + + + {errors.name && ( + + {errors.name} + + )} + +
+ + {/* EMAIL */} +
+ + + + {errors.email && ( + + {errors.email} + + )} + +
+ + {/* SUBJECT */} +
+ + + + {errors.subject && ( + + {errors.subject} + + )} + +
+ + {/* MESSAGE */} +
+ +