diff --git a/apps/web/app/api/contact/route.js b/apps/web/app/api/contact/route.js index a48d592..29e388f 100644 --- a/apps/web/app/api/contact/route.js +++ b/apps/web/app/api/contact/route.js @@ -1,6 +1,10 @@ import { createContactRoute } from '@profullstack/stack/email'; +import { contactGuard } from '@/lib/contact-guard'; export const POST = createContactRoute({ + // Requires a token minted when the form rendered. Runs before field + // validation, so a bot never learns which fields the route wants. + guard: contactGuard ?? undefined, from: 'MarkSyncr Contact ', to: 'support@marksyncr.com', requiredFields: ['name', 'email', 'subject', 'message'], diff --git a/apps/web/app/contact/ContactPageClient.jsx b/apps/web/app/contact/ContactPageClient.jsx new file mode 100644 index 0000000..918198a --- /dev/null +++ b/apps/web/app/contact/ContactPageClient.jsx @@ -0,0 +1,311 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import Image from 'next/image'; + +export default function ContactPageClient({ token, tokenName, honeypotName }) { + // Honeypot. Nothing visible sets this, so anything in it came from a bot. + const [honeypot, setHoneypot] = useState(''); + const [formData, setFormData] = useState({ + name: '', + email: '', + subject: '', + message: '', + }); + const [status, setStatus] = useState({ type: '', message: '' }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setIsSubmitting(true); + setStatus({ type: '', message: '' }); + + try { + const response = await fetch('/api/contact', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...formData, + ...(tokenName && token ? { [tokenName]: token } : {}), + ...(honeypotName ? { [honeypotName]: honeypot } : {}), + }), + }); + + const data = await response.json(); + + if (response.ok) { + setStatus({ + type: 'success', + message: 'Thank you! Your message has been sent successfully.', + }); + setFormData({ name: '', email: '', subject: '', message: '' }); + } else { + setStatus({ + type: 'error', + message: data.error || 'Failed to send message. Please try again.', + }); + } + } catch (error) { + setStatus({ + type: 'error', + message: 'An unexpected error occurred. Please try again later.', + }); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ {/* Navigation */} + + + {/* Main Content */} +
+
+
+

Contact Us

+

+ Have a question or need help? We'd love to hear from you. +

+
+ + {/* Contact Form */} +
+ {/* Honeypot. Off-canvas rather than display:none, because some + bots skip fields they can tell are not rendered. The route had + one configured all along but never rendered it. */} + {honeypotName && ( + + )} + {status.message && ( +
+ {status.message} +
+ )} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +