mirror of
https://github.com/emmabostian/developer-portfolios.git
synced 2025-04-21 05:38:01 +00:00
373 lines
14 KiB
TypeScript
373 lines
14 KiB
TypeScript
|
"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;
|