diff --git a/frontend/components/ContactDialog.tsx b/frontend/components/ContactDialog.tsx new file mode 100644 index 0000000..19a431e --- /dev/null +++ b/frontend/components/ContactDialog.tsx @@ -0,0 +1,365 @@ +// src/components/ContactDialog.tsx +// ───────────────────────────────────────────────────────────────────────────── +// Wraps the existing in a Radix Dialog (shadcn/ui). +// Usage: +// ← renders its own "Contact us" trigger button +// } /> ← use a custom trigger element +// +// To wire up a real backend later, update the handleSubmit mock in ContactForm.tsx +// (look for "Mock API delay") and replace with your fetch/API call. +// ───────────────────────────────────────────────────────────────────────────── + +"use client"; + +import { useState } from "react"; +import { + MessageSquare, + Loader2, + X, + CheckCircle2, + AlertCircle, +} from "lucide-react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; + +// ─── Inline form (self-contained so the modal owns its own state) ───────────── + +type FormData = { + name: string; + email: string; + subject: string; + message: string; +}; +type Status = "idle" | "loading" | "success" | "error"; + +function ContactModalForm({ onSuccess }: { onSuccess?: () => void }) { + const [formData, setFormData] = useState({ + name: "", + email: "", + subject: "", + message: "", + }); + const [errors, setErrors] = useState>({}); + const [status, setStatus] = useState("idle"); + const [feedback, setFeedback] = useState(""); + const [honeypot, setHoneypot] = useState(""); + + const validate = (): boolean => { + const e: Partial = {}; + if (!formData.name.trim()) e.name = "Name is required."; + if (!formData.email.trim()) { + e.email = "Email is required."; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + e.email = "Enter a valid email address."; + } + if (!formData.message.trim()) e.message = "Message is required."; + if (formData.message.length > 500) + e.message = "Message cannot exceed 500 characters."; + setErrors(e); + return Object.keys(e).length === 0; + }; + + const handleChange = ( + e: React.ChangeEvent, + ) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + // Clear field error on change + if (errors[name as keyof FormData]) { + setErrors((prev) => ({ ...prev, [name]: undefined })); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (honeypot) { + setStatus("success"); + setFeedback("Message sent!"); + return; + } + if (!validate()) return; + + setStatus("loading"); + setFeedback(""); + + try { + // ── TODO: replace with real API call ────────────────────────────────── + // await fetch("/api/contact", { + // method: "POST", + // headers: { "Content-Type": "application/json" }, + // body: JSON.stringify(formData), + // }); + await new Promise((res) => setTimeout(res, 1500)); + // ───────────────────────────────────────────────────────────────────── + + setStatus("success"); + setFeedback("Thank you! We'll get back to you soon."); + setFormData({ name: "", email: "", subject: "", message: "" }); + onSuccess?.(); + } catch { + setStatus("error"); + setFeedback("Something went wrong. Please try again later."); + } + }; + + const field = + "w-full rounded-lg border bg-background px-4 py-2.5 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1 focus:ring-offset-background disabled:opacity-50"; + const fieldError = "border-destructive focus:ring-destructive"; + const fieldNormal = "border-border"; + + if (status === "success") { + return ( +
+
+ +
+
+

+ Message sent! +

+

+ {feedback} +

+
+
+ ); + } + + return ( +
+ {/* Honeypot */} + + + {/* Name + Email */} +
+
+ + + {errors.name && ( + + )} +
+ +
+ + + {errors.email && ( + + )} +
+
+ + {/* Subject (optional) */} +
+ + +
+ + {/* Message */} +
+ +