- Introduced LanguageProvider to manage language state and translations. - Updated components (Header, Hero, About, Skills, Projects, Education, Contact) to utilize translations. - Added language toggle button in the header for switching between French and English. - Enhanced styling for language toggle button in the header.
375 lines
12 KiB
TypeScript
375 lines
12 KiB
TypeScript
import { useState } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { Mail, Phone, MapPin, Send, Github, Linkedin, MessageCircle, CheckCircle, AlertCircle } from 'lucide-react';
|
|
import { sendContactEmail } from '../services/emailService';
|
|
import type { ContactFormData } from '../services/emailService';
|
|
import { useLanguage } from '../contexts/LanguageContext';
|
|
|
|
const Contact = () => {
|
|
const { t } = useLanguage();
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
email: '',
|
|
subject: '',
|
|
message: ''
|
|
});
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
const { name, value } = e.target;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[name]: value
|
|
}));
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setIsSubmitting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const contactData: ContactFormData = {
|
|
name: formData.name,
|
|
email: formData.email,
|
|
subject: formData.subject,
|
|
message: formData.message
|
|
};
|
|
|
|
const result = await sendContactEmail(contactData);
|
|
|
|
if (result.success) {
|
|
setIsSubmitted(true);
|
|
setFormData({ name: '', email: '', subject: '', message: '' });
|
|
|
|
// Reset du message de succès après 5 secondes
|
|
setTimeout(() => {
|
|
setIsSubmitted(false);
|
|
}, 5000);
|
|
} else {
|
|
setError(result.message);
|
|
}
|
|
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : 'Une erreur est survenue lors de l\'envoi du message.';
|
|
setError(errorMessage);
|
|
console.error('Erreur lors de l\'envoi de l\'email:', err);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const contactInfo = [
|
|
{
|
|
icon: <Mail size={24} />,
|
|
title: "Email",
|
|
content: "dayronvanleemput@gmail.com",
|
|
link: "mailto:dayronvanleemput@gmail.com",
|
|
color: "#EA4335"
|
|
},
|
|
{
|
|
icon: <Phone size={24} />,
|
|
title: "Téléphone",
|
|
content: "+32 455 19 47 62",
|
|
link: "tel:+32455194762",
|
|
color: "#34A853"
|
|
},
|
|
{
|
|
icon: <MapPin size={24} />,
|
|
title: "Localisation",
|
|
content: "Ath, Belgique",
|
|
link: "https://maps.google.com/?q=Ath,Belgium",
|
|
color: "#4285F4"
|
|
}
|
|
];
|
|
|
|
const socialLinks = [
|
|
{
|
|
icon: <Github size={24} />,
|
|
name: "GitHub",
|
|
url: "https://github.com/Dayron-HELHa", // Remplacez par votre profil
|
|
color: "#333"
|
|
},
|
|
{
|
|
icon: <Linkedin size={24} />,
|
|
name: "LinkedIn",
|
|
url: "https://www.linkedin.com/in/dayron-van-leemput-992a94398", // Remplacez par votre profil
|
|
color: "#0077B5"
|
|
}
|
|
];
|
|
|
|
const containerVariants = {
|
|
hidden: { opacity: 0 },
|
|
visible: {
|
|
opacity: 1,
|
|
transition: {
|
|
staggerChildren: 0.2,
|
|
delayChildren: 0.3
|
|
}
|
|
}
|
|
};
|
|
|
|
const itemVariants = {
|
|
hidden: { opacity: 0, y: 50 },
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
transition: {
|
|
duration: 0.6,
|
|
ease: [0.25, 0.1, 0.25, 1] as const
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<section id="contact" className="contact">
|
|
<div className="container">
|
|
<motion.div
|
|
className="section-header"
|
|
initial={{ opacity: 0, y: 50 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.8 }}
|
|
viewport={{ once: true }}
|
|
>
|
|
<h2 className="section-title">{t('contact.title')}</h2>
|
|
<p className="section-subtitle">
|
|
{t('contact.subtitle')}
|
|
</p>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
className="contact-content"
|
|
variants={containerVariants}
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={{ once: true }}
|
|
>
|
|
{/* Informations de contact */}
|
|
<motion.div className="contact-info" variants={itemVariants}>
|
|
<div className="contact-intro">
|
|
<h3>
|
|
<MessageCircle size={24} />
|
|
{t('contact.stayInTouch')}
|
|
</h3>
|
|
<p>
|
|
{t('contact.intro')}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="contact-methods">
|
|
{contactInfo.map((contact, index) => (
|
|
<motion.a
|
|
key={index}
|
|
href={contact.link}
|
|
className="contact-method"
|
|
initial={{ opacity: 0, x: -30 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
whileHover={{
|
|
scale: 1.05,
|
|
x: 10,
|
|
transition: { duration: 0.2 }
|
|
}}
|
|
viewport={{ once: true }}
|
|
>
|
|
<div
|
|
className="contact-icon"
|
|
style={{ backgroundColor: `${contact.color}20`, color: contact.color }}
|
|
>
|
|
{contact.icon}
|
|
</div>
|
|
<div className="contact-details">
|
|
<h4>{contact.title}</h4>
|
|
<span>{contact.content}</span>
|
|
</div>
|
|
</motion.a>
|
|
))}
|
|
</div>
|
|
|
|
<div className="social-links">
|
|
<h4>{t('contact.findMeOn')}</h4>
|
|
<div className="social-grid">
|
|
{socialLinks.map((social, index) => (
|
|
<motion.a
|
|
key={index}
|
|
href={social.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="social-link"
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
whileInView={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
whileHover={{
|
|
scale: 1.1,
|
|
rotate: 5,
|
|
transition: { duration: 0.2 }
|
|
}}
|
|
viewport={{ once: true }}
|
|
>
|
|
<div
|
|
className="social-icon"
|
|
style={{ backgroundColor: `${social.color}20`, color: social.color }}
|
|
>
|
|
{social.icon}
|
|
</div>
|
|
<span>{social.name}</span>
|
|
</motion.a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Formulaire de contact */}
|
|
<motion.div className="contact-form-container" variants={itemVariants}>
|
|
<h3>{t('contact.sendMessage')}</h3>
|
|
|
|
{isSubmitted && (
|
|
<motion.div
|
|
className="success-message"
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.8 }}
|
|
>
|
|
<CheckCircle size={20} />
|
|
{t('contact.success')}
|
|
</motion.div>
|
|
)}
|
|
|
|
{error && (
|
|
<motion.div
|
|
className="error-message"
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.8 }}
|
|
>
|
|
<AlertCircle size={20} />
|
|
{error}
|
|
</motion.div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="contact-form">
|
|
<div className="form-row">
|
|
<motion.div
|
|
className="form-group"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.1 }}
|
|
viewport={{ once: true }}
|
|
>
|
|
<label htmlFor="name">{t('contact.form.name')}</label>
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
name="name"
|
|
value={formData.name}
|
|
onChange={handleChange}
|
|
required
|
|
placeholder={t('contact.form.name')}
|
|
/>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
className="form-group"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.2 }}
|
|
viewport={{ once: true }}
|
|
>
|
|
<label htmlFor="email">{t('contact.form.email')}</label>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
name="email"
|
|
value={formData.email}
|
|
onChange={handleChange}
|
|
required
|
|
placeholder="votre.email@example.com"
|
|
/>
|
|
</motion.div>
|
|
</div>
|
|
|
|
<motion.div
|
|
className="form-group"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.3 }}
|
|
viewport={{ once: true }}
|
|
>
|
|
<label htmlFor="subject">{t('contact.form.subject')}</label>
|
|
<input
|
|
type="text"
|
|
id="subject"
|
|
name="subject"
|
|
value={formData.subject}
|
|
onChange={handleChange}
|
|
required
|
|
placeholder={t('contact.form.subject')}
|
|
/>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
className="form-group"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.4 }}
|
|
viewport={{ once: true }}
|
|
>
|
|
<label htmlFor="message">{t('contact.form.message')}</label>
|
|
<textarea
|
|
id="message"
|
|
name="message"
|
|
value={formData.message}
|
|
onChange={handleChange}
|
|
required
|
|
rows={6}
|
|
placeholder={t('contact.form.message')}
|
|
/>
|
|
</motion.div>
|
|
|
|
<motion.button
|
|
type="submit"
|
|
className={`submit-btn ${isSubmitting ? 'submitting' : ''}`}
|
|
disabled={isSubmitting}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.5 }}
|
|
whileHover={!isSubmitting ? { scale: 1.05 } : {}}
|
|
whileTap={!isSubmitting ? { scale: 0.95 } : {}}
|
|
viewport={{ once: true }}
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<div className="loading-spinner" />
|
|
{t('contact.form.sending')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Send size={20} />
|
|
{t('contact.form.send')}
|
|
</>
|
|
)}
|
|
</motion.button>
|
|
</form>
|
|
</motion.div>
|
|
</motion.div>
|
|
|
|
{/* Footer */}
|
|
<motion.footer
|
|
className="contact-footer"
|
|
initial={{ opacity: 0 }}
|
|
whileInView={{ opacity: 1 }}
|
|
transition={{ duration: 0.8, delay: 0.5 }}
|
|
viewport={{ once: true }}
|
|
>
|
|
<p>
|
|
© 2025 Dayron Van Leemput.
|
|
</p>
|
|
</motion.footer>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default Contact; |