Wire contact form to self-hosted email relay API

Replace broken mailto: form with proper fetch-based submission
to email-relay.jeffemmett.com/contact. Adds loading spinner,
success confirmation, and error feedback states.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-20 15:55:20 -07:00
parent ca63536783
commit 850b4e6a2f
1 changed files with 79 additions and 7 deletions

View File

@ -1,11 +1,54 @@
"use client"; "use client";
import { useState } from "react";
import { useInView } from "@/hooks/use-in-view"; import { useInView } from "@/hooks/use-in-view";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Mail, Phone, MapPin, Zap } from "lucide-react"; import { Mail, Phone, MapPin, Zap, Check, AlertCircle, Loader2 } from "lucide-react";
const CONTACT_API = "https://email-relay.jeffemmett.com/contact";
export function ContactSection() { export function ContactSection() {
const { ref, isInView } = useInView(0.1); const { ref, isInView } = useInView(0.1);
const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");
const [errorMsg, setErrorMsg] = useState("");
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus("sending");
setErrorMsg("");
const form = e.currentTarget;
const data = new FormData(form);
try {
const res = await fetch(CONTACT_API, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: data.get("name"),
email: data.get("email"),
subject: "PortaPower Event Inquiry",
message: data.get("message") || "",
fields: {
"Festival / Event": data.get("festival") || "Not specified",
"Expected Attendance": data.get("attendees") || "Not specified",
},
}),
});
if (res.ok) {
setStatus("sent");
form.reset();
} else {
const body = await res.json().catch(() => ({}));
setErrorMsg(body.error || "Something went wrong. Please try again.");
setStatus("error");
}
} catch {
setErrorMsg("Network error. Please check your connection and try again.");
setStatus("error");
}
}
return ( return (
<section id="contact" className="py-24 px-4 bg-brown-dark/30" ref={ref}> <section id="contact" className="py-24 px-4 bg-brown-dark/30" ref={ref}>
@ -34,10 +77,27 @@ export function ContactSection() {
className={`${isInView ? "animate-fade-in-up" : "opacity-0"}`} className={`${isInView ? "animate-fade-in-up" : "opacity-0"}`}
style={{ animationDelay: "0.2s" }} style={{ animationDelay: "0.2s" }}
> >
{status === "sent" ? (
<div className="flex flex-col items-center justify-center gap-4 py-16 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-neon/10 border border-neon/30">
<Check className="h-8 w-8 text-neon" />
</div>
<h3 className="text-2xl font-bold text-cream">Message Sent!</h3>
<p className="text-cream-dim max-w-sm">
Thanks for reaching out. We&apos;ll get back to you shortly.
</p>
<Button
type="button"
variant="outline"
onClick={() => setStatus("idle")}
className="mt-4"
>
Send Another
</Button>
</div>
) : (
<form <form
action="mailto:hello@portapower.buzz" onSubmit={handleSubmit}
method="POST"
encType="text/plain"
className="space-y-5" className="space-y-5"
> >
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
@ -128,11 +188,23 @@ export function ContactSection() {
/> />
</div> </div>
<Button type="submit" size="lg" className="w-full"> {status === "error" && (
<Zap className="h-5 w-5" /> <div className="flex items-center gap-2 rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-400">
Send Inquiry <AlertCircle className="h-4 w-4 flex-shrink-0" />
{errorMsg}
</div>
)}
<Button type="submit" size="lg" className="w-full" disabled={status === "sending"}>
{status === "sending" ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<Zap className="h-5 w-5" />
)}
{status === "sending" ? "Sending..." : "Send Inquiry"}
</Button> </Button>
</form> </form>
)}
</div> </div>
{/* Contact info */} {/* Contact info */}