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:
parent
ca63536783
commit
850b4e6a2f
|
|
@ -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'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 */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue