developer-portfolios/app/components/ContactSection.tsx

373 lines
14 KiB
TypeScript
Raw Normal View History

2025-04-11 01:05:41 +00:00
"use client";
import { useState, FormEvent, useEffect } from 'react';
import { motion } from 'framer-motion';
import { MailIcon, PhoneIcon, MapPinIcon, SendIcon, CheckCircleIcon, AlertCircleIcon } from 'lucide-react';
import { toast } from 'react-hot-toast';
import emailjs from 'emailjs-com';
interface ContactProps {
contact: {
email: string;
tel: string;
social: {
name: string;
url: string;
icon: any;
}[];
};
location: string;
}
type FormData = {
name: string;
email: string;
subject: string;
message: string;
};
type FormStatus = 'idle' | 'submitting' | 'success' | 'error';
const ContactSection: React.FC<ContactProps> = ({ contact, location }) => {
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
subject: '',
message: '',
});
const [formStatus, setFormStatus] = useState<FormStatus>('idle');
const [errors, setErrors] = useState<Partial<FormData>>({});
const validateForm = (): boolean => {
const newErrors: Partial<FormData> = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (!formData.subject.trim()) {
newErrors.subject = 'Subject is required';
}
if (!formData.message.trim()) {
newErrors.message = 'Message is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear error when user types
if (errors[name as keyof FormData]) {
setErrors(prev => ({ ...prev, [name]: undefined }));
}
};
useEffect(() => {
emailjs.init('k7Xbdqofx3sBAJf3I');
}, []);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!validateForm()) {
toast.error('Please fix the errors in the form');
return;
}
setFormStatus('submitting');
try {
await emailjs.send(
'service_s5tsk8o',
'template_9rbucml',
{
from_name: formData.name,
from_email: formData.email,
subject: formData.subject,
message: formData.message,
},
'user_id'
);
setFormStatus('success');
toast.success('Message sent successfully!');
setFormData({ name: '', email: '', subject: '', message: '' });
// Reset to idle after 3 seconds
setTimeout(() => {
setFormStatus('idle');
}, 3000);
} catch (error) {
setFormStatus('error');
toast.error('Failed to send message. Please try again.');
console.error('Email error:', error);
// Reset to idle after 3 seconds
setTimeout(() => {
setFormStatus('idle');
}, 3000);
}
};
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: {
type: "spring",
stiffness: 100,
},
},
};
return (
<div className="py-20 px-4 sm:px-6 md:px-12 overflow-hidden" id="contact">
<div className="container mx-auto max-w-6xl">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">Get In Touch</h2>
<div className="w-20 h-1 bg-gradient-to-r from-primary to-secondary mx-auto rounded-full" />
<p className="mt-4 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
I'm always open to discussing new projects, creative ideas, or opportunities to be part of your vision.
</p>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Contact Info */}
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
>
<h3 className="text-2xl font-semibold mb-8">Contact Information</h3>
<div className="space-y-6">
<motion.div
className="flex items-start space-x-4"
variants={itemVariants}
>
<div className="bg-primary/10 dark:bg-primary/20 p-3 rounded-lg text-primary dark:text-primary-light">
<MailIcon className="w-6 h-6" />
</div>
<div className="flex-1">
<h4 className="font-medium text-lg mb-1">Email</h4>
<a href={`mailto:${contact.email}`} className="text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light transition-colors">
{contact.email}
</a>
</div>
</motion.div>
<motion.div
className="flex items-start space-x-4"
variants={itemVariants}
>
<div className="bg-primary/10 dark:bg-primary/20 p-3 rounded-lg text-primary dark:text-primary-light">
<PhoneIcon className="w-6 h-6" />
</div>
<div className="flex-1">
<h4 className="font-medium text-lg mb-1">Phone</h4>
<a href={`tel:${contact.tel}`} className="text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light transition-colors">
{contact.tel}
</a>
</div>
</motion.div>
<motion.div
className="flex items-start space-x-4"
variants={itemVariants}
>
<div className="bg-primary/10 dark:bg-primary/20 p-3 rounded-lg text-primary dark:text-primary-light">
<MapPinIcon className="w-6 h-6" />
</div>
<div className="flex-1">
<h4 className="font-medium text-lg mb-1">Location</h4>
<p className="text-gray-600 dark:text-gray-300">{location}</p>
</div>
</motion.div>
</div>
<motion.div
className="mt-12"
variants={itemVariants}
>
<h4 className="font-medium text-lg mb-3">Connect with me</h4>
<div className="flex space-x-4">
{contact.social.map((platform, index) => (
<motion.a
key={index}
href={platform.url}
target="_blank"
rel="noopener noreferrer"
className="bg-white dark:bg-gray-800 p-3 rounded-lg shadow-md text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light hover:shadow-lg transition-all duration-300"
whileHover={{ y: -5 }}
whileTap={{ scale: 0.95 }}
>
<platform.icon className="w-6 h-6" />
</motion.a>
))}
</div>
</motion.div>
</motion.div>
{/* Contact Form */}
<motion.div
initial={{ opacity: 0, x: 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl p-8">
<h3 className="text-2xl font-semibold mb-6">Send Me a Message</h3>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Name
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
className={`w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white border ${
errors.name ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} focus:outline-none focus:ring-2 focus:ring-primary/50`}
placeholder="Your name"
/>
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name}</p>}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className={`w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white border ${
errors.email ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} focus:outline-none focus:ring-2 focus:ring-primary/50`}
placeholder="your.email@example.com"
/>
{errors.email && <p className="mt-1 text-sm text-red-500">{errors.email}</p>}
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Subject
</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleInputChange}
className={`w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white border ${
errors.subject ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} focus:outline-none focus:ring-2 focus:ring-primary/50`}
placeholder="What is this regarding?"
/>
{errors.subject && <p className="mt-1 text-sm text-red-500">{errors.subject}</p>}
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Message
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleInputChange}
rows={5}
className={`w-full px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white border ${
errors.message ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} focus:outline-none focus:ring-2 focus:ring-primary/50`}
placeholder="Your message here..."
/>
{errors.message && <p className="mt-1 text-sm text-red-500">{errors.message}</p>}
</div>
<div>
<motion.button
type="submit"
className={`w-full py-3 px-6 rounded-lg text-white font-medium flex items-center justify-center transition-all duration-300 ${
formStatus === 'submitting'
? 'bg-gray-400 cursor-not-allowed'
: formStatus === 'success'
? 'bg-green-500 hover:bg-green-600'
: formStatus === 'error'
? 'bg-red-500 hover:bg-red-600'
: 'bg-primary hover:bg-primary-dark'
}`}
disabled={formStatus === 'submitting'}
whileHover={formStatus !== 'submitting' ? { scale: 1.02 } : {}}
whileTap={formStatus !== 'submitting' ? { scale: 0.98 } : {}}
>
{formStatus === 'submitting' ? (
<span className="flex items-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Sending...
</span>
) : formStatus === 'success' ? (
<span className="flex items-center">
<CheckCircleIcon className="w-5 h-5 mr-2" />
Message Sent!
</span>
) : formStatus === 'error' ? (
<span className="flex items-center">
<AlertCircleIcon className="w-5 h-5 mr-2" />
Failed to Send
</span>
) : (
<span className="flex items-center">
<SendIcon className="w-5 h-5 mr-2" />
Send Message
</span>
)}
</motion.button>
</div>
</form>
</div>
</motion.div>
</div>
</div>
</div>
);
};
export default ContactSection;