feat: update portfolio with new contact form functionality and improved styling

- Removed outdated CV file and replaced with a new image for the profile.
- Implemented a new email service using EmailJS for contact form submissions.
- Enhanced the contact form to handle errors and success messages.
- Updated the About section for grammatical accuracy.
- Modified the Hero component to link to the new CV file and updated GitHub profile link.
- Updated project links to point to the correct GitHub repositories.
- Improved styling for error messages and avatar image with hover effects.
This commit is contained in:
Dayron
2025-11-12 00:47:02 +01:00
parent cdff0c8c5c
commit a4b4423ff4
16 changed files with 1657 additions and 35 deletions

1
.gitignore vendored
View File

@@ -22,3 +22,4 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.env

105
EMAILJS_SETUP.md Normal file
View File

@@ -0,0 +1,105 @@
# Configuration EmailJS pour le Portfolio
Ce guide vous explique comment configurer EmailJS pour que le formulaire de contact de votre portfolio fonctionne réellement.
## 1. Créer un compte EmailJS
1. Allez sur [EmailJS.com](https://www.emailjs.com/)
2. Créez un compte gratuit (jusqu'à 200 emails/mois)
3. Connectez-vous à votre dashboard
## 2. Configurer le service email
1. Dans votre dashboard EmailJS, allez dans **Email Services**
2. Cliquez sur **Add New Service**
3. Choisissez votre fournisseur email (Gmail, Outlook, etc.)
4. Suivez les instructions pour connecter votre compte email
5. Notez votre **Service ID** (ex: `service_xyz123`)
## 3. Créer un template d'email
1. Allez dans **Email Templates**
2. Cliquez sur **Create New Template**
3. Configurez votre template avec ces variables :
- `{{from_name}}` - Nom de l'expéditeur
- `{{from_email}}` - Email de l'expéditeur
- `{{subject}}` - Sujet du message
- `{{message}}` - Corps du message
Exemple de template :
```
Nouveau message depuis votre portfolio
De: {{from_name}} ({{from_email}})
Sujet: {{subject}}
Message:
{{message}}
---
Ce message a été envoyé depuis votre formulaire de contact.
```
4. Notez votre **Template ID** (ex: `template_abc456`)
## 4. Obtenir votre clé publique
1. Allez dans **Account** > **General**
2. Trouvez votre **Public Key** (ex: `user_def789`)
## 5. Configurer les variables d'environnement
Créez un fichier `.env` à la racine de votre projet :
```env
VITE_EMAILJS_SERVICE_ID=your_service_id_here
VITE_EMAILJS_TEMPLATE_ID=your_template_id_here
VITE_EMAILJS_PUBLIC_KEY=your_public_key_here
```
## 6. Sécurité et limitations
### Version gratuite d'EmailJS :
- 200 emails par mois maximum
- Pas de limitation de domaine
- Support communautaire
### Pour la production :
- Considérez un plan payant pour plus d'emails
- Ajoutez une validation côté serveur si nécessaire
- Implémentez un captcha pour éviter le spam
## 7. Test du formulaire
1. Remplissez les variables d'environnement
2. Redémarrez votre serveur de développement
3. Testez le formulaire de contact
4. Vérifiez la réception de l'email
## 8. Dépannage
### Erreurs communes :
**"Invalid template ID"**
- Vérifiez que le Template ID est correct
- Assurez-vous que le template est publié
**"Forbidden"**
- Vérifiez votre Public Key
- Assurez-vous que le service est actif
**Email non reçu**
- Vérifiez vos spams
- Vérifiez la configuration du service email
- Testez depuis le dashboard EmailJS
### Fallback automatique
Si EmailJS ne fonctionne pas, le système bascule automatiquement vers `mailto:` pour ouvrir le client email par défaut de l'utilisateur.
## 9. Fichiers modifiés
- `src/services/emailService.ts` - Service d'envoi d'emails
- `src/components/Contact.tsx` - Intégration du service
- `src/styles/components/_contact.scss` - Styles des messages d'erreur
Le formulaire de contact est maintenant prêt à fonctionner avec une vraie logique d'envoi d'emails !

10
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "xeewy.eu", "name": "xeewy.eu",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@emailjs/browser": "^4.4.1",
"framer-motion": "^12.23.24", "framer-motion": "^12.23.24",
"lucide-react": "^0.553.0", "lucide-react": "^0.553.0",
"react": "^19.2.0", "react": "^19.2.0",
@@ -312,6 +313,15 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@emailjs/browser": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@emailjs/browser/-/browser-4.4.1.tgz",
"integrity": "sha512-DGSlP9sPvyFba3to2A50kDtZ+pXVp/0rhmqs2LmbMS3I5J8FSOgLwzY2Xb4qfKlOVHh29EAutLYwe5yuEZmEFg==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",

View File

@@ -10,6 +10,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@emailjs/browser": "^4.4.1",
"framer-motion": "^12.23.24", "framer-motion": "^12.23.24",
"lucide-react": "^0.553.0", "lucide-react": "^0.553.0",
"react": "^19.2.0", "react": "^19.2.0",

File diff suppressed because it is too large Load Diff

BIN
public/dvl.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -69,7 +69,7 @@ const About = () => {
<motion.div className="about-card" variants={itemVariants}> <motion.div className="about-card" variants={itemVariants}>
<h3>Ma passion</h3> <h3>Ma passion</h3>
<p> <p>
Ce qui m'anime le plus, c'est la création d'solutions innovantes qui résolvent Ce qui m'anime le plus, c'est la création de solutions innovantes qui résolvent
des problèmes réels. J'aime particulièrement le développement mobile avec des problèmes réels. J'aime particulièrement le développement mobile avec
<strong> Flutter</strong> et le développement web moderne avec <strong> Flutter</strong> et le développement web moderne avec
<strong> React</strong> et <strong> TypeScript</strong>. <strong> React</strong> et <strong> TypeScript</strong>.

View File

@@ -1,6 +1,8 @@
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Mail, Phone, MapPin, Send, Github, Linkedin, MessageCircle, CheckCircle } from 'lucide-react'; import { Mail, Phone, MapPin, Send, Github, Linkedin, MessageCircle, CheckCircle, AlertCircle } from 'lucide-react';
import { sendContactEmail } from '../services/emailService';
import type { ContactFormData } from '../services/emailService';
const Contact = () => { const Contact = () => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@@ -11,6 +13,7 @@ const Contact = () => {
}); });
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
@@ -23,10 +26,19 @@ const Contact = () => {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
setError(null);
// Simulation d'envoi (remplacez par votre logique d'envoi réelle) try {
setTimeout(() => { const contactData: ContactFormData = {
setIsSubmitting(false); name: formData.name,
email: formData.email,
subject: formData.subject,
message: formData.message
};
const result = await sendContactEmail(contactData);
if (result.success) {
setIsSubmitted(true); setIsSubmitted(true);
setFormData({ name: '', email: '', subject: '', message: '' }); setFormData({ name: '', email: '', subject: '', message: '' });
@@ -34,21 +46,31 @@ const Contact = () => {
setTimeout(() => { setTimeout(() => {
setIsSubmitted(false); setIsSubmitted(false);
}, 5000); }, 5000);
}, 2000); } 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 = [ const contactInfo = [
{ {
icon: <Mail size={24} />, icon: <Mail size={24} />,
title: "Email", title: "Email",
content: "dayronvanleemput@gmail.com", // Remplacez par votre email content: "dayronvanleemput@gmail.com",
link: "mailto:dayronvanleemput@gmail.com", link: "mailto:dayronvanleemput@gmail.com",
color: "#EA4335" color: "#EA4335"
}, },
{ {
icon: <Phone size={24} />, icon: <Phone size={24} />,
title: "Téléphone", title: "Téléphone",
content: "+32 455 19 47 62", // Remplacez par votre numéro content: "+32 455 19 47 62",
link: "tel:+32455194762", link: "tel:+32455194762",
color: "#34A853" color: "#34A853"
}, },
@@ -215,6 +237,18 @@ const Contact = () => {
</motion.div> </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"> <form onSubmit={handleSubmit} className="contact-form">
<div className="form-row"> <div className="form-row">
<motion.div <motion.div
@@ -330,7 +364,7 @@ const Contact = () => {
viewport={{ once: true }} viewport={{ once: true }}
> >
<p> <p>
© 2025 Dayron Van Leemput. Développé avec ❤️ en React et TypeScript. © 2025 Dayron Van Leemput.
</p> </p>
</motion.footer> </motion.footer>
</div> </div>

View File

@@ -5,8 +5,8 @@ const Hero = () => {
const handleDownloadCV = () => { const handleDownloadCV = () => {
// Ici, vous pouvez ajouter le lien vers votre CV // Ici, vous pouvez ajouter le lien vers votre CV
const link = document.createElement('a'); const link = document.createElement('a');
link.href = '/cv-dayron-van-leemput.pdf'; // Ajoutez votre CV dans le dossier public link.href = '/Dayron_Van_Leemput_CV.pdf'; // Ajoutez votre CV dans le dossier public
link.download = 'CV-Dayron-Van-Leemput.pdf'; link.download = 'Dayron_Van_Leemput_CV.pdf';
link.click(); link.click();
}; };
@@ -85,7 +85,7 @@ const Hero = () => {
transition={{ duration: 0.8, delay: 1.2 }} transition={{ duration: 0.8, delay: 1.2 }}
> >
<motion.a <motion.a
href="https://github.com/dayronvanleemput" // Remplacez par votre profil GitHub href="https://github.com/Dayron-HELHa" // Remplacez par votre profil GitHub
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="social-link" className="social-link"
@@ -119,10 +119,11 @@ const Hero = () => {
whileHover={{ scale: 1.05, rotate: 5 }} whileHover={{ scale: 1.05, rotate: 5 }}
transition={{ type: "spring", stiffness: 300, damping: 10 }} transition={{ type: "spring", stiffness: 300, damping: 10 }}
> >
{/* Vous pouvez remplacer ceci par votre photo */} <img
<div className="avatar-placeholder"> src="/dvl.jpg"
<span>DV</span> alt="Dayron Van Leemput - Portrait"
</div> className="avatar-image"
/>
</motion.div> </motion.div>
</motion.div> </motion.div>
</div> </div>

View File

@@ -18,7 +18,7 @@ const Projects = () => {
color: "#4CAF50", color: "#4CAF50",
icon: <MapPin size={24} />, icon: <MapPin size={24} />,
links: { links: {
github: "#", // Remplacez par votre lien GitHub github: "https://github.com/Dayron-HELHa/travel_mate", // Remplacez par votre lien GitHub
demo: "#" demo: "#"
}, },
image: "/travel-mate-preview.png" // Ajoutez votre image dans le dossier public image: "/travel-mate-preview.png" // Ajoutez votre image dans le dossier public
@@ -38,8 +38,8 @@ const Projects = () => {
color: "#2196F3", color: "#2196F3",
icon: <ExternalLink size={24} />, icon: <ExternalLink size={24} />,
links: { links: {
github: "https://github.com/dayronvanleemput/portfolio", // Remplacez par votre lien github: "https://github.com/Dayron-HELHa/xeewy.eu", // Remplacez par votre lien
demo: "https://dayronvanleemput.dev" // Remplacez par votre lien demo: "https://xeewy.eu" // Remplacez par votre lien
} }
} }
]; ];

View File

@@ -0,0 +1,121 @@
// ========================
// SERVICE D'ENVOI D'EMAIL
// ========================
import emailjs from '@emailjs/browser';
// Configuration EmailJS depuis les variables d'environnement
const EMAILJS_CONFIG = {
SERVICE_ID: import.meta.env.VITE_EMAILJS_SERVICE_ID || 'YOUR_SERVICE_ID',
TEMPLATE_ID: import.meta.env.VITE_EMAILJS_TEMPLATE_ID || 'YOUR_TEMPLATE_ID',
PUBLIC_KEY: import.meta.env.VITE_EMAILJS_PUBLIC_KEY || 'YOUR_PUBLIC_KEY'
};
// Interface pour les données du formulaire
export interface ContactFormData {
name: string;
email: string;
subject: string;
message: string;
}
// Interface pour la réponse
export interface EmailResponse {
success: boolean;
message: string;
}
// Fonction pour envoyer l'email
export const sendContactEmail = async (formData: ContactFormData): Promise<EmailResponse> => {
try {
// Validation des données
if (!formData.name || !formData.email || !formData.subject || !formData.message) {
return {
success: false,
message: 'Tous les champs sont requis.'
};
}
// Validation de l'email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
return {
success: false,
message: 'Veuillez entrer une adresse email valide.'
};
}
// Vérification de la configuration EmailJS
if (!validateEmailJSConfig()) {
console.warn('Configuration EmailJS non trouvée, utilisation du fallback mailto');
// Fallback vers mailto
const mailtoLink = createMailtoLink(formData);
window.location.href = mailtoLink;
return {
success: true,
message: 'Ouverture de votre client email par défaut...'
};
}
console.log('Envoi avec EmailJS...');
console.log('Service ID:', EMAILJS_CONFIG.SERVICE_ID);
console.log('Template ID:', EMAILJS_CONFIG.TEMPLATE_ID);
// Préparation des paramètres pour EmailJS
const templateParams = {
from_name: formData.name,
from_email: formData.email,
subject: formData.subject,
message: formData.message,
to_name: 'Dayron Van Leemput', // Votre nom
reply_to: formData.email
};
// Envoi avec EmailJS
const response = await emailjs.send(
EMAILJS_CONFIG.SERVICE_ID,
EMAILJS_CONFIG.TEMPLATE_ID,
templateParams,
EMAILJS_CONFIG.PUBLIC_KEY
);
if (response.status === 200) {
return {
success: true,
message: 'Message envoyé avec succès ! Je vous répondrai bientôt.'
};
} else {
throw new Error('Erreur lors de l\'envoi');
}
} catch (error) {
console.error('Erreur lors de l\'envoi de l\'email:', error);
return {
success: false,
message: 'Une erreur s\'est produite lors de l\'envoi. Veuillez réessayer ou me contacter directement.'
};
}
};
// Fonction alternative pour fallback (formulaire mailto)
export const createMailtoLink = (formData: ContactFormData): string => {
const subject = encodeURIComponent(`[Portfolio] ${formData.subject}`);
const body = encodeURIComponent(
`Bonjour Dayron,\n\n` +
`${formData.message}\n\n` +
`Cordialement,\n${formData.name}\n` +
`Email: ${formData.email}`
);
return `mailto:dayron.vanleemput@example.com?subject=${subject}&body=${body}`;
};
// Fonction pour valider les credentials EmailJS
export const validateEmailJSConfig = (): boolean => {
return (
EMAILJS_CONFIG.SERVICE_ID !== 'YOUR_SERVICE_ID' &&
EMAILJS_CONFIG.TEMPLATE_ID !== 'YOUR_TEMPLATE_ID' &&
EMAILJS_CONFIG.PUBLIC_KEY !== 'YOUR_PUBLIC_KEY'
);
};

View File

@@ -138,6 +138,19 @@
font-weight: 500; font-weight: 500;
} }
.error-message {
display: flex;
align-items: center;
gap: 12px;
background: #ff4757;
color: white;
padding: 16px;
border-radius: $border-radius;
margin-bottom: 24px;
font-weight: 500;
border: 1px solid #ff3838;
}
.contact-form { .contact-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -131,27 +131,56 @@
} }
} }
.avatar-placeholder { .avatar-image {
width: 300px; width: 300px;
height: 300px; height: 300px;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(135deg, $primary-color, $accent-color); object-fit: cover;
@include flex-center(); object-position: center;
font-size: map-get($font-sizes, 4xl);
font-weight: 800;
color: white;
@include box-shadow(large); @include box-shadow(large);
border: 4px solid white;
// Ajouter un effet de dégradé subtil en overlay
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 50%;
background: linear-gradient(135deg,
rgba($primary-color, 0.1) 0%,
rgba($accent-color, 0.1) 100%);
pointer-events: none;
}
@include respond-to(md) { @include respond-to(md) {
width: 250px; width: 250px;
height: 250px; height: 250px;
font-size: map-get($font-sizes, 3xl); border-width: 3px;
} }
@include respond-to(sm) { @include respond-to(sm) {
width: 200px; width: 200px;
height: 200px; height: 200px;
font-size: map-get($font-sizes, 2xl); border-width: 2px;
}
// Effet de survol amélioré pour les images
@media (hover: hover) {
&:hover {
transform: scale(1.02);
@include box-shadow(large);
&::after {
background: linear-gradient(135deg,
rgba($primary-color, 0.2) 0%,
rgba($accent-color, 0.2) 100%);
}
}
} }
} }