250 lines
13 KiB
TypeScript
250 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { User, Phone, Mail, MapPin, Send } from "lucide-react";
|
|
import Image from "next/image";
|
|
import { useState, ChangeEvent, FormEvent, useEffect } from "react";
|
|
import ReCAPTCHA from "react-google-recaptcha";
|
|
import axios from "axios";
|
|
|
|
interface FormData {
|
|
name: string;
|
|
phone: string;
|
|
email: string;
|
|
location: string;
|
|
}
|
|
|
|
interface FormErrors {
|
|
name?: string;
|
|
phone?: string;
|
|
email?: string;
|
|
location?: string;
|
|
captcha?: string;
|
|
}
|
|
|
|
export default function ContactCTA() {
|
|
const [formData, setFormData] = useState<FormData>({
|
|
name: "",
|
|
phone: "",
|
|
email: "",
|
|
location: "",
|
|
});
|
|
|
|
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
|
const [formErrors, setFormErrors] = useState<FormErrors>({});
|
|
const [alert, setAlert] = useState<{ show: boolean; type: string; message: string }>({
|
|
show: false,
|
|
type: "",
|
|
message: "",
|
|
});
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
|
const { name, value } = e.target;
|
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
|
// Clear error when user types
|
|
if (formErrors[name as keyof FormErrors]) {
|
|
setFormErrors(prev => ({ ...prev, [name]: undefined }));
|
|
}
|
|
};
|
|
|
|
const handleCaptchaChange = (token: string | null) => {
|
|
setCaptchaToken(token);
|
|
if (token && formErrors.captcha) {
|
|
setFormErrors(prev => ({ ...prev, captcha: undefined }));
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
const errors: FormErrors = {};
|
|
if (!formData.name.trim()) errors.name = "Name is required.";
|
|
if (!formData.phone.trim()) errors.phone = "Phone number is required.";
|
|
if (!formData.email.trim()) errors.email = "Email is required.";
|
|
if (!formData.location) errors.location = "Please select a location.";
|
|
if (!captchaToken) errors.captcha = "Please verify the CAPTCHA.";
|
|
|
|
setFormErrors(errors);
|
|
if (Object.keys(errors).length > 0) return;
|
|
|
|
setIsSubmitting(true);
|
|
|
|
const emailData = {
|
|
name: formData.name,
|
|
phone: formData.phone,
|
|
email: formData.email,
|
|
subject: `New Inquiry from ${formData.name} - ${formData.location}`,
|
|
message: `Name: ${formData.name}<br />Phone: ${formData.phone}<br />Email: ${formData.email}<br />Location Interest: ${formData.location}`,
|
|
to: "hello@skyandsoil.com",
|
|
senderName: "Sky and Soil Contact Form",
|
|
recaptchaToken: captchaToken,
|
|
};
|
|
|
|
try {
|
|
const res = await axios.post("https://mailserver.metatronnest.com/send", emailData, {
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
|
|
setAlert({
|
|
show: true,
|
|
type: "success",
|
|
message: res?.data?.message || "Message sent successfully! We will contact you soon.",
|
|
});
|
|
|
|
setFormData({ name: "", phone: "", email: "", location: "" });
|
|
setCaptchaToken(null);
|
|
setFormErrors({});
|
|
} catch (error) {
|
|
setAlert({
|
|
show: true,
|
|
type: "danger",
|
|
message: "Failed to send message. Please try again later.",
|
|
});
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (alert.show) {
|
|
const timer = setTimeout(() => {
|
|
setAlert(prev => ({ ...prev, show: false }));
|
|
}, 5000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [alert.show]);
|
|
|
|
return (
|
|
<section id="contact" className="relative py-24 bg-[#f3f1e6] dark:bg-gray-900">
|
|
|
|
<div className="relative z-10 max-w-6xl mx-auto px-6">
|
|
<div className="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl overflow-hidden border border-gray-100 dark:border-gray-700 flex flex-col md:flex-row">
|
|
|
|
{/* Left Side - Form */}
|
|
<div className="w-full md:w-1/2 p-8 md:p-12">
|
|
<div className="text-left mb-8">
|
|
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
|
|
Ready to Visit Aurora Springs?
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-400">
|
|
Schedule a private tour or request a call back from our relationship manager.
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
{alert.show && (
|
|
<div className={`p-4 rounded-xl text-sm font-medium ${alert.type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-700 border border-red-200'}`}>
|
|
{alert.message}
|
|
</div>
|
|
)}
|
|
|
|
<div className="relative group">
|
|
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-primary transition-colors">
|
|
<User size={20} />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
name="name"
|
|
value={formData.name}
|
|
onChange={handleChange}
|
|
placeholder="Your Name"
|
|
className={`w-full pl-12 pr-4 py-4 rounded-xl bg-gray-50 dark:bg-gray-700/50 border ${formErrors.name ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'} text-foreground placeholder-gray-400 dark:placeholder-gray-500 focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all duration-300`}
|
|
/>
|
|
{formErrors.name && <small className="text-red-500 text-xs ml-1">{formErrors.name}</small>}
|
|
</div>
|
|
|
|
<div className="relative group">
|
|
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-primary transition-colors">
|
|
<Phone size={20} />
|
|
</div>
|
|
<input
|
|
type="tel"
|
|
name="phone"
|
|
value={formData.phone}
|
|
onChange={handleChange}
|
|
placeholder="Phone Number"
|
|
className={`w-full pl-12 pr-4 py-4 rounded-xl bg-gray-50 dark:bg-gray-700/50 border ${formErrors.phone ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'} text-foreground placeholder-gray-400 dark:placeholder-gray-500 focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all duration-300`}
|
|
/>
|
|
{formErrors.phone && <small className="text-red-500 text-xs ml-1">{formErrors.phone}</small>}
|
|
</div>
|
|
|
|
<div className="relative group">
|
|
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-primary transition-colors">
|
|
<Mail size={20} />
|
|
</div>
|
|
<input
|
|
type="email"
|
|
name="email"
|
|
value={formData.email}
|
|
onChange={handleChange}
|
|
placeholder="Email Address"
|
|
className={`w-full pl-12 pr-4 py-4 rounded-xl bg-gray-50 dark:bg-gray-700/50 border ${formErrors.email ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'} text-foreground placeholder-gray-400 dark:placeholder-gray-500 focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all duration-300`}
|
|
/>
|
|
{formErrors.email && <small className="text-red-500 text-xs ml-1">{formErrors.email}</small>}
|
|
</div>
|
|
|
|
<div className="relative group">
|
|
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-primary transition-colors">
|
|
<MapPin size={20} />
|
|
</div>
|
|
<select
|
|
name="location"
|
|
value={formData.location}
|
|
onChange={handleChange}
|
|
className={`w-full pl-12 pr-4 py-4 rounded-xl bg-gray-50 dark:bg-gray-700/50 border ${formErrors.location ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'} text-gray-600 dark:text-gray-300 focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all duration-300 appearance-none`}
|
|
>
|
|
<option value="">Select Preferred Location</option>
|
|
<option value="North Bengaluru">North Bengaluru</option>
|
|
<option value="Whitefield">Whitefield</option>
|
|
<option value="Sarjapur">Sarjapur</option>
|
|
</select>
|
|
<div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</div>
|
|
{formErrors.location && <small className="text-red-500 text-xs ml-1">{formErrors.location}</small>}
|
|
</div>
|
|
|
|
<div className="mt-2">
|
|
<ReCAPTCHA
|
|
sitekey="6Lea8ZYrAAAAAHaghaLjDx_K084IFATZby7Rzqhl"
|
|
onChange={handleCaptchaChange}
|
|
/>
|
|
{formErrors.captcha && <small className="text-red-500 text-xs ml-1">{formErrors.captcha}</small>}
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className={`w-full py-4 text-base font-semibold text-white bg-primary rounded-xl hover:bg-blue-600 transition-all shadow-lg hover:shadow-primary/30 active:scale-[0.98] flex items-center justify-center gap-2 group ${isSubmitting ? 'opacity-70 cursor-not-allowed' : ''}`}
|
|
>
|
|
<span>{isSubmitting ? 'Sending...' : 'Request a Call Back'}</span>
|
|
{!isSubmitting && <Send size={18} className="group-hover:translate-x-1 transition-transform" />}
|
|
</button>
|
|
</form>
|
|
|
|
<p className="mt-6 text-xs text-gray-400 dark:text-gray-500 flex items-center gap-2">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
We respect your privacy. No spam, ever.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Right Side - Image */}
|
|
<div className="w-full md:w-1/2 relative min-h-[400px] md:min-h-full">
|
|
<Image
|
|
src="/assets/images/home/ready.webp"
|
|
alt="Luxury Interior"
|
|
fill
|
|
className="object-cover"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent md:bg-gradient-to-l" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|