253 lines
9.6 KiB
TypeScript
253 lines
9.6 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
|
|
|
type FormData = {
|
|
name: string;
|
|
email: string;
|
|
message: string;
|
|
};
|
|
|
|
type FormErrors = {
|
|
name?: string;
|
|
email?: string;
|
|
message?: string;
|
|
};
|
|
|
|
export function ContactForm() {
|
|
const [formData, setFormData] = useState<FormData>({
|
|
name: "",
|
|
email: "",
|
|
message: "",
|
|
});
|
|
|
|
const [errors, setErrors] = useState<FormErrors>({});
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [showToast, setShowToast] = useState(false);
|
|
|
|
const handleInputChange = (
|
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
) => {
|
|
const { name, value } = e.target;
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
[name]: value,
|
|
}));
|
|
// Clear error for this field as the user types
|
|
if (errors[name as keyof FormErrors]) {
|
|
setErrors((prev) => ({
|
|
...prev,
|
|
[name]: undefined,
|
|
}));
|
|
}
|
|
};
|
|
|
|
const validateForm = (): boolean => {
|
|
const tempErrors: FormErrors = {};
|
|
let isValid = true;
|
|
|
|
if (!formData.name.trim()) {
|
|
tempErrors.name = "Name is required";
|
|
isValid = false;
|
|
}
|
|
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!formData.email.trim()) {
|
|
tempErrors.email = "Email is required";
|
|
isValid = false;
|
|
} else if (!emailRegex.test(formData.email)) {
|
|
tempErrors.email = "Please enter a valid email address";
|
|
isValid = false;
|
|
}
|
|
|
|
if (!formData.message.trim()) {
|
|
tempErrors.message = "Message is required";
|
|
isValid = false;
|
|
} else if (formData.message.trim().length < 10) {
|
|
tempErrors.message = "Message must be at least 10 characters long";
|
|
isValid = false;
|
|
}
|
|
|
|
setErrors(tempErrors);
|
|
return isValid;
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!validateForm()) return;
|
|
|
|
setIsSubmitting(true);
|
|
|
|
// Simulate API Request delay
|
|
await new Promise((resolve) => setTimeout(resolve, 1200));
|
|
|
|
setIsSubmitting(false);
|
|
setShowToast(true);
|
|
setFormData({ name: "", email: "", message: "" });
|
|
|
|
// Auto-hide toast after 5 seconds
|
|
setTimeout(() => {
|
|
setShowToast(false);
|
|
}, 5000);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="lg:col-span-7 bg-white rounded-3xl p-8 md:p-10 border border-outline-variant/25 shadow-xl relative overflow-hidden">
|
|
{/* Background Accent Mesh */}
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/5 rounded-full blur-3xl" />
|
|
<div className="absolute bottom-0 left-0 w-32 h-32 bg-secondary/5 rounded-full blur-3xl" />
|
|
|
|
<div className="relative z-10">
|
|
<span className="text-label-sm font-black text-primary uppercase tracking-widest">
|
|
Write to us
|
|
</span>
|
|
<h2 className="font-display text-2xl md:text-3xl font-black text-brand-dark mt-2 mb-8">
|
|
Send Message
|
|
</h2>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Name field */}
|
|
<div className="flex flex-col">
|
|
<label htmlFor="field-name" className="text-label-sm text-on-surface-variant font-bold mb-2">
|
|
Your Name
|
|
</label>
|
|
<div className="relative">
|
|
<span className="absolute inset-y-0 left-0 flex items-center pl-4 text-on-surface-variant/40">
|
|
<MaterialIcon icon="person" size={20} />
|
|
</span>
|
|
<input
|
|
type="text"
|
|
id="field-name"
|
|
name="name"
|
|
value={formData.name}
|
|
onChange={handleInputChange}
|
|
placeholder="e.g. John Doe"
|
|
className={`w-full pl-11 pr-4 py-3.5 bg-surface rounded-xl border ${
|
|
errors.name ? "border-error" : "border-outline-variant/30"
|
|
} text-body-md text-on-surface placeholder:text-on-surface-variant/45 focus:outline-none focus:ring-2 ${
|
|
errors.name ? "focus:ring-error/20" : "focus:ring-primary/20"
|
|
} focus:border-primary transition-all`}
|
|
/>
|
|
</div>
|
|
{errors.name && (
|
|
<span className="text-body-sm text-error font-bold mt-1.5 flex items-center gap-1">
|
|
<MaterialIcon icon="error" size={14} />
|
|
{errors.name}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Email field */}
|
|
<div className="flex flex-col">
|
|
<label htmlFor="field-email" className="text-label-sm text-on-surface-variant font-bold mb-2">
|
|
Your Email
|
|
</label>
|
|
<div className="relative">
|
|
<span className="absolute inset-y-0 left-0 flex items-center pl-4 text-on-surface-variant/40">
|
|
<MaterialIcon icon="email" size={20} />
|
|
</span>
|
|
<input
|
|
type="email"
|
|
id="field-email"
|
|
name="email"
|
|
value={formData.email}
|
|
onChange={handleInputChange}
|
|
placeholder="e.g. john@example.com"
|
|
className={`w-full pl-11 pr-4 py-3.5 bg-surface rounded-xl border ${
|
|
errors.email ? "border-error" : "border-outline-variant/30"
|
|
} text-body-md text-on-surface placeholder:text-on-surface-variant/45 focus:outline-none focus:ring-2 ${
|
|
errors.email ? "focus:ring-error/20" : "focus:ring-primary/20"
|
|
} focus:border-primary transition-all`}
|
|
/>
|
|
</div>
|
|
{errors.email && (
|
|
<span className="text-body-sm text-error font-bold mt-1.5 flex items-center gap-1">
|
|
<MaterialIcon icon="error" size={14} />
|
|
{errors.email}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Message field */}
|
|
<div className="flex flex-col">
|
|
<label htmlFor="field-message" className="text-label-sm text-on-surface-variant font-bold mb-2">
|
|
Message
|
|
</label>
|
|
<div className="relative">
|
|
<span className="absolute top-3.5 left-4 text-on-surface-variant/40">
|
|
<MaterialIcon icon="chat_bubble" size={20} />
|
|
</span>
|
|
<textarea
|
|
id="field-message"
|
|
name="message"
|
|
rows={5}
|
|
value={formData.message}
|
|
onChange={handleInputChange}
|
|
placeholder="Describe your inquiry in detail (e.g. partnership options, tech support, etc.)"
|
|
className={`w-full pl-11 pr-4 py-3.5 bg-surface rounded-xl border ${
|
|
errors.message ? "border-error" : "border-outline-variant/30"
|
|
} text-body-md text-on-surface placeholder:text-on-surface-variant/45 focus:outline-none focus:ring-2 ${
|
|
errors.message ? "focus:ring-error/20" : "focus:ring-primary/20"
|
|
} focus:border-primary transition-all resize-none`}
|
|
/>
|
|
</div>
|
|
{errors.message && (
|
|
<span className="text-body-sm text-error font-bold mt-1.5 flex items-center gap-1">
|
|
<MaterialIcon icon="error" size={14} />
|
|
{errors.message}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Submit button */}
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="w-full bg-primary hover:bg-primary-container disabled:bg-primary-container/60 text-on-primary font-black px-8 py-4 rounded-xl text-label-md hover:shadow-lg transition-all duration-300 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] flex items-center justify-center gap-2 cursor-pointer"
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
<span>Sending Message...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<MaterialIcon icon="send" size={18} />
|
|
<span>Submit Message</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ─── Success Toast Notification ─── */}
|
|
{showToast && (
|
|
<div className="fixed bottom-8 right-8 z-50 animate-bounce-subtle max-w-sm w-full bg-brand-dark-surface border border-brand-accent/30 rounded-2xl shadow-2xl p-4 flex gap-4 items-start">
|
|
<div className="w-10 h-10 rounded-xl bg-secondary text-white flex items-center justify-center shrink-0">
|
|
<MaterialIcon icon="check_circle" size={24} />
|
|
</div>
|
|
<div className="flex-1">
|
|
<h4 className="text-body-md font-black text-white">Message Sent!</h4>
|
|
<p className="text-body-sm text-white/70 mt-1 leading-normal">
|
|
Thank you for reaching out. We will get back to you within 24 hours.
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowToast(false)}
|
|
className="text-white/40 hover:text-white transition-colors cursor-pointer shrink-0"
|
|
aria-label="Close Toast"
|
|
>
|
|
<MaterialIcon icon="close" size={18} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|