feat: add language context and translation support

- 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.
This commit is contained in:
Dayron
2025-11-12 18:36:51 +01:00
parent a4b4423ff4
commit e8722cf25d
10 changed files with 468 additions and 130 deletions

View File

@@ -1,12 +1,15 @@
import { motion } from 'framer-motion';
import { User, Heart, Target, Coffee } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
const About = () => {
const { t } = useLanguage();
const stats = [
{ icon: <User size={24} />, value: "3ème", label: "Année d'études" },
{ icon: <Heart size={24} />, value: "100%", label: "Passion" },
{ icon: <Target size={24} />, value: "∞", label: "Objectifs" },
{ icon: <Coffee size={24} />, value: "☕", label: "Fuel quotidien" }
{ icon: <User size={24} />, value: t('about.stats.year'), label: t('about.stats.yearLabel') },
{ icon: <Heart size={24} />, value: t('about.stats.passion'), label: t('about.stats.passionLabel') },
{ icon: <Target size={24} />, value: t('about.stats.goals'), label: t('about.stats.goalsLabel') },
{ icon: <Coffee size={24} />, value: t('about.stats.fuel'), label: t('about.stats.fuelLabel') }
];
const containerVariants = {
@@ -42,9 +45,9 @@ const About = () => {
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">À propos de moi</h2>
<h2 className="section-title">{t('about.title')}</h2>
<p className="section-subtitle">
Découvrez qui je suis et ce qui me passionne
{t('about.subtitle')}
</p>
</motion.div>
@@ -57,32 +60,23 @@ const About = () => {
viewport={{ once: true }}
>
<motion.div className="about-card" variants={itemVariants}>
<h3>Mon parcours</h3>
<h3>{t('about.journey.title')}</h3>
<p>
Actuellement en 3ème année de <strong>Technologies de l'Informatique</strong> à la
HELHa de Tournai, je me passionne pour le développement d'applications et les
nouvelles technologies. Mon parcours m'a permis d'acquérir une solide base
technique et une approche méthodique du développement.
{t('about.journey.content')}
</p>
</motion.div>
<motion.div className="about-card" variants={itemVariants}>
<h3>Ma passion</h3>
<h3>{t('about.passion.title')}</h3>
<p>
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
<strong> Flutter</strong> et le développement web moderne avec
<strong> React</strong> et <strong> TypeScript</strong>.
{t('about.passion.content')}
</p>
</motion.div>
<motion.div className="about-card" variants={itemVariants}>
<h3>Mes objectifs</h3>
<h3>{t('about.goals.title')}</h3>
<p>
Je cherche constamment à améliorer mes compétences et à rester à jour avec
les dernières tendances technologiques. Mon objectif est de devenir un
développeur full-stack polyvalent et de contribuer à des projets qui ont
un impact positif.
{t('about.goals.content')}
</p>
</motion.div>
</motion.div>
@@ -123,13 +117,11 @@ const About = () => {
viewport={{ once: true }}
>
<div className="highlight-content">
<h3>En quelques mots</h3>
<h3>{t('about.quote.title')}</h3>
<p>
"La technologie n'est rien. Ce qui est important, c'est d'avoir la foi en les gens,
qu'ils soient fondamentalement bons et intelligents, et si vous leur donnez des outils,
ils feront des choses merveilleuses avec."
"{t('about.quote.content')}"
</p>
<cite>- Steve Jobs</cite>
<cite>- {t('about.quote.author')}</cite>
</div>
</motion.div>
</div>

View File

@@ -3,8 +3,10 @@ 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: '',
@@ -93,7 +95,7 @@ const Contact = () => {
{
icon: <Linkedin size={24} />,
name: "LinkedIn",
url: "https://linkedin.com/in/dayronvanleemput", // Remplacez par votre profil
url: "https://www.linkedin.com/in/dayron-van-leemput-992a94398", // Remplacez par votre profil
color: "#0077B5"
}
];
@@ -131,9 +133,9 @@ const Contact = () => {
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">Contactez-moi</h2>
<h2 className="section-title">{t('contact.title')}</h2>
<p className="section-subtitle">
Une question, un projet ou simplement envie d'échanger ? N'hésitez pas à me contacter !
{t('contact.subtitle')}
</p>
</motion.div>
@@ -149,12 +151,10 @@ const Contact = () => {
<div className="contact-intro">
<h3>
<MessageCircle size={24} />
Restons en contact
{t('contact.stayInTouch')}
</h3>
<p>
Je suis toujours intéressé par de nouveaux projets, des collaborations
ou simplement des discussions autour de la technologie. N'hésitez pas
à me contacter !
{t('contact.intro')}
</p>
</div>
@@ -189,7 +189,7 @@ const Contact = () => {
</div>
<div className="social-links">
<h4>Retrouvez-moi aussi sur :</h4>
<h4>{t('contact.findMeOn')}</h4>
<div className="social-grid">
{socialLinks.map((social, index) => (
<motion.a
@@ -223,7 +223,7 @@ const Contact = () => {
{/* Formulaire de contact */}
<motion.div className="contact-form-container" variants={itemVariants}>
<h3>Envoyez-moi un message</h3>
<h3>{t('contact.sendMessage')}</h3>
{isSubmitted && (
<motion.div
@@ -233,7 +233,7 @@ const Contact = () => {
exit={{ opacity: 0, scale: 0.8 }}
>
<CheckCircle size={20} />
Message envoyé avec succès ! Je vous répondrai bientôt.
{t('contact.success')}
</motion.div>
)}
@@ -258,7 +258,7 @@ const Contact = () => {
transition={{ duration: 0.5, delay: 0.1 }}
viewport={{ once: true }}
>
<label htmlFor="name">Nom complet</label>
<label htmlFor="name">{t('contact.form.name')}</label>
<input
type="text"
id="name"
@@ -266,7 +266,7 @@ const Contact = () => {
value={formData.name}
onChange={handleChange}
required
placeholder="Votre nom"
placeholder={t('contact.form.name')}
/>
</motion.div>
@@ -277,7 +277,7 @@ const Contact = () => {
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
>
<label htmlFor="email">Email</label>
<label htmlFor="email">{t('contact.form.email')}</label>
<input
type="email"
id="email"
@@ -297,7 +297,7 @@ const Contact = () => {
transition={{ duration: 0.5, delay: 0.3 }}
viewport={{ once: true }}
>
<label htmlFor="subject">Sujet</label>
<label htmlFor="subject">{t('contact.form.subject')}</label>
<input
type="text"
id="subject"
@@ -305,7 +305,7 @@ const Contact = () => {
value={formData.subject}
onChange={handleChange}
required
placeholder="Objet de votre message"
placeholder={t('contact.form.subject')}
/>
</motion.div>
@@ -316,7 +316,7 @@ const Contact = () => {
transition={{ duration: 0.5, delay: 0.4 }}
viewport={{ once: true }}
>
<label htmlFor="message">Message</label>
<label htmlFor="message">{t('contact.form.message')}</label>
<textarea
id="message"
name="message"
@@ -324,7 +324,7 @@ const Contact = () => {
onChange={handleChange}
required
rows={6}
placeholder="Votre message..."
placeholder={t('contact.form.message')}
/>
</motion.div>
@@ -342,12 +342,12 @@ const Contact = () => {
{isSubmitting ? (
<>
<div className="loading-spinner" />
Envoi en cours...
{t('contact.form.sending')}
</>
) : (
<>
<Send size={20} />
Envoyer le message
{t('contact.form.send')}
</>
)}
</motion.button>

View File

@@ -1,24 +1,26 @@
import { motion } from 'framer-motion';
import { GraduationCap, Calendar, MapPin, Award, BookOpen, Target } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
const Education = () => {
const { t } = useLanguage();
const education = [
{
id: 1,
degree: "Bachelier en Technologies de l'Informatique",
school: "HELHa - Haute École Louvain en Hainaut",
location: "Tournai, Belgique",
period: "2023 - 2026",
currentYear: "3ème année",
status: "En cours",
description: "Formation complète en développement logiciel, programmation, bases de données, réseaux et gestion de projets informatiques.",
degree: t('education.degree'),
school: t('education.school'),
location: t('education.location'),
period: t('education.period'),
currentYear: t('education.currentYear'),
status: t('education.status'),
description: t('education.description'),
highlights: [
"Programmation orientée objet (Java, C#)",
"Développement web (HTML, CSS, JavaScript, React)",
"Développement mobile (Flutter, Dart)",
"Bases de données et SQL",
"Gestion de projets",
"Réseaux et systèmes"
t('education.highlights.0'),
t('education.highlights.1'),
t('education.highlights.2'),
t('education.highlights.3'),
t('education.highlights.4'),
t('education.highlights.5')
],
color: "#4CAF50",
icon: <GraduationCap size={24} />
@@ -27,16 +29,16 @@ const Education = () => {
const certifications = [
{
title: "Développement Mobile Flutter",
provider: "Formation autodidacte",
date: "2024",
title: t('education.cert1.title'),
provider: t('education.cert1.provider'),
date: t('education.cert1.date'),
skills: ["Dart", "Flutter", "Firebase", "API REST"],
color: "#2196F3"
},
{
title: "React & TypeScript",
provider: "Projets personnels",
date: "2024",
title: t('education.cert2.title'),
provider: t('education.cert2.provider'),
date: t('education.cert2.date'),
skills: ["React", "TypeScript", "Hooks", "Context API"],
color: "#FF9800"
}
@@ -75,9 +77,9 @@ const Education = () => {
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">Formation & Apprentissage</h2>
<h2 className="section-title">{t('education.title')}</h2>
<p className="section-subtitle">
Mon parcours académique et mes apprentissages continus
{t('education.subtitle')}
</p>
</motion.div>
@@ -178,7 +180,7 @@ const Education = () => {
>
<h3 className="certifications-title">
<Award size={24} />
Formations complémentaires & Autodidacte
{t('education.certifications.title')}
</h3>
<div className="certifications-grid">
{certifications.map((cert, index) => (
@@ -224,13 +226,13 @@ const Education = () => {
transition={{ duration: 0.8, delay: 0.4 }}
viewport={{ once: true }}
>
<h3>Objectifs d'apprentissage 2025</h3>
<h3>{t('education.learningGoals2025')}</h3>
<div className="goals-grid">
{[
{ goal: "Maîtriser Firebase et les services cloud", progress: 60 },
{ goal: "Approfondir Spring Boot pour le backend", progress: 30 },
{ goal: "Apprendre Docker et les conteneurs", progress: 20 },
{ goal: "Développer mes compétences en UI/UX", progress: 45 }
{ goal: t('education.goal1'), progress: 60 },
{ goal: t('education.goal2'), progress: 30 },
{ goal: t('education.goal3'), progress: 20 },
{ goal: t('education.goal4'), progress: 45 }
].map((item, index) => (
<motion.div
key={index}

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Menu, X, Sun, Moon } from 'lucide-react';
import { Menu, X, Sun, Moon, Globe } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
interface HeaderProps {
darkMode: boolean;
@@ -9,14 +10,15 @@ interface HeaderProps {
const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { language, setLanguage, t } = useLanguage();
const menuItems = [
{ name: 'Accueil', href: '#hero' },
{ name: 'À propos', href: '#about' },
{ name: 'Compétences', href: '#skills' },
{ name: 'Projets', href: '#projects' },
{ name: 'Formation', href: '#education' },
{ name: 'Contact', href: '#contact' }
{ name: t('nav.home'), href: '#hero' },
{ name: t('nav.about'), href: '#about' },
{ name: t('nav.skills'), href: '#skills' },
{ name: t('nav.projects'), href: '#projects' },
{ name: t('nav.education'), href: '#education' },
{ name: t('nav.contact'), href: '#contact' }
];
const scrollToSection = (href: string) => {
@@ -27,6 +29,10 @@ const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
setIsMenuOpen(false);
};
const toggleLanguage = () => {
setLanguage(language === 'fr' ? 'en' : 'fr');
};
return (
<motion.header
initial={{ y: -100 }}
@@ -68,13 +74,26 @@ const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
</ul>
<div className="nav-controls">
{/* Toggle langue */}
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={toggleLanguage}
className="language-toggle"
aria-label={t('btn.changeLang')}
title={t('btn.changeLang')}
>
<Globe size={18} />
<span className="language-text">{language === 'fr' ? 'EN' : 'FR'}</span>
</motion.button>
{/* Toggle thème */}
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={toggleDarkMode}
className="theme-toggle"
aria-label="Changer de thème"
aria-label={t('btn.changeTheme')}
>
{darkMode ? <Sun size={20} /> : <Moon size={20} />}
</motion.button>
@@ -85,7 +104,7 @@ const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
whileTap={{ scale: 0.9 }}
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="menu-toggle"
aria-label="Menu"
aria-label={t('btn.menu')}
>
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
</motion.button>

View File

@@ -1,7 +1,10 @@
import { motion } from 'framer-motion';
import { Download, Github, Linkedin, Mail } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
const Hero = () => {
const { t } = useLanguage();
const handleDownloadCV = () => {
// Ici, vous pouvez ajouter le lien vers votre CV
const link = document.createElement('a');
@@ -25,7 +28,7 @@ const Hero = () => {
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
>
Dayron Van Leemput
{t('hero.title')}
</motion.h1>
<motion.h2
@@ -34,7 +37,7 @@ const Hero = () => {
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
>
Étudiant en Technologies de l'Informatique
{t('hero.subtitle')}
</motion.h2>
<motion.p
@@ -43,8 +46,7 @@ const Hero = () => {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.8 }}
>
Bac 3 à la HELHa de Tournai | Jeune développeur passionné par les nouvelles technologies
et le développement d'applications innovantes
{t('hero.description')}
</motion.p>
<motion.div
@@ -60,7 +62,7 @@ const Hero = () => {
whileTap={{ scale: 0.95 }}
>
<Download size={20} />
Télécharger mon CV
{t('btn.downloadCV')}
</motion.button>
<motion.a
@@ -74,7 +76,7 @@ const Hero = () => {
whileTap={{ scale: 0.95 }}
>
<Mail size={20} />
Me contacter
{t('btn.contactMe')}
</motion.a>
</motion.div>
@@ -96,7 +98,7 @@ const Hero = () => {
</motion.a>
<motion.a
href="https://linkedin.com/in/dayronvanleemput" // Remplacez par votre profil LinkedIn
href="https://www.linkedin.com/in/dayron-van-leemput-992a94398" // Remplacez par votre profil LinkedIn
target="_blank"
rel="noopener noreferrer"
className="social-link"

View File

@@ -1,19 +1,21 @@
import { motion } from 'framer-motion';
import { ExternalLink, Github, MapPin } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
const Projects = () => {
const { t } = useLanguage();
const projects = [
{
id: 1,
title: "Travel Mate",
description: "Application mobile conçue pour simplifier l'organisation de voyages de groupe. Elle permet de centraliser toutes les informations importantes d'un voyage : planification, gestion des dépenses, découverte d'activités et coordination entre les participants.",
status: "Bientôt disponible sur App Store et Play Store",
title: t('projects.travelMate.title'),
description: t('projects.travelMate.description'),
status: t('projects.status.available'),
technologies: ["Dart", "Flutter", "Firebase"],
features: [
"Planification de voyage collaborative",
"Gestion des dépenses partagées",
"Découverte d'activités locales",
"Coordination en temps réel"
t('projects.travelMate.feature1'),
t('projects.travelMate.feature2'),
t('projects.travelMate.feature3'),
t('projects.travelMate.feature4')
],
color: "#4CAF50",
icon: <MapPin size={24} />,
@@ -25,15 +27,15 @@ const Projects = () => {
},
{
id: 2,
title: "Portfolio Web",
description: "Site web personnel moderne et responsive développé avec React et TypeScript. Inclut des animations fluides, un mode sombre/clair et une architecture modulaire.",
status: "Projet actuel",
title: t('projects.portfolio.title'),
description: t('projects.portfolio.description'),
status: t('projects.status.current'),
technologies: ["React", "TypeScript", "Framer Motion", "CSS3"],
features: [
"Design responsive",
"Animations fluides",
"Mode sombre/clair",
"Performance optimisée"
t('projects.portfolio.feature1'),
t('projects.portfolio.feature2'),
t('projects.portfolio.feature3'),
t('projects.portfolio.feature4')
],
color: "#2196F3",
icon: <ExternalLink size={24} />,
@@ -77,9 +79,9 @@ const Projects = () => {
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">Mes Projets</h2>
<h2 className="section-title">{t('projects.title')}</h2>
<p className="section-subtitle">
Découvrez les projets sur lesquels j'ai travaillé et qui me tiennent à cœur
{t('projects.subtitle')}
</p>
</motion.div>
@@ -138,7 +140,7 @@ const Projects = () => {
</div>
<div className="project-features">
<h4>Fonctionnalités principales :</h4>
<h4>{t('projects.features')}</h4>
<ul>
{project.features.map((feature, featureIndex) => (
<motion.li
@@ -170,7 +172,7 @@ const Projects = () => {
whileTap={{ scale: 0.95 }}
>
<Github size={20} />
Code
{t('projects.btn.code')}
</motion.a>
)}
{project.links.demo !== "#" && (
@@ -183,7 +185,7 @@ const Projects = () => {
whileTap={{ scale: 0.95 }}
>
<ExternalLink size={20} />
Voir le projet
{t('projects.btn.viewProject')}
</motion.a>
)}
</div>

View File

@@ -1,11 +1,13 @@
import { motion } from 'framer-motion';
import { Code, Database, Smartphone, Globe, Server, Wrench } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
const Skills = () => {
const { t } = useLanguage();
const skillCategories = [
{
icon: <Smartphone size={32} />,
title: "Mobile",
title: t('skills.category.mobile'),
color: "#4FC3F7",
skills: [
{ name: "Dart", level: 85, color: "#0175C2" },
@@ -14,7 +16,7 @@ const Skills = () => {
},
{
icon: <Globe size={32} />,
title: "Frontend",
title: t('skills.category.frontend'),
color: "#42A5F5",
skills: [
{ name: "React", level: 75, color: "#61DAFB" },
@@ -24,7 +26,7 @@ const Skills = () => {
},
{
icon: <Server size={32} />,
title: "Backend",
title: t('skills.category.backend'),
color: "#66BB6A",
skills: [
{ name: "Java", level: 75, color: "#ED8B00" },
@@ -33,7 +35,7 @@ const Skills = () => {
},
{
icon: <Database size={32} />,
title: "Outils & Autres",
title: t('skills.category.tools'),
color: "#AB47BC",
skills: [
{ name: "Git", level: 70, color: "#F05032" },
@@ -76,9 +78,9 @@ const Skills = () => {
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">Compétences & Technologies</h2>
<h2 className="section-title">{t('skills.title')}</h2>
<p className="section-subtitle">
Les technologies que je maîtrise et avec lesquelles j'aime travailler
{t('skills.subtitle')}
</p>
</motion.div>
@@ -157,13 +159,13 @@ const Skills = () => {
transition={{ duration: 0.8, delay: 0.3 }}
viewport={{ once: true }}
>
<h3 className="soft-skills-title">Autres compétences</h3>
<h3 className="soft-skills-title">{t('skills.otherSkills')}</h3>
<div className="soft-skills-grid">
{[
{ name: "Résolution de problèmes", icon: <Wrench size={20} /> },
{ name: "Travail en équipe", icon: <Code size={20} /> },
{ name: "Apprentissage continu", icon: <Database size={20} /> },
{ name: "Communication", icon: <Globe size={20} /> }
{ name: t('skills.problemSolving'), icon: <Wrench size={20} /> },
{ name: t('skills.teamwork'), icon: <Code size={20} /> },
{ name: t('skills.continuousLearning'), icon: <Database size={20} /> },
{ name: t('skills.communication'), icon: <Globe size={20} /> }
].map((softSkill, index) => (
<motion.div
key={softSkill.name}