diff --git a/data/contact-submissions.json b/data/contact-submissions.json new file mode 100644 index 00000000..b559cc62 --- /dev/null +++ b/data/contact-submissions.json @@ -0,0 +1,47 @@ +[ + { + "id": "contact_1739124000000_a1b2c3", + "name": "Avery Lane", + "email": "avery@example.com", + "subject": "Question about premium access", + "message": "Hi team, I want to understand what is included in the premium plan for small agent teams.", + "type": "general", + "submittedAt": "2026-02-01T16:00:00.000Z" + }, + { + "id": "contact_1739131200000_d4e5f6", + "name": "Morgan Chen", + "email": "morgan@opsbot.ai", + "subject": "Need help with setup", + "message": "Our CI workflow fails during bootstrap. Could someone point us to the right debugging guide?", + "type": "support", + "submittedAt": "2026-02-01T18:00:00.000Z" + }, + { + "id": "contact_1739203200000_g7h8i9", + "name": "Jordan Patel", + "email": "jordan@partnerco.dev", + "subject": "Potential integration partnership", + "message": "We are building agent tooling and would like to discuss a partnership and listing opportunities.", + "type": "partnership", + "submittedAt": "2026-02-02T14:00:00.000Z" + }, + { + "id": "contact_1739289600000_j1k2l3", + "name": "Sam Rivera", + "email": "sam.rivera@example.net", + "subject": "Feedback on onboarding", + "message": "The onboarding flow is smooth overall, but adding one more example for environment setup would help.", + "type": "feedback", + "submittedAt": "2026-02-03T14:00:00.000Z" + }, + { + "id": "contact_1739376000000_m4n5o6", + "name": "Taylor Brooks", + "email": "taylor@agentlabs.io", + "subject": "Support response time question", + "message": "Can you share expected response times for technical support requests submitted on weekends?", + "type": "support", + "submittedAt": "2026-02-04T14:00:00.000Z" + } +] diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts new file mode 100644 index 00000000..bfc274e8 --- /dev/null +++ b/src/app/api/contact/route.ts @@ -0,0 +1,125 @@ +import { NextRequest, NextResponse } from "next/server"; +import { promises as fs } from "fs"; +import path from "path"; + +const CONTACT_SUBMISSIONS_PATH = path.join( + process.cwd(), + "data", + "contact-submissions.json" +); + +const CONTACT_TYPES = ["general", "support", "partnership", "feedback"] as const; + +type ContactType = (typeof CONTACT_TYPES)[number]; + +type ContactSubmission = { + id: string; + name: string; + email: string; + subject: string; + message: string; + type: ContactType; + submittedAt: string; +}; + +type ContactPayload = { + name?: unknown; + email?: unknown; + subject?: unknown; + message?: unknown; + type?: unknown; +}; + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +function toTrimmedString(value: unknown): string { + return typeof value === "string" ? value.trim() : ""; +} + +function validateContactPayload(payload: ContactPayload): string[] { + const errors: string[] = []; + const name = toTrimmedString(payload.name); + const email = toTrimmedString(payload.email); + const subject = toTrimmedString(payload.subject); + const message = toTrimmedString(payload.message); + const type = payload.type; + + if (name.length < 2) { + errors.push("Name must be at least 2 characters long."); + } + + if (!EMAIL_REGEX.test(email)) { + errors.push("Email must be a valid email address."); + } + + if (subject.length < 3) { + errors.push("Subject must be at least 3 characters long."); + } + + if (message.length < 10) { + errors.push("Message must be at least 10 characters long."); + } + + if (!CONTACT_TYPES.includes(type as ContactType)) { + errors.push("Type must be one of: general, support, partnership, feedback."); + } + + return errors; +} + +async function readSubmissions(): Promise { + try { + const raw = await fs.readFile(CONTACT_SUBMISSIONS_PATH, "utf-8"); + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? (parsed as ContactSubmission[]) : []; + } catch { + return []; + } +} + +async function writeSubmissions(submissions: ContactSubmission[]): Promise { + await fs.writeFile(CONTACT_SUBMISSIONS_PATH, JSON.stringify(submissions, null, 2)); +} + +export async function POST(request: NextRequest) { + try { + const body = (await request.json()) as ContactPayload; + const errors = validateContactPayload(body); + + if (errors.length > 0) { + return NextResponse.json( + { success: false, error: "Validation failed", details: errors }, + { status: 400 } + ); + } + + const submission: ContactSubmission = { + id: `contact_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + name: toTrimmedString(body.name), + email: toTrimmedString(body.email).toLowerCase(), + subject: toTrimmedString(body.subject), + message: toTrimmedString(body.message), + type: body.type as ContactType, + submittedAt: new Date().toISOString(), + }; + + const submissions = await readSubmissions(); + submissions.push(submission); + await writeSubmissions(submissions); + + return NextResponse.json( + { + success: true, + message: "Contact form submitted successfully.", + submissionId: submission.id, + }, + { status: 201 } + ); + } catch (error) { + console.error("Contact submission error:", error); + return NextResponse.json( + { success: false, error: "Invalid request. Expected JSON body." }, + { status: 400 } + ); + } +} diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx index b3e447a6..d01fb592 100644 --- a/src/app/contact/page.tsx +++ b/src/app/contact/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/no-unescaped-entities */ "use client"; import { useState } from "react"; @@ -8,37 +9,119 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Mail, Github, MessageSquare, MapPin, Clock } from "lucide-react"; +type ContactType = "general" | "support" | "partnership" | "feedback"; + +type FormData = { + name: string; + email: string; + subject: string; + message: string; + type: ContactType; +}; + +const INITIAL_FORM: FormData = { + name: "", + email: "", + subject: "", + message: "", + type: "general", +}; + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + export default function ContactPage() { const webPageJsonLd = { "@context": "https://schema.org", "@type": "ContactPage", name: "Contact — forAgents.dev", description: "Get in touch with the forAgents.dev team. Send us a message or reach out via email, GitHub, or Discord.", - url: "https://foragents.dev/contact" + url: "https://foragents.dev/contact", }; - const [formData, setFormData] = useState({ - name: "", - email: "", - subject: "", - message: "", - }); - const [submitted, setSubmitted] = useState(false); + const [formData, setFormData] = useState(INITIAL_FORM); + const [fieldErrors, setFieldErrors] = useState>>({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + const validateForm = (data: FormData) => { + const errors: Partial> = {}; - const handleSubmit = (e: React.FormEvent) => { + if (data.name.trim().length < 2) errors.name = "Name must be at least 2 characters."; + if (!EMAIL_REGEX.test(data.email.trim())) errors.email = "Enter a valid email address."; + if (data.subject.trim().length < 3) errors.subject = "Subject must be at least 3 characters."; + if (data.message.trim().length < 10) errors.message = "Message must be at least 10 characters."; + if (!data.type) errors.type = "Please select a message type."; + + return errors; + }; + + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setSubmitted(true); - // Reset form after 3 seconds - setTimeout(() => { - setFormData({ name: "", email: "", subject: "", message: "" }); - setSubmitted(false); - }, 3000); + setSuccessMessage(""); + setErrorMessage(""); + + const errors = validateForm(formData); + setFieldErrors(errors); + + if (Object.keys(errors).length > 0) { + setErrorMessage("Please fix the highlighted fields before submitting."); + return; + } + + try { + setIsSubmitting(true); + + const response = await fetch("/api/contact", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: formData.name.trim(), + email: formData.email.trim(), + subject: formData.subject.trim(), + message: formData.message.trim(), + type: formData.type, + }), + }); + + const payload = (await response.json()) as { + success?: boolean; + error?: string; + details?: string[]; + submissionId?: string; + }; + + if (!response.ok || !payload.success) { + const detailText = Array.isArray(payload.details) ? ` (${payload.details.join(" ")})` : ""; + setErrorMessage(payload.error ? `${payload.error}${detailText}` : "Failed to send message. Please try again."); + return; + } + + setFormData(INITIAL_FORM); + setFieldErrors({}); + setSuccessMessage( + payload.submissionId + ? `Message sent successfully! Submission ID: ${payload.submissionId}` + : "Message sent successfully!" + ); + } catch { + setErrorMessage("Something went wrong while submitting. Please try again."); + } finally { + setIsSubmitting(false); + } }; const handleChange = ( e: React.ChangeEvent ) => { - setFormData({ ...formData, [e.target.name]: e.target.value }); + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + + if (fieldErrors[name as keyof FormData]) { + setFieldErrors((prev) => ({ ...prev, [name]: undefined })); + } }; return ( @@ -48,8 +131,6 @@ export default function ContactPage() { dangerouslySetInnerHTML={{ __html: JSON.stringify(webPageJsonLd) }} /> - - {/* Hero Section */}
@@ -59,142 +140,132 @@ export default function ContactPage() {

Get in Touch

-

- We'd love to hear from you -

+

We'd love to hear from you

- {/* Main Content */}
- {/* Contact Form */}
Send us a message - {submitted ? ( -
-
- - - -
-

Message sent!

-

- Thank you for reaching out. We'll get back to you soon. -

+ {successMessage ? ( +
+ {successMessage} +
+ ) : null} + + {errorMessage ? ( +
+ {errorMessage}
- ) : ( -
-
- - -
- -
- - -
- -
- - -
- -
- -