feat: Initialize backend with Express and MySQL, and restructure frontend with new routing and language support.

This commit is contained in:
Van Leemput Dayron
2025-12-15 17:03:03 +01:00
parent 56897a0c2d
commit 6c11cf5213
54 changed files with 2000 additions and 174 deletions

26
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
EMAILJS_SETUP.md

153
frontend/README.md Normal file
View File

@@ -0,0 +1,153 @@
# 🌟 Portfolio Personnel - Dayron Van Leemput
Site web personnel moderne et responsive développé avec React, TypeScript et Framer Motion. Présentation complète de mon profil d'étudiant en Technologies de l'Informatique à la HELHa de Tournai.
## 🎯 À propos
Ce portfolio présente mon parcours, mes compétences et mes projets en tant qu'étudiant en 3ème année de Technologies de l'Informatique. Il met en avant ma passion pour le développement mobile (Flutter/Dart) et le développement web moderne (React/TypeScript).
## ✨ Fonctionnalités
- 🎨 **Design moderne** : Interface élégante et professionnelle
- 🌙 **Mode sombre/clair** : Basculement automatique selon les préférences système
- 📱 **Responsive** : Optimisé pour tous les appareils (mobile, tablette, desktop)
- 🎪 **Animations fluides** : Powered by Framer Motion
-**Performance** : Optimisé pour des chargements rapides
-**Accessibilité** : Respecte les standards WCAG
## 🏗️ Sections
- **🏠 Hero** - Présentation principale avec CTA
- **👨‍💻 À propos** - Mon histoire et ma passion
- **🛠️ Compétences** - Technologies avec barres de progression animées
- **🚀 Projets** - Travel Mate et projets à venir
- **🎓 Formation** - Parcours à la HELHa et objectifs d'apprentissage
- **📞 Contact** - Formulaire et informations de contact
## 🛠️ Technologies
### Frontend
- **React 19** - Framework JavaScript moderne
- **TypeScript** - Typage statique pour JavaScript
- **Framer Motion** - Animations et transitions fluides
- **CSS3** - Styles modernes avec variables CSS
- **Lucide React** - Icônes SVG optimisées
### Outils de développement
- **Vite** - Build tool rapide et moderne
- **ESLint** - Linter pour maintenir la qualité du code
- **Git** - Contrôle de version
## 🚀 Installation et utilisation
### Prérequis
- Node.js (version 18 ou supérieure)
- npm ou yarn
### Installation
```bash
# Cloner le repository
git clone https://github.com/Dayron-HELHa/xeewy.eu.git
cd xeewy.eu
# Installer les dépendances
npm install
# Démarrer le serveur de développement
npm run dev
# Construire pour la production
npm run build
# Prévisualiser la build de production
npm run preview
```
### Scripts disponibles
- `npm run dev` - Démarre le serveur de développement
- `npm run build` - Construit l'application pour la production
- `npm run preview` - Prévisualise la build de production
- `npm run lint` - Exécute ESLint pour vérifier la qualité du code
## 📂 Structure du projet
```
src/
├── components/ # Composants React
│ ├── Header.tsx # Navigation principale
│ ├── Hero.tsx # Section d'accueil
│ ├── About.tsx # Section à propos
│ ├── Skills.tsx # Compétences techniques
│ ├── Projects.tsx # Portfolio de projets
│ ├── Education.tsx # Formation et apprentissage
│ └── Contact.tsx # Formulaire de contact
├── App.tsx # Composant principal
├── main.tsx # Point d'entrée React
├── App.css # Styles principaux
└── index.css # Styles globaux
```
## 🎨 Personnalisation
### Couleurs et thèmes
Les variables CSS dans `src/index.css` et `src/App.css` permettent de personnaliser facilement :
- Palette de couleurs
- Espacements
- Typographie
- Effets d'ombre
- Transitions
### Contenu
Pour personnaliser le contenu :
1. **Informations personnelles** : Modifiez les données dans chaque composant
2. **Projets** : Ajoutez vos projets dans `Projects.tsx`
3. **Compétences** : Mettez à jour les technologies dans `Skills.tsx`
4. **CV** : Remplacez `public/cv-dayron-van-leemput.pdf`
## 🚀 Déploiement
### Netlify
```bash
npm run build
# Déployez le dossier 'dist' sur Netlify
```
### Vercel
```bash
npm run build
# Connectez votre repo GitHub à Vercel
```
### GitHub Pages
```bash
npm run build
# Configurez GitHub Pages pour utiliser le dossier 'dist'
```
## 🤝 Contribution
Ce portfolio est personnel, mais les suggestions d'amélioration sont les bienvenues !
1. Fork le projet
2. Créez une branche feature (`git checkout -b feature/amélioration`)
3. Commitez vos changements (`git commit -m 'Add: nouvelle fonctionnalité'`)
4. Push vers la branche (`git push origin feature/amélioration`)
5. Ouvrez une Pull Request
## 📄 Licence
Ce projet est sous licence MIT. Voir le fichier `LICENSE` pour plus de détails.
## 📞 Contact
**Dayron Van Leemput**
- 🎓 Étudiant en Technologies de l'Informatique - HELHa Tournai
- 💼 [LinkedIn](https://www.linkedin.com/in/dayron-van-leemput-992a94398)
- 🐱 [GitHub](https://github.com/Dayron-HELHa)
- 📧 Email : dev.dayronvl@gmail.com
---
*Développé par Dayron Van Leemput - Novembre 2025*

23
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/public/personnes.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>xeewy.be</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3957
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
frontend/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "xeewy.eu",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@emailjs/browser": "^4.4.1",
"framer-motion": "^12.23.24",
"lucide-react": "^0.553.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.10.1"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"sass": "^1.94.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.3",
"vite": "^7.2.2"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

72
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,72 @@
import { useState, useEffect } from 'react';
import { BrowserRouter, Routes, Route, Navigate, useParams } from 'react-router-dom';
import { LanguageProvider } from './contexts/LanguageContext';
import Header from './components/Header';
import Footer from './components/Footer';
import Home from './components/Home';
import TravelMate from './components/TravelMate';
import Policies from './components/Policies';
import ScrollToTop from './components/ScrollToTop';
import './styles/main.scss';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="/fr" replace />} />
<Route path="/:lang/*" element={
<LanguageProvider>
<AppContent />
</LanguageProvider>
} />
</Routes>
</BrowserRouter>
);
}
function AppContent() {
const [darkMode, setDarkMode] = useState(false);
const useParamsLang = useParams();
// Sync URL language with context if needed, or validate it
useEffect(() => {
// Optional: Check if useParamsLang.lang is valid, else redirect
}, [useParamsLang]);
useEffect(() => {
const savedTheme = localStorage.getItem('darkMode');
if (savedTheme) {
setDarkMode(JSON.parse(savedTheme));
} else {
setDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches);
}
}, []);
useEffect(() => {
localStorage.setItem('darkMode', JSON.stringify(darkMode));
document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : 'light');
}, [darkMode]);
const toggleDarkMode = () => {
setDarkMode(!darkMode);
};
return (
<>
<ScrollToTop />
<div className={`app ${darkMode ? 'dark' : 'light'}`} style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<Header darkMode={darkMode} toggleDarkMode={toggleDarkMode} />
<main style={{ flex: 1 }}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="travelmate" element={<TravelMate />} />
<Route path="travelmate/policies" element={<Policies />} />
</Routes>
</main>
<Footer />
</div>
</>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 KiB

BIN
frontend/src/assets/dvl.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,136 @@
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: 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 = {
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="about" className="about">
<div className="container">
<motion.div
key="about-header"
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('about.title')}</h2>
<p className="section-subtitle">
{t('about.subtitle')}
</p>
</motion.div>
<div className="about-content">
<motion.div
key="about-text"
className="about-text"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<motion.div className="about-card" variants={itemVariants}>
<h3>{t('about.journey.title')}</h3>
<p>
{t('about.journey.content')}
</p>
</motion.div>
<motion.div className="about-card" variants={itemVariants}>
<h3>{t('about.passion.title')}</h3>
<p>
{t('about.passion.content')}
</p>
</motion.div>
<motion.div className="about-card" variants={itemVariants}>
<h3>{t('about.goals.title')}</h3>
<p>
{t('about.goals.content')}
</p>
</motion.div>
</motion.div>
<motion.div
key="about-stats"
className="about-stats"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{stats.map((stat, index) => (
<motion.div
key={`stat-${index}`}
className="stat-card"
variants={itemVariants}
whileHover={{
scale: 1.05,
y: -5,
transition: { duration: 0.3 }
}}
>
<div className="stat-icon">
{stat.icon}
</div>
<div className="stat-value">{stat.value}</div>
<div className="stat-label">{stat.label}</div>
</motion.div>
))}
</motion.div>
</div>
<motion.div
key="about-highlight"
className="about-highlight"
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, delay: 0.5 }}
viewport={{ once: true }}
>
<div className="highlight-content">
<h3>{t('about.quote.title')}</h3>
<p>
"{t('about.quote.content')}"
</p>
<cite>- {t('about.quote.author')}</cite>
</div>
</motion.div>
</div>
</section>
);
};
export default About;

View File

@@ -0,0 +1,381 @@
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();
// Vérification du délai de 15 minutes par email
const storageKey = `lastMessageTime_${formData.email}`;
const lastMessageTime = localStorage.getItem(storageKey);
if (lastMessageTime) {
const timeSinceLastMessage = Date.now() - parseInt(lastMessageTime, 10);
const fifteenMinutes = 15 * 60 * 1000;
if (timeSinceLastMessage < fifteenMinutes) {
const remainingMinutes = Math.ceil((fifteenMinutes - timeSinceLastMessage) / 60000);
setError(`Veuillez attendre ${remainingMinutes} minutes avant d'envoyer un nouveau message avec cette adresse email.`);
return;
}
}
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) {
localStorage.setItem(`lastMessageTime_${formData.email}`, Date.now().toString());
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://git.xeewy.be/Xeewy", // 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
key="contact-header"
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
key="contact-content"
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>
</div>
</section>
);
};
export default Contact;

View File

@@ -0,0 +1,291 @@
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: 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: [
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} />
},
{
id: 2,
degree: t('education.highschool.degree'),
school: t('education.highschool.school'),
location: t('education.highschool.location'),
period: t('education.highschool.period'),
currentYear: "",
status: t('education.highschool.status'),
description: t('education.highschool.description'),
highlights: [
t('education.highschool.highlights.0'),
t('education.highschool.highlights.1'),
t('education.highschool.highlights.2')
],
color: "#3F51B5",
icon: <GraduationCap size={24} />
}
];
const certifications = [
{
title: t('education.cert1.title'),
provider: t('education.cert1.provider'),
date: t('education.cert1.date'),
skills: ["Dart", "Flutter", "Firebase", "API REST"],
color: "#2196F3"
},
{
title: t('education.cert2.title'),
provider: t('education.cert2.provider'),
date: t('education.cert2.date'),
skills: ["React", "TypeScript", "Hooks", "Context API"],
color: "#FF9800"
}
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.3
}
}
};
const itemVariants = {
hidden: { opacity: 0, x: -50 },
visible: {
opacity: 1,
x: 0,
transition: {
duration: 0.6,
ease: [0.25, 0.1, 0.25, 1] as const
}
}
};
return (
<section id="education" className="education">
<div className="container">
<motion.div
key="education-header"
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('education.title')}</h2>
<p className="section-subtitle">
{t('education.subtitle')}
</p>
</motion.div>
<div className="education-content">
{/* Formation principale */}
<motion.div
key="education-main"
className="education-main"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{education.map((edu) => (
<motion.div
key={edu.id}
className="education-card main-education"
variants={itemVariants}
whileHover={{
scale: 1.02,
y: -5,
transition: { duration: 0.3 }
}}
>
<div className="education-timeline">
<div
className="timeline-dot"
style={{ backgroundColor: edu.color }}
>
{edu.icon}
</div>
<div className="timeline-line"></div>
</div>
<div className="education-content-card">
<div className="education-header">
<div className="education-title">
<h3>{edu.degree}</h3>
<div className="education-meta">
<span className="school-name">
<BookOpen size={16} />
{edu.school}
</span>
<span className="location">
<MapPin size={16} />
{edu.location}
</span>
<span className="period">
<Calendar size={16} />
{edu.period}
</span>
</div>
</div>
<div className="education-status">
<span
className="status-badge"
style={{ backgroundColor: `${edu.color}20`, color: edu.color }}
>
{edu.status}
</span>
<span className="current-year">{edu.currentYear}</span>
</div>
</div>
<p className="education-description">{edu.description}</p>
<div className="education-highlights">
<h4>
<Target size={16} />
Matières principales
</h4>
<div className="highlights-grid">
{edu.highlights.map((highlight, index) => (
<motion.span
key={index}
className="highlight-tag"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
viewport={{ once: true }}
>
{highlight}
</motion.span>
))}
</div>
</div>
</div>
</motion.div>
))}
</motion.div>
{/* Certifications et formations complémentaires */}
<motion.div
className="certifications"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
viewport={{ once: true }}
>
<h3 className="certifications-title">
<Award size={24} />
{t('education.certifications.title')}
</h3>
<div className="certifications-grid">
{certifications.map((cert, index) => (
<motion.div
key={index}
className="certification-card"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{
scale: 1.03,
y: -3,
transition: { duration: 0.2 }
}}
viewport={{ once: true }}
>
<div className="cert-header">
<h4>{cert.title}</h4>
<span className="cert-provider">{cert.provider}</span>
<span className="cert-date">{cert.date}</span>
</div>
<div className="cert-skills">
{cert.skills.map((skill, skillIndex) => (
<span
key={skillIndex}
className="cert-skill-tag"
style={{ backgroundColor: `${cert.color}20`, color: cert.color }}
>
{skill}
</span>
))}
</div>
</motion.div>
))}
</div>
</motion.div>
{/* Objectifs d'apprentissage */}
<motion.div
className="learning-goals"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
viewport={{ once: true }}
>
<h3>{t('education.learningGoals2025')}</h3>
<div className="goals-grid">
{[
{ 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}
className="goal-item"
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
>
<div className="goal-header">
<span className="goal-text">{item.goal}</span>
<span className="goal-percentage">{item.progress}%</span>
</div>
<div className="goal-progress">
<motion.div
className="goal-progress-bar"
initial={{ width: 0 }}
whileInView={{ width: `${item.progress}%` }}
transition={{
duration: 1.5,
delay: index * 0.1 + 0.5,
ease: [0.25, 0.1, 0.25, 1] as const
}}
viewport={{ once: true }}
/>
</div>
</motion.div>
))}
</div>
</motion.div>
</div>
</div>
</section>
);
};
export default Education;

View File

@@ -0,0 +1,28 @@
import { motion } from 'framer-motion';
const Footer = () => {
return (
<motion.footer
className="footer"
style={{
padding: '2rem 0',
textAlign: 'center',
borderTop: '1px solid var(--border-color, rgba(255, 255, 255, 0.1))',
background: 'var(--bg-secondary, rgba(0, 0, 0, 0.2))',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true }}
>
<p style={{ opacity: 0.7, margin: 0 }}>
© {new Date().getFullYear()} Dayron Van Leemput.
</p>
</motion.footer>
);
};
export default Footer;

View File

@@ -0,0 +1,159 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Menu, X, Sun, Moon, Globe } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
import { useNavigate, useLocation, Link } from 'react-router-dom';
interface HeaderProps {
darkMode: boolean;
toggleDarkMode: () => void;
}
const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { language, setLanguage, t } = useLanguage();
const navigate = useNavigate();
const location = useLocation();
const menuItems = [
{ id: 'home', name: t('nav.home'), href: '#hero' },
{ id: 'about', name: t('nav.about'), href: '#about' },
{ id: 'skills', name: t('nav.skills'), href: '#skills' },
{ id: 'projects', name: t('nav.projects'), href: '#projects' },
{ id: 'education', name: t('nav.education'), href: '#education' },
{ id: 'contact', name: t('nav.contact'), href: '#contact' }
];
const handleNavigation = (href: string) => {
setIsMenuOpen(false);
if (location.pathname === `/${language}` || location.pathname === `/${language}/`) {
// Already on home, just scroll
const element = document.querySelector(href);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
} else {
// Not on home, navigate then scroll (using a simple timeout for simplicity or hash)
navigate(`/${language}`);
// Small timeout to allow navigation to render Home before scrolling
setTimeout(() => {
const element = document.querySelector(href);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}, 100);
}
};
const toggleLanguage = () => {
setLanguage(language === 'fr' ? 'en' : 'fr');
};
return (
<motion.header
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ duration: 0.5 }}
className="header"
>
<nav className="nav">
<motion.div
className="nav-brand"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Link to={`/${language}`} onClick={(e) => {
e.preventDefault();
handleNavigation('#hero');
}}>
Dayron Van Leemput
</Link>
</motion.div>
{/* Navigation desktop */}
<ul className="nav-menu desktop-menu">
{menuItems.map((item, index) => (
<motion.li
key={item.id}
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<a
href={item.href}
onClick={(e) => {
e.preventDefault();
handleNavigation(item.href);
}}
>
{item.name}
</a>
</motion.li>
))}
</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={t('btn.changeTheme')}
>
{darkMode ? <Sun size={20} /> : <Moon size={20} />}
</motion.button>
{/* Menu hamburger mobile */}
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="menu-toggle"
aria-label={t('btn.menu')}
>
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
</motion.button>
</div>
{/* Navigation mobile */}
<motion.ul
className={`nav-menu mobile-menu ${isMenuOpen ? 'open' : ''}`}
initial={false}
animate={isMenuOpen ? { opacity: 1, x: 0 } : { opacity: 0, x: '100%' }}
transition={{ duration: 0.3 }}
>
{menuItems.map((item) => (
<li key={item.id}>
<a
href={item.href}
onClick={(e) => {
e.preventDefault();
handleNavigation(item.href);
}}
>
{item.name}
</a>
</li>
))}
</motion.ul>
</nav>
</motion.header>
);
};
export default Header;

View File

@@ -0,0 +1,165 @@
import { motion } from 'framer-motion';
import { Download, Github, Linkedin, Mail } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
import dvlPhoto from '../assets/dvl.jpg';
const Hero = () => {
const { t } = useLanguage();
const handleDownloadCV = () => {
// Ici, vous pouvez ajouter le lien vers votre CV
const link = document.createElement('a');
link.href = '/Dayron_Van_Leemput_CV.pdf'; // Ajoutez votre CV dans le dossier public
link.download = 'Dayron_Van_Leemput_CV.pdf';
link.click();
};
return (
<section id="hero" className="hero">
<div className="hero-content">
<motion.div
key="hero-text"
className="hero-text"
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
>
<motion.h1
className="hero-title"
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
>
{t('hero.title')}
</motion.h1>
<motion.h2
className="hero-subtitle"
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
>
{t('hero.subtitle')}
</motion.h2>
<motion.p
className="hero-description"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.8 }}
>
{t('hero.description')}
</motion.p>
<motion.div
className="hero-buttons"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 1 }}
>
<motion.button
onClick={handleDownloadCV}
className="btn btn-primary"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Download size={20} />
{t('btn.downloadCV')}
</motion.button>
<motion.a
href="#contact"
onClick={(e) => {
e.preventDefault();
document.querySelector('#contact')?.scrollIntoView({ behavior: 'smooth' });
}}
className="btn btn-secondary"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Mail size={20} />
{t('btn.contactMe')}
</motion.a>
</motion.div>
<motion.div
className="hero-social"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 1.2 }}
>
<motion.a
href="https://git.xeewy.be/Xeewy" // Remplacez par votre profil GitHub
target="_blank"
rel="noopener noreferrer"
className="social-link"
whileHover={{ scale: 1.2, rotate: 5 }}
whileTap={{ scale: 0.9 }}
>
<Github size={24} />
</motion.a>
<motion.a
href="https://www.linkedin.com/in/dayron-van-leemput-992a94398" // Remplacez par votre profil LinkedIn
target="_blank"
rel="noopener noreferrer"
className="social-link"
whileHover={{ scale: 1.2, rotate: -5 }}
whileTap={{ scale: 0.9 }}
>
<Linkedin size={24} />
</motion.a>
</motion.div>
</motion.div>
<motion.div
key="hero-image"
className="hero-image"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1, delay: 0.5 }}
>
<motion.div
className="hero-avatar"
whileHover={{ scale: 1.05, rotate: 5 }}
transition={{ type: "spring", stiffness: 300, damping: 10 }}
>
<img
src={dvlPhoto}
alt="Dayron Van Leemput - Portrait"
className="avatar-image"
/>
</motion.div>
</motion.div>
</div>
{/* Particules d'animation en arrière-plan */}
<div className="hero-particles">
{[...Array(20)].map((_, i) => (
<motion.div
key={i}
className="particle"
initial={{
opacity: 0,
scale: 0,
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
}}
animate={{
opacity: [0, 1, 0],
scale: [0, 1, 0],
y: [null, -100],
}}
transition={{
duration: Math.random() * 3 + 2,
repeat: Infinity,
delay: Math.random() * 2,
}}
/>
))}
</div>
</section>
);
};
export default Hero;

View File

@@ -0,0 +1,26 @@
import { useEffect } from 'react';
import Hero from './Hero';
import About from './About';
import Skills from './Skills';
import Projects from './Projects';
import Education from './Education';
import Contact from './Contact';
const Home = () => {
useEffect(() => {
document.title = "Portfolio - Dayron Van Leemput";
}, []);
return (
<>
<Hero />
<About />
<Skills />
<Projects />
<Education />
<Contact />
</>
);
};
export default Home;

View File

@@ -0,0 +1,148 @@
import { motion } from 'framer-motion';
import { useLanguage } from '../contexts/LanguageContext';
import { Link } from 'react-router-dom';
import { ArrowLeft, ExternalLink, Camera, MapPin, Bell } from 'lucide-react';
const Policies = () => {
const { t, language } = useLanguage();
const sections = [2, 3, 4, 5, 6]; // Sections after the permission cards (1 is before)
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
};
const sectionVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
};
return (
<div className="policies-page" style={{ paddingTop: '100px', minHeight: '100vh', paddingBottom: '50px' }}>
<div className="container" style={{ maxWidth: '800px', margin: '0 auto', padding: '0 20px' }}>
<Link
to={`/${language}/travelmate`}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
marginBottom: '40px',
color: 'var(--text-color)',
textDecoration: 'none',
fontSize: '1rem',
opacity: 0.8
}}
>
<ArrowLeft size={18} />
{t('policies.back')}
</Link>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* Header */}
<motion.div variants={sectionVariants} style={{ marginBottom: '3rem', textAlign: 'center' }}>
<h1 style={{ fontSize: '2.5rem', marginBottom: '1rem', fontWeight: 'bold' }}>{t('policies.title')}</h1>
<p style={{ fontSize: '1.2rem', opacity: 0.7 }}>{t('policies.intro')}</p>
</motion.div>
{/* Section 1: Collection (Text) */}
<motion.div variants={sectionVariants} className="policy-section" style={{ marginBottom: '2.5rem' }}>
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem', fontWeight: '700' }}>
{t('policies.section.1.title')}
</h2>
<p style={{ lineHeight: '1.8', opacity: 0.9, fontSize: '1rem' }}>
{t('policies.section.1.content')}
</p>
</motion.div>
{/* Visual Permission Cards */}
<motion.div variants={sectionVariants} style={{ marginBottom: '4rem' }}>
<h3 style={{ fontSize: '1.2rem', marginBottom: '1.5rem', opacity: 0.8 }}>{t('policies.permissions.title')}</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: '1.5rem' }}>
{/* Camera */}
<div style={{ background: 'var(--card-bg, rgba(255,255,255,0.05))', padding: '1.5rem', borderRadius: '1rem', border: '1px solid var(--border-color, rgba(255, 255, 255, 0.1))' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '1rem', color: 'var(--primary-color)' }}>
<Camera size={24} />
<h4 style={{ margin: 0, fontSize: '1.1rem' }}>{t('policies.data.camera')}</h4>
</div>
<p style={{ opacity: 0.7, fontSize: '0.9rem', lineHeight: '1.5' }}>{t('policies.data.camera.desc')}</p>
</div>
{/* GPS */}
<div style={{ background: 'var(--card-bg, rgba(255,255,255,0.05))', padding: '1.5rem', borderRadius: '1rem', border: '1px solid var(--border-color, rgba(255, 255, 255, 0.1))' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '1rem', color: 'var(--primary-color)' }}>
<MapPin size={24} />
<h4 style={{ margin: 0, fontSize: '1.1rem' }}>{t('policies.data.gps')}</h4>
</div>
<p style={{ opacity: 0.7, fontSize: '0.9rem', lineHeight: '1.5' }}>{t('policies.data.gps.desc')}</p>
</div>
{/* Notifications */}
<div style={{ background: 'var(--card-bg, rgba(255,255,255,0.05))', padding: '1.5rem', borderRadius: '1rem', border: '1px solid var(--border-color, rgba(255, 255, 255, 0.1))' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '1rem', color: 'var(--primary-color)' }}>
<Bell size={24} />
<h4 style={{ margin: 0, fontSize: '1.1rem' }}>{t('policies.data.notif')}</h4>
</div>
<p style={{ opacity: 0.7, fontSize: '0.9rem', lineHeight: '1.5' }}>{t('policies.data.notif.desc')}</p>
</div>
</div>
</motion.div>
{/* Remaining Sections (Loop) */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '2.5rem' }}>
{sections.map((num) => (
<motion.div key={num} variants={sectionVariants} className="policy-section">
<h2 style={{ fontSize: '1.5rem', marginBottom: '1rem', fontWeight: '700' }}>
{t(`policies.section.${num}.title`)}
</h2>
<p style={{ lineHeight: '1.8', opacity: 0.9, fontSize: '1rem' }}>
{t(`policies.section.${num}.content`)}
</p>
</motion.div>
))}
</div>
{/* Google Policy Button */}
<motion.div variants={sectionVariants} style={{ marginTop: '5rem', textAlign: 'center' }}>
<a
href="https://policies.google.com/privacy"
target="_blank"
rel="noopener noreferrer"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '10px',
background: 'transparent',
border: '1px solid var(--primary-color)',
color: 'var(--primary-color)',
padding: '12px 30px',
borderRadius: '50px',
textDecoration: 'none',
fontWeight: '600',
transition: 'all 0.3s ease'
}}
className="hover-scale"
>
<ExternalLink size={18} />
{t('policies.googleBtn')}
</a>
</motion.div>
</motion.div>
</div>
</div>
);
};
export default Policies;

View File

@@ -0,0 +1,221 @@
import { motion } from 'framer-motion';
import { ExternalLink, MapPin, Wine } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
import { Link } from 'react-router-dom';
const Projects = () => {
const { t, language } = useLanguage();
const projects = [
{
id: 1,
title: t('projects.travelMate.title'),
description: t('projects.travelMate.description'),
status: t('projects.status.available'),
technologies: ["Dart", "Flutter", "Firebase"],
features: [
t('projects.travelMate.feature1'),
t('projects.travelMate.feature2'),
t('projects.travelMate.feature3'),
t('projects.travelMate.feature4')
],
color: "#4CAF50",
icon: <MapPin size={24} />,
links: {
demo: "/travelmate"
},
image: "/travel-mate-preview.png" // Ajoutez votre image dans le dossier public
},
{
id: 3,
title: t('projects.shelbys.title'),
description: t('projects.shelbys.description'),
status: t('projects.status.online'),
technologies: ["React", "Vite", "TailwindCSS", "Framer Motion"], // Inferring stack based on modern standards and user's other projects
features: [
t('projects.shelbys.feature1'),
t('projects.shelbys.feature2'),
t('projects.shelbys.feature4')
],
color: "#E91E63", // A distinct color
icon: <Wine size={24} />,
links: {
demo: "https://shelbys.be"
},
image: "/shelbys-preview.png" // Placeholder, user might need to add this
},
{
id: 2,
title: t('projects.portfolio.title'),
description: t('projects.portfolio.description'),
status: t('projects.status.current'),
technologies: ["React", "TypeScript", "Framer Motion", "CSS3"],
features: [
t('projects.portfolio.feature1'),
t('projects.portfolio.feature2'),
t('projects.portfolio.feature3'),
t('projects.portfolio.feature4')
],
color: "#2196F3",
icon: <ExternalLink size={24} />,
links: {
demo: "https://xeewy.be" // Remplacez par votre lien
}
}
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.3,
delayChildren: 0.2
}
}
};
const projectVariants = {
hidden: { opacity: 0, y: 50 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.8,
ease: [0.25, 0.1, 0.25, 1] as const
}
}
};
return (
<section id="projects" className="projects">
<div className="container">
<motion.div
key="projects-header"
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('projects.title')}</h2>
<p className="section-subtitle">
{t('projects.subtitle')}
</p>
</motion.div>
<motion.div
key="projects-grid"
className="projects-grid"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{projects.map((project, index) => (
<motion.div
key={project.id}
className="project-card"
variants={projectVariants}
whileHover={{
y: -10,
scale: 1.02,
transition: { duration: 0.3 }
}}
>
<div className="project-header">
<div
className="project-icon"
style={{ backgroundColor: `${project.color}20`, color: project.color }}
>
{project.icon}
</div>
<div className="project-status">
<span className="status-badge" style={{ backgroundColor: `${project.color}20`, color: project.color }}>
{project.status}
</span>
</div>
</div>
<div className="project-content">
<h3 className="project-title">{project.title}</h3>
<p className="project-description">{project.description}</p>
<div className="project-technologies">
{project.technologies.map((tech, techIndex) => (
<motion.span
key={tech}
className="tech-tag"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.3,
delay: (index * 0.1) + (techIndex * 0.05)
}}
viewport={{ once: true }}
>
{tech}
</motion.span>
))}
</div>
<div className="project-features">
<h4>{t('projects.features')}</h4>
<ul>
{project.features.map((feature, featureIndex) => (
<motion.li
key={featureIndex}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{
duration: 0.5,
delay: (index * 0.2) + (featureIndex * 0.1)
}}
viewport={{ once: true }}
>
{feature}
</motion.li>
))}
</ul>
</div>
</div>
<div className="project-footer">
<div className="project-links">
{project.links.demo !== "#" && (
project.links.demo.startsWith('/') ? (
<Link to={`/${language}${project.links.demo}`} style={{ textDecoration: 'none' }}>
<motion.div
className="project-link primary"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
<ExternalLink size={20} />
{t('projects.btn.viewProject')}
</motion.div>
</Link>
) : (
<motion.a
href={project.links.demo}
target="_blank"
rel="noopener noreferrer"
className="project-link primary"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
<ExternalLink size={20} />
{t('projects.btn.viewProject')}
</motion.a>
)
)}
</div>
</div>
</motion.div>
))}
</motion.div>
</div>
</section>
);
};
export default Projects;

View File

@@ -0,0 +1,18 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const ScrollToTop = () => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo({
top: 0,
left: 0,
behavior: 'instant',
});
}, [pathname]);
return null;
};
export default ScrollToTop;

View File

@@ -0,0 +1,199 @@
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 = [
{
id: 'mobile',
icon: <Smartphone size={32} />,
title: t('skills.category.mobile'),
color: "#4FC3F7",
skills: [
{ name: "Dart", level: 85, color: "#0175C2" },
{ name: "Flutter", level: 80, color: "#02569B" }
]
},
{
id: 'frontend',
icon: <Globe size={32} />,
title: t('skills.category.frontend'),
color: "#42A5F5",
skills: [
{ name: "React", level: 75, color: "#61DAFB" },
{ name: "TypeScript", level: 70, color: "#3178C6" },
{ name: "JavaScript", level: 80, color: "#F7DF1E" }
]
},
{
id: 'backend',
icon: <Server size={32} />,
title: t('skills.category.backend'),
color: "#66BB6A",
skills: [
{ name: "Java", level: 75, color: "#ED8B00" },
{ name: "C#", level: 65, color: "#239120" }
]
},
{
id: 'tools',
icon: <Wrench size={32} />,
title: t('skills.category.tools'),
color: "#AB47BC",
skills: [
{ name: "Git", level: 70, color: "#F05032" },
{ name: "VS Code", level: 90, color: "#007ACC" },
{ name: "Android Studio", level: 75, color: "#3DDC84" }
]
}
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.3
}
}
};
const categoryVariants = {
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="skills" className="skills">
<div className="container">
<motion.div
key="skills-header"
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('skills.title')}</h2>
<p className="section-subtitle">
{t('skills.subtitle')}
</p>
</motion.div>
<motion.div
key="skills-grid"
className="skills-grid"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{skillCategories.map((category, categoryIndex) => (
<motion.div
key={category.id}
className="skill-category"
variants={categoryVariants}
whileHover={{
scale: 1.02,
y: -5,
transition: { duration: 0.3 }
}}
>
<div className="category-header">
<div
className="category-icon"
style={{ backgroundColor: `${category.color}20`, color: category.color }}
>
{category.icon}
</div>
<h3 className="category-title">{category.title}</h3>
</div>
<div className="skills-list">
{category.skills.map((skill, skillIndex) => (
<motion.div
key={skill.name}
className="skill-item"
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{
duration: 0.5,
delay: (categoryIndex * 0.2) + (skillIndex * 0.1)
}}
viewport={{ once: true }}
>
<div className="skill-header">
<span className="skill-name">{skill.name}</span>
<span className="skill-percentage">{skill.level}%</span>
</div>
<div className="skill-bar">
<motion.div
className="skill-progress"
style={{ backgroundColor: skill.color }}
initial={{ width: 0 }}
whileInView={{ width: `${skill.level}%` }}
transition={{
duration: 1.5,
delay: (categoryIndex * 0.2) + (skillIndex * 0.1) + 0.5,
ease: [0.25, 0.1, 0.25, 1] as const
}}
viewport={{ once: true }}
/>
</div>
</motion.div>
))}
</div>
</motion.div>
))}
</motion.div>
{/* Section des soft skills */}
<motion.div
className="soft-skills"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
viewport={{ once: true }}
>
<h3 className="soft-skills-title">{t('skills.otherSkills')}</h3>
<div className="soft-skills-grid">
{[
{ 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}
className="soft-skill-tag"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{
scale: 1.05,
transition: { duration: 0.2 }
}}
viewport={{ once: true }}
>
{softSkill.icon}
<span>{softSkill.name}</span>
</motion.div>
))}
</div>
</motion.div>
</div>
</section>
);
};
export default Skills;

View File

@@ -0,0 +1,271 @@
import { useEffect } from 'react';
import { motion } from 'framer-motion';
import { useLanguage } from '../contexts/LanguageContext';
import { Link, Outlet } from 'react-router-dom';
import { Shield, Smartphone, Map, DollarSign, Users, Globe, Code } from 'lucide-react';
import appIcon from '../assets/app_icon.png';
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
};
const FeatureCard = ({ title, icon: Icon, description }: { title: string, icon: any, description: string }) => (
<motion.div
variants={itemVariants}
className="feature-card"
style={{
background: 'var(--card-bg, rgba(255, 255, 255, 0.03))', // Slightly more transparent for glass effect
backdropFilter: 'blur(10px)', // Glassmorphism
padding: '2rem',
borderRadius: '1.5rem',
border: '1px solid var(--border-color, rgba(255, 255, 255, 0.08))',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center', // Center items horizontally
textAlign: 'center', // Center text
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)'
}}
whileHover={{
y: -5,
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
borderColor: 'var(--primary-color, #4f46e5)'
}}
>
<div style={{
marginBottom: '1.5rem',
background: 'var(--primary-color-alpha, rgba(79, 70, 229, 0.1))',
width: 'fit-content',
padding: '12px',
borderRadius: '12px'
}}>
<Icon size={32} color="var(--primary-color)" />
</div>
<h3 style={{
marginBottom: '1rem',
fontSize: '1.5rem',
fontWeight: '600',
color: 'var(--text-color)' // Explicit color for dark mode
}}>
{title}
</h3>
<p style={{
opacity: 0.8,
lineHeight: '1.7',
flex: 1,
fontSize: '1rem',
color: 'var(--text-color)' // Explicit color
}}>
{description}
</p>
</motion.div>
);
const TravelMate = () => {
const { t, language } = useLanguage();
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2
}
}
};
useEffect(() => {
document.title = "Travel Mate";
}, []);
return (
<div className="travel-mate-page" style={{ paddingTop: '100px', minHeight: '100vh', paddingBottom: '50px' }}>
<div className="container" style={{ maxWidth: '1400px', margin: '0 auto', padding: '0 20px' }}>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* Header Section */}
<motion.div variants={itemVariants} style={{ textAlign: 'center', marginBottom: '4rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<motion.img
src={appIcon}
alt="Travel Mate Icon"
style={{ width: '120px', height: '120px', borderRadius: '24px', marginBottom: '2rem', boxShadow: '0 10px 30px rgba(0,0,0,0.2)' }}
whileHover={{ scale: 1.05, rotate: 5 }}
transition={{ type: "spring", stiffness: 300 }}
/>
<h1 className="gradient-text" style={{ fontSize: '4rem', fontWeight: '800', marginBottom: '1rem' }}>
{t('travelmate.page.mainTitle')}
</h1>
<p style={{ fontSize: '1.5rem', opacity: 0.7, maxWidth: '600px', margin: '0 auto 2rem auto' }}>
{t('travelmate.page.subtitle')}
</p>
{/* View Code Button */}
<motion.a
href="https://git.xeewy.be/Xeewy/TravelMate"
target="_blank"
rel="noopener noreferrer"
className="btn btn-primary"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '10px',
textDecoration: 'none',
fontSize: '1.1rem',
padding: '0.8rem 1.5rem',
borderRadius: '50px'
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Code size={20} />
{t('travelmate.viewCode') || "Voir le code"}
</motion.a>
</motion.div>
{/* Description as Intro */}
<motion.div variants={itemVariants} style={{ marginBottom: '5rem', maxWidth: '800px', margin: '0 auto 5rem auto', textAlign: 'center' }}>
<p style={{
lineHeight: '1.8',
fontSize: '1.4rem',
opacity: 0.9,
fontStyle: 'italic',
color: 'var(--text-color)'
}}>
"{t('travelmate.page.intro')}"
</p>
</motion.div>
{/* Highlights Sections */}
<motion.div variants={itemVariants} style={{ marginBottom: '6rem' }}>
<h2 style={{
textAlign: 'center',
marginBottom: '4rem',
fontSize: '2.5rem',
fontWeight: '700',
color: 'var(--text-color)'
}}>{t('travelmate.highlights.title')}</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '2.5rem' }}>
{[1, 2, 3, 4].map((num) => (
<FeatureCard
key={num}
title={t(`travelmate.highlight.${num}.title`)}
description={t(`travelmate.highlight.${num}.desc`)}
icon={
num === 1 ? Users :
num === 2 ? DollarSign :
num === 3 ? Map :
Smartphone
}
/>
))}
</div>
</motion.div>
{/* Conclusion */}
<motion.div variants={itemVariants} style={{ marginBottom: '6rem', textAlign: 'center', maxWidth: '800px', margin: '0 auto 6rem auto' }}>
<p style={{
fontSize: '1.8rem',
fontWeight: 'bold',
color: 'var(--primary-color)',
lineHeight: '1.4'
}}>
{t('travelmate.page.conclusion')}
</p>
</motion.div>
{/* Tech Stack */}
<motion.div variants={itemVariants} style={{ marginBottom: '5rem' }}>
<h2 style={{ textAlign: 'center', marginBottom: '3rem' }}>{t('travelmate.tech.title')}</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '2rem' }}>
{/* Frontend */}
<div style={{ padding: '1.5rem' }}>
<h3 style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '1.5rem', color: 'var(--primary-color)' }}>
<Smartphone /> {t('travelmate.tech.frontend')}
</h3>
<ul style={{ listStyle: 'none', padding: 0 }}>
{[1, 2, 3].map(i => (
<li key={i} style={{ marginBottom: '0.8rem', paddingLeft: '1rem', borderLeft: '2px solid var(--primary-color)' }}>
{t(`travelmate.tech.frontend.${i}`)}
</li>
))}
</ul>
</div>
{/* Backend */}
<div style={{ padding: '1.5rem' }}>
<h3 style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '1.5rem', color: 'var(--primary-color)' }}>
<Globe /> {t('travelmate.tech.backend')}
</h3>
<ul style={{ listStyle: 'none', padding: 0 }}>
{[1, 2, 3, 4].map(i => (
<li key={i} style={{ marginBottom: '0.8rem', paddingLeft: '1rem', borderLeft: '2px solid var(--primary-color)' }}>
{t(`travelmate.tech.backend.${i}`)}
</li>
))}
</ul>
</div>
{/* API */}
<div style={{ padding: '1.5rem' }}>
<h3 style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '1.5rem', color: 'var(--primary-color)' }}>
<Code /> {t('travelmate.tech.api')}
</h3>
<ul style={{ listStyle: 'none', padding: 0 }}>
{[1, 2, 3].map(i => (
<li key={i} style={{ marginBottom: '0.8rem', paddingLeft: '1rem', borderLeft: '2px solid var(--primary-color)' }}>
{t(`travelmate.tech.api.${i}`)}
</li>
))}
</ul>
</div>
</div>
</motion.div>
{/* Policies CTA */}
<motion.div
variants={itemVariants}
style={{
padding: '2rem',
background: 'var(--card-bg, rgba(255, 255, 255, 0.05))',
borderRadius: '1rem',
border: '1px solid var(--border-color, rgba(255, 255, 255, 0.1))',
textAlign: 'center',
maxWidth: '600px',
margin: '0 auto'
}}
>
<Shield className="text-primary" size={48} style={{ marginBottom: '1rem', color: 'var(--primary-color)' }} />
<h3 style={{ marginBottom: '1.5rem' }}>
{t('policies.title')}
</h3>
<Link
to={`/${language}/travelmate/policies`}
className="btn btn-secondary"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '10px',
textDecoration: 'none'
}}
>
{t('travelmate.policies.link')}
</Link>
</motion.div>
</motion.div>
<Outlet />
</div>
</div>
);
};
export default TravelMate;

View File

@@ -0,0 +1,495 @@
// ========================
// CONTEXTE DE LANGUE
// ========================
import React, { createContext, useContext } from 'react';
import type { ReactNode } from 'react';
// Types pour les langues supportées
export type Language = 'fr' | 'en';
// Interface pour le contexte
interface LanguageContextType {
language: Language;
setLanguage: (lang: Language) => void;
t: (key: string) => string;
}
// Textes traduits
const translations = {
fr: {
// Navigation
'nav.home': 'Accueil',
'nav.about': 'À propos',
'nav.skills': 'Compétences',
'nav.projects': 'Projets',
'nav.education': 'Formation',
'nav.contact': 'Contact',
// Boutons
'btn.changeTheme': 'Changer de thème',
'btn.menu': 'Menu',
'btn.changeLang': 'English',
'btn.downloadCV': 'Télécharger CV',
'btn.viewProjects': 'Voir mes projets',
'btn.contactMe': 'Me contacter',
// Hero
'hero.greeting': 'Salut, je suis',
'hero.title': 'Dayron Van Leemput',
'hero.subtitle': 'Étudiant en Technologies de l\'Informatique',
'hero.description': 'Bac 3 à la HELHa de Tournai | Jeune développeur passionné par les nouvelles technologies et le développement d\'applications innovantes',
// About
'about.title': 'À propos de moi',
'about.subtitle': 'Découvrez mon parcours et mes passions',
'about.stats.year': '3ème',
'about.stats.yearLabel': 'Année d\'études',
'about.stats.passion': '100%',
'about.stats.passionLabel': 'Passion',
'about.stats.goals': '∞',
'about.stats.goalsLabel': 'Objectifs',
'about.stats.fuel': '☕',
'about.stats.fuelLabel': 'Fuel quotidien',
'about.journey.title': 'Mon parcours',
'about.journey.content': 'Actuellement en 3ème année de Technologies de l\'Informatique à 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.',
'about.passion.title': 'Ma passion',
'about.passion.content': '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 Flutter et le développement web moderne avec React et TypeScript.',
'about.goals.title': 'Mes objectifs',
'about.goals.content': '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.',
'about.quote.title': 'En quelques mots',
'about.quote.content': '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.',
'about.quote.author': 'Steve Jobs',
// Skills
'skills.title': 'Mes Compétences',
'skills.subtitle': 'Technologies et outils que je maîtrise',
'skills.category.mobile': 'Mobile',
'skills.category.frontend': 'Frontend',
'skills.category.backend': 'Backend',
'skills.category.tools': 'Outils & Autres',
'skills.otherSkills': 'Autres compétences',
'skills.problemSolving': 'Résolution de problèmes',
'skills.teamwork': 'Travail en équipe',
'skills.continuousLearning': 'Apprentissage continu',
'skills.communication': 'Communication',
// Projects
'projects.title': 'Mes Projets',
'projects.subtitle': 'Découvrez mes réalisations et mes expériences',
'projects.status.available': 'Bientôt disponible sur App Store et Play Store',
'projects.status.current': 'Projet actuel',
'projects.status.online': 'En ligne',
'projects.features': 'Fonctionnalités principales :',
'projects.btn.code': 'Code',
'projects.btn.viewProject': 'Voir le projet',
'projects.travelMate.title': 'Travel Mate',
'projects.travelMate.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.',
'projects.travelMate.feature1': 'Planification de voyage collaborative',
'projects.travelMate.feature2': 'Gestion des dépenses partagées',
'projects.travelMate.feature3': 'Découverte d\'activités locales',
'projects.travelMate.feature4': 'Coordination en temps réel',
'projects.portfolio.title': 'Portfolio Web',
'projects.portfolio.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.',
'projects.portfolio.feature1': 'Design responsive',
'projects.portfolio.feature2': 'Animations fluides',
'projects.portfolio.feature3': 'Mode sombre/clair',
'projects.portfolio.feature4': 'Performance optimisée',
'projects.shelbys.title': 'Shelbys Bar',
'projects.shelbys.description': 'Site vitrine élégant pour le bar Shelbys Bar. Présente l\'ambiance, le menu et les événements du bar avec un design moderne et immersif.',
'projects.shelbys.feature1': 'Design moderne et immersif',
'projects.shelbys.feature2': 'Présentation du menu',
'projects.shelbys.feature4': 'Informations pratiques',
// Education
'education.title': 'Formation',
'education.subtitle': 'Mon parcours académique et professionnel',
'education.learningGoals2025': 'Objectifs d\'apprentissage 2025',
'education.goal1': 'Maîtriser Firebase et les services cloud',
'education.goal2': 'Approfondir Spring Boot pour le backend',
'education.goal3': 'Apprendre Docker et les conteneurs',
'education.goal4': 'Développer mes compétences en UI/UX',
'education.degree': 'Bachelier en Technologies de l\'Informatique',
'education.school': 'HELHa - Haute École Louvain en Hainaut',
'education.location': 'Tournai, Belgique',
'education.period': '2023 - 2026',
'education.currentYear': '3ème année',
'education.status': 'En cours',
'education.description': 'Formation complète en développement logiciel, programmation, bases de données, réseaux et gestion de projets informatiques.',
'education.highlights.0': 'Programmation orientée objet (Java, C#)',
'education.highlights.1': 'Développement web (HTML, CSS, JavaScript, React)',
'education.highlights.2': 'Développement mobile (Flutter, Dart)',
'education.highlights.3': 'Bases de données et SQL',
'education.highlights.4': 'Gestion de projets',
'education.highlights.5': 'Réseaux et systèmes',
'education.certifications.title': 'Certifications & Formations',
'education.cert1.title': 'Développement Mobile Flutter',
'education.cert1.provider': 'Formation autodidacte',
'education.cert1.date': '2024',
'education.cert2.title': 'React & TypeScript',
'education.cert2.provider': 'Projets personnels',
'education.cert2.date': '2024',
'education.highschool.degree': 'CESS (Certificat d\'Enseignement Secondaire Supérieur)',
'education.highschool.school': 'Athénée Royal d\'Ath',
'education.highschool.location': 'Ath, Belgique',
'education.highschool.period': '2016 - 2022',
'education.highschool.status': 'Diplômé',
'education.highschool.description': 'Section Math-Sciences (Math 8h - Sciences 7h)',
'education.highschool.highlights.0': 'Mathématiques avancées',
'education.highschool.highlights.1': 'Sciences (Physique, Chimie, Biologie)',
'education.highschool.highlights.2': 'Langues',
// Travel Mate Page
'travelmate.page.mainTitle': 'Travel Mate 🌍',
'travelmate.page.subtitle': 'Le compagnon de voyage indispensable',
'travelmate.page.intro': 'Redécouvrez le voyage en groupe avec Travel Mate. Plus qu\'une simple application, c\'est votre copilote pour des aventures sans friction. Oubliez les tableaux Excel complexes et les débats sur "qui doit combien". Concentrez-vous sur l\'essentiel : créer des souvenirs inoubliables.',
'travelmate.highlights.title': '✨ Points Forts',
'travelmate.highlight.1.title': 'Gestion de Groupe Simplifiée',
'travelmate.highlight.1.desc': 'Créez votre voyage et invitez vos compagnons en un clic via un lien unique. L\'organisation démarre instantanément.',
'travelmate.highlight.2.title': 'Dépenses Maîtrisées (Split)',
'travelmate.highlight.2.desc': 'Suivez les dépenses en temps réel et laissez l\'application équilibrer les comptes automatiquement. Fini les calculs compliqués !',
'travelmate.highlight.3.title': 'Planification Collaborative',
'travelmate.highlight.3.desc': 'Un agenda partagé et une carte interactive pour que chacun puisse proposer et visualiser les activités du groupe.',
'travelmate.highlight.4.title': 'Communication Fluide',
'travelmate.highlight.4.desc': 'Un chat intégré pour centraliser les discussions et garder tout le monde sur la même longueur d\'onde.',
'travelmate.page.conclusion': 'Prêt à partir ? Avec Travel Mate, l\'aventure commence dès l\'organisation. Voyagez l\'esprit léger, on s\'occupe du reste.',
'travelmate.tech.title': '🛠️ Technologies utilisées',
'travelmate.tech.frontend': 'Frontend Mobile (Flutter)',
'travelmate.tech.frontend.1': 'Architecture BLoC pour une gestion d\'état robuste',
'travelmate.tech.frontend.2': 'Interface utilisateur fluide et réactive',
'travelmate.tech.frontend.3': 'Intégration native (Caméra, GPS, Notifications)',
'travelmate.tech.backend': 'Backend (Firebase)',
'travelmate.tech.backend.1': 'Authentication (Google, Apple, Email)',
'travelmate.tech.backend.2': 'Firestore (Base de données temps réel)',
'travelmate.tech.backend.3': 'Cloud Functions (Logique serveur)',
'travelmate.tech.backend.4': 'Storage (Médias)',
'travelmate.tech.api': 'APIs & Outils',
'travelmate.tech.api.1': 'Google Maps API / Mapbox',
'travelmate.tech.api.2': 'Stripe (tbc) pour les paiements',
'travelmate.tech.api.3': 'CI/CD avec Gitea Actions',
'travelmate.viewCode': 'Voir le code',
'travelmate.policies.link': 'Voir les politiques de confidentialité',
// Policies Page
'policies.back': 'Retour',
'policies.title': 'Politiques de confidentialité',
'policies.intro': 'Votre vie privée est importante pour nous. Voici comment nous protégeons vos données.',
'policies.section.1.title': 'Collecte d\'informations',
'policies.section.1.content': 'Nous collectons des informations que vous nous fournissez directement, comme votre nom, adresse e-mail et préférences de voyage.',
'policies.permissions.title': 'Permissions Appareil',
'policies.data.camera': 'Caméra & Galerie',
'policies.data.camera.desc': 'Pour ajouter des photos aux souvenirs ou scanner des factures.',
'policies.data.gps': 'Localisation (GPS)',
'policies.data.gps.desc': 'Pour la carte interactive et les suggestions d\'activités à proximité.',
'policies.data.notif': 'Notifications',
'policies.data.notif.desc': 'Pour vous alerter des nouveaux messages, dépenses ou changements de programme.',
'policies.section.2.title': 'Utilisation des données',
'policies.section.2.content': 'Vos données sont utilisées pour améliorer votre expérience utilisateur et vous proposer des recommandations personnalisées.',
'policies.section.3.title': 'Protection des données',
'policies.section.3.content': 'Nous mettons en place des mesures de sécurité appropriées pour protéger vos informations personnelles.',
'policies.section.4.title': 'Partage des données',
'policies.section.4.content': 'Nous ne partageons pas vos informations personnelles avec des tiers sans votre consentement explicite.',
'policies.section.5.title': 'Vos droits',
'policies.section.5.content': 'Vous avez le droit d\'accéder, de corriger ou de supprimer vos données personnelles à tout moment. Veuillez nous contacter pour toute demande.',
'policies.section.6.title': 'Nous contacter',
'policies.section.6.content': 'Pour toute question concernant cette politique de confidentialité, veuillez nous contacter à support@travelmate.com',
'policies.googleBtn': 'Politique de confidentialité Google',
// Contact
'contact.title': 'Contactez-moi',
'contact.subtitle': 'Une question, un projet ou simplement envie de discuter ? N\'hésitez pas à me contacter !',
'contact.form.name': 'Nom complet',
'contact.form.email': 'Email',
'contact.form.subject': 'Sujet',
'contact.form.message': 'Message',
'contact.form.send': 'Envoyer le message',
'contact.form.sending': 'Envoi en cours...',
'contact.success': 'Message envoyé avec succès ! Je vous répondrai bientôt.',
'contact.stayInTouch': 'Restons en contact',
'contact.intro': 'Je suis toujours intéressé par de nouveaux projets, des collaborations ou simplement des discussions sur la technologie. N\'hésitez pas à me contacter !',
'contact.findMeOn': 'Retrouvez-moi aussi sur :',
'contact.sendMessage': 'Envoyez-moi un message',
},
en: {
// Navigation
'nav.home': 'Home',
'nav.about': 'About',
'nav.skills': 'Skills',
'nav.projects': 'Projects',
'nav.education': 'Education',
'nav.contact': 'Contact',
// Boutons
'btn.changeTheme': 'Change theme',
'btn.menu': 'Menu',
'btn.changeLang': 'Français',
'btn.downloadCV': 'Download CV',
'btn.viewProjects': 'View my projects',
'btn.contactMe': 'Contact me',
// Hero
'hero.greeting': 'Hi, I am',
'hero.title': 'Dayron Van Leemput',
'hero.subtitle': 'Computer Technology Student',
'hero.description': 'Bachelor 3 at HELHa Tournai | Young developer passionate about new technologies and innovative application development',
// About
'about.title': 'About me',
'about.subtitle': 'Discover my journey and passions',
'about.stats.year': '3rd',
'about.stats.yearLabel': 'Year of studies',
'about.stats.passion': '100%',
'about.stats.passionLabel': 'Passion',
'about.stats.goals': '∞',
'about.stats.goalsLabel': 'Goals',
'about.stats.fuel': '☕',
'about.stats.fuelLabel': 'Daily fuel',
'about.journey.title': 'My journey',
'about.journey.content': 'Currently in my 3rd year of Computer Technology at HELHa Tournai, I am passionate about application development and new technologies. My journey has allowed me to acquire a solid technical foundation and a methodical approach to development.',
'about.passion.title': 'My passion',
'about.passion.content': 'What drives me most is creating innovative solutions that solve real problems. I particularly enjoy mobile development with Flutter and modern web development with React and TypeScript.',
'about.goals.title': 'My goals',
'about.goals.content': 'I constantly seek to improve my skills and stay up to date with the latest technological trends. My goal is to become a versatile full-stack developer and contribute to projects that have a positive impact.',
'about.quote.title': 'In a few words',
'about.quote.content': 'Technology is nothing. What\'s important is that you have a faith in people, that they\'re basically good and smart, and if you give them tools, they\'ll do wonderful things with them.',
'about.quote.author': 'Steve Jobs',
// Skills
'skills.title': 'My Skills',
'skills.subtitle': 'Technologies and tools I master',
'skills.category.mobile': 'Mobile',
'skills.category.frontend': 'Frontend',
'skills.category.backend': 'Backend',
'skills.category.tools': 'Tools & Others',
'skills.otherSkills': 'Other skills',
'skills.problemSolving': 'Problem solving',
'skills.teamwork': 'Teamwork',
'skills.continuousLearning': 'Continuous learning',
'skills.communication': 'Communication',
// Projects
'projects.title': 'My Projects',
'projects.subtitle': 'Discover my achievements and experiences',
'projects.status.available': 'Coming soon on App Store and Play Store',
'projects.status.current': 'Current project',
'projects.status.online': 'Online',
'projects.features': 'Main features:',
'projects.btn.code': 'Code',
'projects.btn.viewProject': 'View project',
'projects.travelMate.title': 'Travel Mate',
'projects.travelMate.description': 'Mobile application designed to simplify group travel organization. It allows centralizing all important travel information: planning, expense management, activity discovery and coordination between participants.',
'projects.travelMate.feature1': 'Collaborative travel planning',
'projects.travelMate.feature2': 'Shared expense management',
'projects.travelMate.feature3': 'Local activity discovery',
'projects.travelMate.feature4': 'Real-time coordination',
'projects.portfolio.title': 'Web Portfolio',
'projects.portfolio.description': 'Modern and responsive personal website developed with React and TypeScript. Includes fluid animations, dark/light mode and modular architecture.',
'projects.portfolio.feature1': 'Responsive design',
'projects.portfolio.feature2': 'Fluid animations',
'projects.portfolio.feature3': 'Dark/light mode',
'projects.portfolio.feature4': 'Optimized performance',
'projects.shelbys.title': 'Shelbys Bar',
'projects.shelbys.description': 'Elegant showcase website for Shelbys Bar. Presents the atmosphere, menu, and events of the bar with a modern and immersive design.',
'projects.shelbys.feature1': 'Modern and immersive design',
'projects.shelbys.feature2': 'Menu presentation',
'projects.shelbys.feature4': 'Practical information',
// Education
'education.title': 'Education',
'education.subtitle': 'My academic and professional journey',
'education.learningGoals2025': '2025 Learning Goals',
'education.goal1': 'Master Firebase and cloud services',
'education.goal2': 'Deepen Spring Boot for backend',
'education.goal3': 'Learn Docker and containers',
'education.goal4': 'Develop my UI/UX skills',
'education.degree': 'Bachelor in Computer Technology',
'education.school': 'HELHa - Haute École Louvain en Hainaut',
'education.location': 'Tournai, Belgium',
'education.period': '2023 - 2026',
'education.currentYear': '3rd year',
'education.status': 'In progress',
'education.description': 'Complete training in software development, programming, databases, networks and IT project management.',
'education.highlights.0': 'Object-oriented programming (Java, C#)',
'education.highlights.1': 'Web development (HTML, CSS, JavaScript, React)',
'education.highlights.2': 'Mobile development (Flutter, Dart)',
'education.highlights.3': 'Databases and SQL',
'education.highlights.4': 'Project management',
'education.highlights.5': 'Networks and systems',
'education.certifications.title': 'Certifications & Training',
'education.cert1.title': 'Flutter Mobile Development',
'education.cert1.provider': 'Self-taught training',
'education.cert1.date': '2024',
'education.cert2.title': 'React & TypeScript',
'education.cert2.provider': 'Personal projects',
'education.cert2.date': '2024',
'education.highschool.degree': 'CESS (Certificate of Upper Secondary Education)',
'education.highschool.school': 'Athénée Royal d\'Ath',
'education.highschool.location': 'Ath, Belgium',
'education.highschool.period': '2016 - 2022',
'education.highschool.status': 'Graduated',
'education.highschool.description': 'Math-Science Section (Math 8h - Science 7h)',
'education.highschool.highlights.0': 'Advanced Mathematics',
'education.highschool.highlights.1': 'Sciences (Physics, Chemistry, Biology)',
'education.highschool.highlights.2': 'Languages',
// Travel Mate Page
'travelmate.page.mainTitle': 'Travel Mate 🌍',
'travelmate.page.subtitle': 'The essential travel companion',
'travelmate.page.intro': 'Rediscover group travel with Travel Mate. More than just an app, it\'s your essential co-pilot for friction-free adventures. Forget complex spreadsheets and debates about "who owes what". Focus on what matters: making unforgettable memories.',
'travelmate.highlights.title': '✨ Key Highlights',
'travelmate.highlight.1.title': 'Simplified Group Management',
'travelmate.highlight.1.desc': 'Create your trip and invite companions with a single click via a unique link. Organization starts instantly.',
'travelmate.highlight.2.title': 'Mastered Expenses (Split)',
'travelmate.highlight.2.desc': 'Track expenses in real-time and let the app balance accounts automatically. No more complicated math!',
'travelmate.highlight.3.title': 'Collaborative Planning',
'travelmate.highlight.3.desc': 'A shared agenda and interactive map so everyone can suggest and visualize group activities.',
'travelmate.highlight.4.title': 'Seamless Communication',
'travelmate.highlight.4.desc': 'An integrated chat to centralize discussions and keep everyone on the same page.',
'travelmate.page.conclusion': 'Ready to go? With Travel Mate, the adventure begins with the planning. Travel with peace of mind, we\'ll handle the rest.',
'travelmate.tech.title': '🛠️ Technologies Used',
'travelmate.tech.frontend': 'Mobile Frontend (Flutter)',
'travelmate.tech.frontend.1': 'BLoC Architecture for robust state management',
'travelmate.tech.frontend.2': 'Fluid and responsive User Interface',
'travelmate.tech.frontend.3': 'Native Integration (Camera, GPS, Notifications)',
'travelmate.tech.backend': 'Backend (Firebase)',
'travelmate.tech.backend.1': 'Authentication (Google, Apple, Email)',
'travelmate.tech.backend.2': 'Firestore (Real-time Database)',
'travelmate.tech.backend.3': 'Cloud Functions (Server Logic)',
'travelmate.tech.backend.4': 'Firebase Cloud Messaging - Notifications push',
'travelmate.tech.backend.5': 'Storage (Media)',
'travelmate.tech.api': 'APIs & Tools',
'travelmate.tech.api.1': 'Google Maps API / Mapbox',
'travelmate.tech.api.2': 'Stripe (tbc) for payments',
'travelmate.tech.api.3': 'CI/CD with Gitea Actions',
'travelmate.viewCode': 'View Code',
'travelmate.policies.link': 'View Privacy Policy',
// Policies Page
'policies.back': 'Back',
'policies.title': 'Privacy Policy',
'policies.intro': 'Your privacy is important to us. Here is how we protect your data.',
'policies.section.1.title': 'Information Collection',
'policies.section.1.content': 'We collect information that you provide directly to us, such as your name, email address, and travel preferences.',
'policies.permissions.title': 'Device Permissions',
'policies.data.camera': 'Camera & Gallery',
'policies.data.camera.desc': 'To add photos to memories or scan receipts.',
'policies.data.gps': 'Location (GPS)',
'policies.data.gps.desc': 'For the interactive map and nearby activity suggestions.',
'policies.data.notif': 'Notifications',
'policies.data.notif.desc': 'To alert you of new messages, expenses, or schedule changes.',
'policies.section.2.title': 'Data Usage',
'policies.section.2.content': 'Your data is used to improve your user experience and offer you personalized recommendations.',
'policies.section.3.title': 'Data Protection',
'policies.section.3.content': 'We implement appropriate security measures to protect your personal information.',
'policies.section.4.title': 'Data Sharing',
'policies.section.4.content': 'We do not share your personal information with third parties without your explicit consent.',
'policies.section.5.title': 'Your Rights',
'policies.section.5.content': 'You have the right to access, correct, or delete your personal data at any time. Please contact us for any request.',
'policies.section.6.title': 'Contact Us',
'policies.section.6.content': 'For any questions regarding this privacy policy, please contact us at support@travelmate.com',
'policies.googleBtn': 'Google Privacy Policy',
// Contact
'contact.title': 'Contact me',
'contact.subtitle': 'A question, a project or just want to chat? Feel free to contact me!',
'contact.form.name': 'Full name',
'contact.form.email': 'Email',
'contact.form.subject': 'Subject',
'contact.form.message': 'Message',
'contact.form.send': 'Send message',
'contact.form.sending': 'Sending...',
'contact.success': 'Message sent successfully! I will reply soon.',
'contact.stayInTouch': 'Let\'s stay in touch',
'contact.intro': 'I am always interested in new projects, collaborations or simply discussions about technology. Feel free to contact me!',
'contact.findMeOn': 'Find me also on:',
'contact.sendMessage': 'Send me a message',
}
};
// Création du contexte
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
// Provider
interface LanguageProviderProps {
children: ReactNode;
}
import { useNavigate, useParams, useLocation } from 'react-router-dom';
export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children }) => {
const { lang } = useParams<{ lang: string }>();
const navigate = useNavigate();
const location = useLocation();
// Validate language, default to 'fr' if undefined or invalid (though App.tsx handles defaults)
const currentLanguage: Language = (lang === 'en' || lang === 'fr') ? lang : 'fr';
const setLanguage = (newLang: Language) => {
if (newLang === currentLanguage) return;
// Replace the language segment in the URL
// Assumes the first segment is the language
const currentPath = location.pathname;
const segments = currentPath.split('/');
// segments[0] is empty string (before first slash)
// segments[1] is the language
if (segments[1] === 'fr' || segments[1] === 'en') {
segments[1] = newLang;
} else {
// Should not happen if routing is correct, but safe fallback
segments.splice(1, 0, newLang);
}
const newPath = segments.join('/');
navigate(newPath + location.search + location.hash);
};
// Update HTML lang attribute
React.useEffect(() => {
document.documentElement.lang = currentLanguage;
}, [currentLanguage]);
// Fonction de traduction
const t = (key: string): string => {
return translations[currentLanguage][key as keyof typeof translations['fr']] || key;
};
return (
<LanguageContext.Provider value={{ language: currentLanguage, setLanguage, t }}>
{children}
</LanguageContext.Provider>
);
};
// Hook personnalisé
export const useLanguage = (): LanguageContextType => {
const context = useContext(LanguageContext);
if (!context) {
throw new Error('useLanguage must be used within a LanguageProvider');
}
return context;
};

10
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './styles/index.scss'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

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

@@ -0,0 +1,137 @@
# 🎨 Structure SCSS du Portfolio
Ce projet utilise une architecture SCSS moderne et organisée pour maintenir un code CSS propre et facilement maintenable.
## 📁 Structure des fichiers
```
src/styles/
├── _variables.scss # Variables globales (couleurs, espacements, breakpoints)
├── _mixins.scss # Mixins réutilisables
├── _global.scss # Styles globaux et reset CSS
├── index.scss # Point d'entrée principal
├── main.scss # Import des composants
└── components/ # Styles spécifiques aux composants
├── _header.scss
├── _hero.scss
├── _about.scss
├── _skills.scss
├── _projects.scss
├── _education.scss
└── _contact.scss
```
## 🎯 Architecture
### Variables (`_variables.scss`)
- **Couleurs** : Système de couleurs pour les thèmes clair/sombre
- **Espacements** : Variables responsive pour padding/margin
- **Breakpoints** : Points de rupture pour le responsive design
- **Typography** : Tailles de police fluides
- **Z-index** : Système organisé pour la gestion des couches
### Mixins (`_mixins.scss`)
- **Responsive** : `@include respond-to(md)`, `@include respond-above(lg)`
- **Composants** : `@include card()`, `@include button-primary()`
- **Animations** : `@include transition()`, `@include fade-in-animation()`
- **Layout** : `@include flex-center()`, `@include grid-responsive()`
- **Accessibilité** : `@include focus-style()`, `@include visually-hidden()`
### Styles globaux (`_global.scss`)
- Reset CSS moderne
- Styles de base pour les éléments HTML
- Classes utilitaires
- Animations keyframes
- Gestion des thèmes (clair/sombre)
## 🛠️ Utilisation
### Responsive Design
```scss
.component {
padding: 20px;
@include respond-to(md) {
padding: 10px;
}
@include respond-above(xl) {
padding: 40px;
}
}
```
### Composants réutilisables
```scss
.card {
@include card(32px, $border-radius-large, medium);
}
.button {
@include button-primary();
}
```
### Système de couleurs
```scss
.element {
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
```
### Grilles responsive
```scss
.grid {
@include grid-responsive(300px, 24px);
}
```
## 🎨 Thèmes
Le système de thèmes utilise les variables CSS pour permettre le basculement dynamique :
```scss
// Variables définies dans _variables.scss
$colors: (
light: (
bg-primary: #ffffff,
text-primary: #1e293b,
// ...
),
dark: (
bg-primary: #0f172a,
text-primary: #f1f5f9,
// ...
)
);
```
## 🚀 Avantages de cette architecture
1. **Modularité** : Chaque composant a ses styles séparés
2. **Réutilisabilité** : Mixins pour éviter la duplication
3. **Maintenabilité** : Variables centralisées
4. **Performance** : Compilation optimisée avec Sass
5. **Responsive** : Système de breakpoints cohérent
6. **Accessibilité** : Mixins dédiés pour l'a11y
7. **Thèmes** : Support natif clair/sombre
## 📝 Bonnes pratiques
- Utiliser les variables pour toutes les valeurs récurrentes
- Préférer les mixins aux classes utilitaires
- Organiser les styles par composant
- Utiliser les imports avec `@use` plutôt que `@import`
- Suivre la convention de nommage BEM
- Documenter les mixins complexes
## 🔧 Compilation
Les fichiers SCSS sont automatiquement compilés par Vite lors du développement et de la construction.
```bash
npm run dev # Compilation en mode développement
npm run build # Compilation pour la production
```

View File

@@ -0,0 +1,406 @@
// ========================
// GLOBAL STYLES SCSS
// ========================
@use 'variables' as *;
@use 'mixins' as *;
// Importation des polices Google Fonts
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
// Variables CSS pour les thèmes
:root {
// Couleurs principales
--primary-color: #{$primary-color};
--primary-hover: #{$primary-hover};
--secondary-color: #{$secondary-color};
--accent-color: #{$accent-color};
// Variables des thèmes - Light par défaut
@each $property, $value in map-get($colors, light) {
--#{$property}: #{$value};
}
// Transitions
--transition-fast: #{$transition-fast};
--transition-normal: #{$transition-normal};
--transition-slow: #{$transition-slow};
// Espacements
--section-padding: #{$section-padding};
--container-padding: #{$container-padding};
--border-radius: #{$border-radius};
--border-radius-large: #{$border-radius-large};
}
// Thème sombre
[data-theme="dark"] {
@each $property, $value in map-get($colors, dark) {
--#{$property}: #{$value};
}
}
// Reset et base
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
scroll-padding-top: 80px;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
@include custom-scrollbar();
}
body {
font-family: $font-family;
font-size: map-get($font-sizes, base);
line-height: 1.6;
color: var(--text-primary);
background-color: var(--bg-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
overflow-x: hidden;
@include transition(color background-color);
}
.app {
min-height: 100vh;
overflow-x: hidden;
}
// Amélioration des médias
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
height: auto;
}
img {
border-style: none;
}
// Amélioration des formulaires
input,
button,
textarea,
select {
font: inherit;
}
button {
border: none;
background: none;
cursor: pointer;
}
// Amélioration des liens
a {
color: inherit;
text-decoration: none;
}
// Amélioration des listes
ul,
ol {
list-style: none;
}
// Amélioration des titres
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 600;
line-height: 1.2;
color: var(--text-primary);
}
// Amélioration des paragraphes
p {
color: var(--text-secondary);
max-width: 65ch;
}
// Classes utilitaires
.container {
max-width: map-get($breakpoints, xl);
margin: 0 auto;
padding: var(--container-padding);
@include respond-above(xxl) {
max-width: 1400px;
}
}
.section-header {
text-align: center;
margin-bottom: 60px;
.section-title {
font-size: map-get($font-sizes, 3xl);
font-weight: 700;
margin-bottom: 16px;
@include gradient-text();
@include respond-to(sm) {
font-size: map-get($font-sizes, 2xl);
}
}
.section-subtitle {
font-size: map-get($font-sizes, lg);
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto;
}
}
// Classes d'accessibilité
.visually-hidden,
.sr-only {
@include visually-hidden();
}
.not-sr-only {
position: static;
width: auto;
height: auto;
padding: 0;
margin: 0;
overflow: visible;
clip: auto;
white-space: normal;
}
// Focus pour l'accessibilité
button:focus-visible,
a:focus-visible,
input:focus-visible,
textarea:focus-visible {
@include focus-style();
}
// Amélioration de la sélection de texte
::selection {
background-color: $primary-color;
color: white;
text-shadow: none;
}
::-moz-selection {
background-color: $primary-color;
color: white;
text-shadow: none;
}
// Classes d'animation
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeOut {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-20px);
}
}
@keyframes slideInFromLeft {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
@keyframes slideInFromRight {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes bounce {
0%, 20%, 53%, 80%, 100% {
transform: translate3d(0, 0, 0);
}
40%, 43% {
transform: translate3d(0, -30px, 0);
}
70% {
transform: translate3d(0, -15px, 0);
}
90% {
transform: translate3d(0, -4px, 0);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
// Classes d'animation
.animate-fade-in {
animation: fadeIn 0.6s ease-out;
}
.animate-fade-out {
animation: fadeOut 0.6s ease-out;
}
.animate-slide-in-left {
animation: slideInFromLeft 0.6s ease-out;
}
.animate-slide-in-right {
animation: slideInFromRight 0.6s ease-out;
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.animate-bounce {
animation: bounce 1s infinite;
}
.loading {
pointer-events: none;
opacity: 0.6;
}
.gpu-accelerated {
transform: translateZ(0);
-webkit-transform: translateZ(0);
will-change: transform;
}
// Amélioration pour les utilisateurs préférant les animations réduites
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
// Mode haut contraste
@media (prefers-contrast: high) {
:root {
--text-primary: #000000;
--text-secondary: #333333;
--bg-primary: #ffffff;
--bg-secondary: #f0f0f0;
--border-color: #666666;
}
[data-theme="dark"] {
--text-primary: #ffffff;
--text-secondary: #cccccc;
--bg-primary: #000000;
--bg-secondary: #1a1a1a;
--border-color: #999999;
}
}
// Support pour les très petits écrans
@include respond-to(xs) {
:root {
--container-padding: 0 12px;
}
}
// Amélioration de l'impression
@media print {
* {
background: transparent !important;
color: black !important;
box-shadow: none !important;
text-shadow: none !important;
}
a,
a:visited {
text-decoration: underline;
}
a[href]:after {
content: " (" attr(href) ")";
}
abbr[title]:after {
content: " (" attr(title) ")";
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}

View File

@@ -0,0 +1,233 @@
// ========================
// MIXINS SCSS
// ========================
@use 'variables' as *;
// Mixin pour les breakpoints responsive
@mixin respond-to($breakpoint) {
@if map-has-key($breakpoints, $breakpoint) {
@media (max-width: map-get($breakpoints, $breakpoint)) {
@content;
}
} @else {
@warn "Breakpoint #{$breakpoint} not found in $breakpoints.";
}
}
@mixin respond-above($breakpoint) {
@if map-has-key($breakpoints, $breakpoint) {
@media (min-width: map-get($breakpoints, $breakpoint)) {
@content;
}
} @else {
@warn "Breakpoint #{$breakpoint} not found in $breakpoints.";
}
}
// Mixin pour les transitions
@mixin transition($properties: all, $duration: $transition-normal, $timing: ease) {
transition: $properties $duration $timing;
}
// Mixin pour les ombres
@mixin box-shadow($level: medium) {
@if $level == light {
box-shadow: var(--shadow-light);
} @else if $level == medium {
box-shadow: var(--shadow-medium);
} @else if $level == large {
box-shadow: var(--shadow-large);
}
}
// Mixin pour les cartes
@mixin card($padding: 32px, $radius: $border-radius-large, $shadow: light) {
background: var(--bg-primary);
padding: $padding;
border-radius: $radius;
@include box-shadow($shadow);
@include transition(transform, $transition-normal);
&:hover {
transform: translateY(-4px);
@include box-shadow(medium);
}
}
// Mixin pour les boutons
@mixin button-base {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 14px 28px;
border: none;
border-radius: $border-radius;
font-weight: 600;
text-decoration: none;
cursor: pointer;
@include transition();
font-size: map-get($font-sizes, base);
}
@mixin button-primary {
@include button-base;
background: $primary-color;
color: white;
@include box-shadow(medium);
&:hover {
background: $primary-hover;
transform: translateY(-2px);
@include box-shadow(large);
}
}
@mixin button-secondary {
@include button-base;
background: transparent;
color: $primary-color;
border: 2px solid $primary-color;
&:hover {
background: $primary-color;
color: white;
transform: translateY(-2px);
}
}
// Mixin pour les grilles responsive
@mixin grid-responsive($min-width: 300px, $gap: 32px) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax($min-width, 1fr));
gap: $gap;
}
// Mixin pour le texte dégradé
@mixin gradient-text($color1: $primary-color, $color2: $accent-color, $direction: 135deg) {
background: linear-gradient($direction, $color1, $color2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
// Mixin pour centrer flex
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
// Mixin pour masquer visuellement (accessibilité)
@mixin visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
// Mixin pour les focus accessibles
@mixin focus-style {
outline: 2px solid $primary-color;
outline-offset: 2px;
border-radius: 4px;
}
// Mixin pour l'animation d'apparition
@mixin fade-in-animation($duration: 0.6s, $delay: 0s) {
opacity: 0;
animation: fadeIn $duration ease-out $delay forwards;
}
// Mixin pour les barres de progression
@mixin progress-bar($height: 8px, $bg-color: var(--bg-secondary)) {
height: $height;
background: $bg-color;
border-radius: $height / 2;
overflow: hidden;
.progress-fill {
height: 100%;
border-radius: $height / 2;
@include transition(width, $transition-slow);
}
}
// Mixin pour les icônes avec background
@mixin icon-container($size: 60px, $bg-color: $primary-color, $radius: $border-radius) {
width: $size;
height: $size;
border-radius: $radius;
@include flex-center;
background-color: #{$bg-color}20;
color: $bg-color;
}
// Mixin pour les tags/badges
@mixin tag($padding: 6px 12px, $bg-color: var(--bg-secondary), $text-color: var(--text-secondary)) {
padding: $padding;
background: $bg-color;
border-radius: 20px;
font-size: map-get($font-sizes, xs);
font-weight: 500;
color: $text-color;
@include transition();
}
// Mixin pour les liens avec effet hover
@mixin link-hover($color: $primary-color) {
position: relative;
@include transition(color);
&:hover {
color: $color;
}
&::after {
content: '';
position: absolute;
width: 0;
height: 2px;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
background: $color;
@include transition(width);
}
&:hover::after {
width: 100%;
}
}
// Mixin pour les scrollbars personnalisées
@mixin custom-scrollbar($width: 8px, $track-color: var(--bg-secondary), $thumb-color: var(--text-muted)) {
&::-webkit-scrollbar {
width: $width;
height: $width;
}
&::-webkit-scrollbar-track {
background: $track-color;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: $thumb-color;
border-radius: 4px;
@include transition(background-color);
&:hover {
background: var(--text-secondary);
}
}
// Support Firefox
scrollbar-width: thin;
scrollbar-color: $thumb-color $track-color;
}

View File

@@ -0,0 +1,95 @@
// ========================
// VARIABLES SCSS
// ========================
// Couleurs principales
$primary-color: #3b82f6;
$primary-hover: #2563eb;
$secondary-color: #64748b;
$accent-color: #10b981;
// Système de couleurs pour les thèmes
$colors: (
light: (
bg-primary: #ffffff,
bg-secondary: #f8fafc,
bg-tertiary: #f1f5f9,
text-primary: #1e293b,
text-secondary: #64748b,
text-muted: #94a3b8,
border-color: #e2e8f0,
shadow-light: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
shadow-medium: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
shadow-large: 0 10px 15px -3px rgba(0, 0, 0, 0.1)
),
dark: (
bg-primary: #0f172a,
bg-secondary: #1e293b,
bg-tertiary: #334155,
text-primary: #f1f5f9,
text-secondary: #cbd5e1,
text-muted: #64748b,
border-color: #334155,
shadow-light: 0 1px 3px 0 rgba(0, 0, 0, 0.3),
shadow-medium: 0 4px 6px -1px rgba(0, 0, 0, 0.3),
shadow-large: 0 10px 15px -3px rgba(0, 0, 0, 0.3)
)
);
// Transitions
$transition-fast: 0.2s ease;
$transition-normal: 0.3s ease;
$transition-slow: 0.5s ease;
// Espacements
$section-padding: 80px 0;
$container-padding: 0 20px;
// Border radius
$border-radius: 12px;
$border-radius-large: 20px;
// Breakpoints
$breakpoints: (
xs: 320px,
sm: 480px,
md: 768px,
lg: 1024px,
xl: 1200px,
xxl: 1440px
);
// Z-index
$z-index: (
dropdown: 1000,
sticky: 1020,
fixed: 1030,
modal-backdrop: 1040,
modal: 1050,
popover: 1060,
tooltip: 1070
);
// Polices
$font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
// Tailles de police responsive
$font-sizes: (
xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem),
sm: clamp(0.875rem, 0.8rem + 0.375vw, 1rem),
base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem),
lg: clamp(1.125rem, 1.05rem + 0.375vw, 1.25rem),
xl: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem),
2xl: clamp(1.5rem, 1.35rem + 0.75vw, 2rem),
3xl: clamp(2rem, 1.75rem + 1.25vw, 2.5rem),
4xl: clamp(2.5rem, 2rem + 2.5vw, 4rem)
);
// Espacements responsive
$spaces: (
xs: clamp(0.25rem, 0.2rem + 0.25vw, 0.5rem),
sm: clamp(0.5rem, 0.4rem + 0.5vw, 1rem),
md: clamp(1rem, 0.8rem + 1vw, 2rem),
lg: clamp(2rem, 1.5rem + 2.5vw, 4rem),
xl: clamp(4rem, 3rem + 5vw, 8rem)
);

View File

@@ -0,0 +1,112 @@
// ========================
// ABOUT SECTION SCSS
// ========================
@use '../variables' as *;
@use '../mixins' as *;
.about {
padding: var(--section-padding);
background: var(--bg-secondary);
&-content {
display: grid;
grid-template-columns: 1fr 300px;
gap: 60px;
margin-bottom: 60px;
@include respond-to(md) {
grid-template-columns: 1fr;
gap: 40px;
}
}
&-text {
display: flex;
flex-direction: column;
gap: 32px;
}
&-card {
@include card(32px, $border-radius-large, light);
h3 {
font-size: map-get($font-sizes, xl);
font-weight: 700;
margin-bottom: 16px;
color: $primary-color;
}
p {
color: var(--text-secondary);
line-height: 1.7;
strong {
color: $primary-color;
font-weight: 600;
}
}
}
&-stats {
display: flex;
flex-direction: column;
gap: 20px;
@include respond-to(md) {
flex-direction: row;
flex-wrap: wrap;
}
.stat-card {
@include card(24px, $border-radius, light);
text-align: center;
flex: 1;
min-width: 140px;
.stat-icon {
color: $primary-color;
margin-bottom: 12px;
}
.stat-value {
font-size: map-get($font-sizes, 2xl);
font-weight: 800;
color: var(--text-primary);
margin-bottom: 4px;
}
.stat-label {
font-size: map-get($font-sizes, sm);
color: var(--text-secondary);
font-weight: 500;
}
}
}
&-highlight {
background: linear-gradient(135deg, $primary-color, $accent-color);
padding: 40px;
border-radius: $border-radius-large;
text-align: center;
color: white;
h3 {
font-size: map-get($font-sizes, xl);
margin-bottom: 16px;
}
p {
font-style: italic;
margin-bottom: 16px;
line-height: 1.7;
max-width: none;
color: rgba(255, 255, 255, 0.9);
}
cite {
font-weight: 600;
color: rgba(255, 255, 255, 0.8);
}
}
}

View File

@@ -0,0 +1,244 @@
// ========================
// CONTACT SECTION SCSS
// ========================
@use '../variables' as *;
@use '../mixins' as *;
.contact {
padding: var(--section-padding);
background: var(--bg-secondary);
&-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
@include respond-to(md) {
grid-template-columns: 1fr;
gap: 40px;
}
}
&-intro {
h3 {
display: flex;
align-items: center;
gap: 12px;
font-size: map-get($font-sizes, xl);
font-weight: 700;
margin-bottom: 16px;
color: var(--text-primary);
}
p {
color: var(--text-secondary);
line-height: 1.7;
margin-bottom: 32px;
}
}
&-methods {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 40px;
.contact-method {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: var(--bg-primary);
border-radius: $border-radius;
@include box-shadow(light);
@include transition();
&:hover {
transform: translateX(8px);
@include box-shadow(medium);
}
.contact-icon {
@include icon-container(50px, $primary-color, $border-radius);
flex-shrink: 0;
}
.contact-details {
h4 {
font-weight: 600;
margin-bottom: 4px;
color: var(--text-primary);
}
span {
color: var(--text-secondary);
}
}
}
}
.social-links {
h4 {
margin-bottom: 16px;
color: var(--text-primary);
}
.social-grid {
display: flex;
gap: 16px;
@include respond-to(md) {
flex-direction: column;
}
.social-link {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--bg-primary);
border-radius: $border-radius;
@include box-shadow(light);
@include transition();
&:hover {
@include box-shadow(medium);
}
.social-icon {
@include icon-container(32px, $primary-color, 6px);
}
span {
font-weight: 500;
color: var(--text-secondary);
}
}
}
}
&-form-container {
h3 {
font-size: map-get($font-sizes, xl);
font-weight: 700;
margin-bottom: 24px;
color: var(--text-primary);
}
.success-message {
display: flex;
align-items: center;
gap: 12px;
background: $accent-color;
color: white;
padding: 16px;
border-radius: $border-radius;
margin-bottom: 24px;
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 {
display: flex;
flex-direction: column;
gap: 20px;
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
@include respond-to(md) {
grid-template-columns: 1fr;
}
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
label {
font-weight: 500;
color: var(--text-primary);
}
input,
textarea {
padding: 12px 16px;
border: 2px solid var(--border-color);
border-radius: $border-radius;
background: var(--bg-primary);
color: var(--text-primary);
font-family: inherit;
@include transition(border-color);
&:focus {
outline: none;
border-color: $primary-color;
}
&::placeholder {
color: var(--text-muted);
}
}
textarea {
resize: vertical;
min-height: 120px;
}
}
.submit-btn {
@include button-primary();
justify-content: center;
gap: 12px;
padding: 16px 32px;
font-size: map-get($font-sizes, base);
&:disabled {
opacity: 0.7;
cursor: not-allowed;
&:hover {
transform: none;
}
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
}
}
&-footer {
text-align: center;
margin-top: 60px;
padding-top: 40px;
border-top: 1px solid var(--border-color);
p {
color: var(--text-muted);
margin: 0 auto;
}
}
}

View File

@@ -0,0 +1,292 @@
// ========================
// EDUCATION SECTION SCSS
// ========================
@use '../variables' as *;
@use '../mixins' as *;
.education {
padding: var(--section-padding);
&-content {
display: flex;
flex-direction: column;
gap: 60px;
}
&-card {
display: flex;
gap: 32px;
@include card(32px, $border-radius-large, light);
@include respond-to(md) {
flex-direction: column;
text-align: center;
}
&.main-education {
.education-timeline {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
@include respond-to(md) {
flex-direction: row;
justify-content: center;
margin-bottom: 20px;
}
.timeline-dot {
width: 60px;
height: 60px;
border-radius: 50%;
@include flex-center();
color: white;
margin-bottom: 16px;
@include respond-to(md) {
margin-bottom: 0;
margin-right: 16px;
}
}
.timeline-line {
width: 2px;
height: 100px;
background: var(--border-color);
border-radius: 1px;
@include respond-to(md) {
display: none;
}
}
}
.education-content-card {
flex: 1;
}
.education-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
@include respond-to(md) {
flex-direction: column;
align-items: center;
text-align: center;
}
.education-title {
h3 {
font-size: map-get($font-sizes, xl);
font-weight: 700;
margin-bottom: 12px;
color: var(--text-primary);
}
.education-meta {
display: flex;
flex-direction: column;
gap: 8px;
span {
display: flex;
align-items: center;
gap: 8px;
font-size: map-get($font-sizes, sm);
color: var(--text-secondary);
@include respond-to(md) {
justify-content: center;
}
}
.school-name {
font-weight: 600;
}
}
}
.education-status {
text-align: right;
@include respond-to(md) {
text-align: center;
margin-top: 16px;
}
.status-badge {
@include tag(6px 12px);
font-size: map-get($font-sizes, xs);
font-weight: 600;
border-radius: 20px;
}
.current-year {
display: block;
font-size: map-get($font-sizes, sm);
color: var(--text-muted);
margin-top: 8px;
}
}
}
.education-description {
color: var(--text-secondary);
line-height: 1.7;
margin-bottom: 24px;
}
.education-highlights {
h4 {
display: flex;
align-items: center;
gap: 8px;
font-size: map-get($font-sizes, base);
font-weight: 600;
margin-bottom: 16px;
color: var(--text-primary);
@include respond-to(md) {
justify-content: center;
}
}
.highlights-grid {
@include grid-responsive(200px, 12px);
.highlight-tag {
@include tag(8px 12px, var(--bg-secondary), var(--text-secondary));
font-size: map-get($font-sizes, sm);
text-align: center;
border-radius: $border-radius;
&:hover {
background: $primary-color;
color: white;
transform: scale(1.05);
}
}
}
}
}
}
.certifications {
text-align: center;
&-title {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-size: map-get($font-sizes, xl);
font-weight: 700;
margin-bottom: 32px;
color: var(--text-primary);
}
&-grid {
@include grid-responsive(300px, 24px);
margin-bottom: 60px;
@include respond-to(md) {
grid-template-columns: 1fr;
}
.certification-card {
@include card(24px, $border-radius, light);
.cert-header {
h4 {
font-size: map-get($font-sizes, lg);
font-weight: 600;
margin-bottom: 8px;
color: var(--text-primary);
}
.cert-provider {
display: block;
font-size: map-get($font-sizes, sm);
color: var(--text-secondary);
margin-bottom: 4px;
}
.cert-date {
display: block;
font-size: map-get($font-sizes, xs);
color: var(--text-muted);
margin-bottom: 16px;
}
}
.cert-skills {
display: flex;
flex-wrap: wrap;
gap: 8px;
.cert-skill-tag {
@include tag(4px 8px);
font-size: map-get($font-sizes, xs);
font-weight: 500;
border-radius: 12px;
}
}
}
}
}
.learning-goals {
h3 {
font-size: map-get($font-sizes, xl);
font-weight: 700;
margin-bottom: 32px;
color: var(--text-primary);
text-align: center;
}
.goals-grid {
@include grid-responsive(300px, 20px);
@include respond-to(md) {
grid-template-columns: 1fr;
}
.goal-item {
@include card(20px, $border-radius, light);
.goal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.goal-text {
font-weight: 500;
color: var(--text-primary);
}
.goal-percentage {
font-size: map-get($font-sizes, sm);
color: $primary-color;
font-weight: 600;
}
}
.goal-progress {
@include progress-bar(6px);
.goal-progress-bar {
height: 100%;
background: $primary-color;
border-radius: 3px;
@include transition(width, $transition-slow);
}
}
}
}
}
}

View File

@@ -0,0 +1,156 @@
// ========================
// HEADER / NAVIGATION SCSS
// ========================
@use '../variables' as *;
@use '../mixins' as *;
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: map-get($z-index, fixed);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border-color);
@include transition();
[data-theme="dark"] & {
background: rgba(15, 23, 42, 0.95);
}
.nav {
@include flex-center();
justify-content: space-between;
padding: 16px 20px;
max-width: map-get($breakpoints, xl);
margin: 0 auto;
}
.nav-brand {
a {
font-size: map-get($font-sizes, xl);
font-weight: 700;
color: var(--text-primary);
@include gradient-text();
@include transition();
&:hover {
transform: scale(1.05);
}
}
}
.nav-menu {
display: flex;
gap: 32px;
a {
color: var(--text-secondary);
font-weight: 500;
@include link-hover();
}
&.mobile-menu {
display: none;
position: fixed;
top: 100%;
right: 0;
width: 100%;
height: calc(100vh - 80px);
background: var(--bg-primary);
flex-direction: column;
padding: 40px 20px;
gap: 24px;
border-top: 1px solid var(--border-color);
@include box-shadow(large);
@include transition(transform opacity);
&.open {
top: 80px;
transform: translateX(0);
opacity: 1;
}
&:not(.open) {
transform: translateX(100%);
opacity: 0;
}
a {
font-size: map-get($font-sizes, lg);
padding: 16px 0;
border-bottom: 1px solid var(--border-color);
}
}
}
.nav-controls {
display: flex;
align-items: center;
gap: 16px;
}
.theme-toggle,
.menu-toggle,
.language-toggle {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 8px;
border-radius: 8px;
@include transition();
&:hover {
color: $primary-color;
background: var(--bg-secondary);
transform: scale(1.1);
}
&:focus-visible {
@include focus-style();
}
}
.language-toggle {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
.language-text {
font-size: map-get($font-sizes, sm);
font-weight: 600;
line-height: 1;
}
}
.menu-toggle {
display: none;
@include respond-to(md) {
display: block;
}
}
.desktop-menu {
@include respond-to(md) {
display: none;
}
}
}
// Responsive
@include respond-to(md) {
.header {
.nav {
padding: 12px 16px;
}
.mobile-menu {
display: flex !important;
}
}
}

View File

@@ -0,0 +1,209 @@
// ========================
// HERO SECTION SCSS
// ========================
@use '../variables' as *;
@use '../mixins' as *;
.hero {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
padding: var(--section-padding);
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
overflow: hidden;
&-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
align-items: center;
max-width: map-get($breakpoints, xl);
margin: 0 auto;
padding: var(--container-padding);
@include respond-to(md) {
grid-template-columns: 1fr;
text-align: center;
gap: 40px;
}
}
&-text {
z-index: 2;
}
&-title {
font-size: map-get($font-sizes, 4xl);
font-weight: 800;
margin: 20px;
@include gradient-text();
@include respond-to(sm) {
font-size: map-get($font-sizes, 3xl);
}
}
&-subtitle {
font-size: map-get($font-sizes, xl);
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 24px;
@include respond-to(sm) {
font-size: map-get($font-sizes, lg);
}
}
&-description {
font-size: map-get($font-sizes, lg);
color: var(--text-secondary);
margin-bottom: 40px;
line-height: 1.7;
}
&-buttons {
display: flex;
gap: 20px;
margin-bottom: 40px;
@include respond-to(md) {
flex-direction: column;
align-items: center;
}
@include respond-to(sm) {
gap: 12px;
}
.btn {
@include button-base();
@include respond-to(sm) {
padding: 12px 20px;
font-size: 0.9rem;
}
&-primary {
@include button-primary();
}
&-secondary {
@include button-secondary();
}
}
}
&-social {
display: flex;
gap: 16px;
@include respond-to(md) {
justify-content: center;
}
.social-link {
@include flex-center();
width: 48px;
height: 48px;
background: var(--bg-secondary);
color: var(--text-secondary);
border-radius: 50%;
@include box-shadow(light);
@include transition();
&:hover {
color: $primary-color;
background: white;
transform: translateY(-2px);
@include box-shadow(medium);
}
}
}
&-image {
@include flex-center();
}
&-avatar {
position: relative;
&:hover {
transform: scale(1.05) rotate(5deg);
@include transition(transform, 0.5s, spring);
}
}
.avatar-image {
width: 300px;
height: 300px;
border-radius: 50%;
object-fit: cover;
object-position: center;
@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) {
width: 250px;
height: 250px;
border-width: 3px;
}
@include respond-to(sm) {
width: 200px;
height: 200px;
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%);
}
}
}
}
&-particles {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 1;
.particle {
position: absolute;
width: 4px;
height: 4px;
background: $primary-color;
border-radius: 50%;
opacity: 0.3;
}
}
}

View File

@@ -0,0 +1,154 @@
// ========================
// PROJECTS SECTION SCSS
// ========================
@use '../variables' as *;
@use '../mixins' as *;
.projects {
padding: var(--section-padding);
background: var(--bg-secondary);
&-grid {
@include grid-responsive(400px, 40px);
margin-bottom: 60px;
@include respond-to(md) {
grid-template-columns: 1fr;
}
}
.project-card {
background: var(--bg-primary);
border-radius: $border-radius-large;
overflow: hidden;
@include box-shadow(light);
@include transition();
&:hover {
transform: translateY(-10px) scale(1.02);
@include box-shadow(large);
}
.project-header {
padding: 24px 24px 0;
display: flex;
justify-content: space-between;
align-items: flex-start;
.project-icon {
@include icon-container(60px, $primary-color, $border-radius);
}
.project-status {
display: flex;
align-items: center;
.status-badge {
@include tag(6px 12px);
font-size: map-get($font-sizes, xs);
font-weight: 600;
border-radius: 20px;
}
}
}
.project-content {
padding: 24px;
.project-title {
font-size: map-get($font-sizes, xl);
font-weight: 700;
margin-bottom: 12px;
color: var(--text-primary);
}
.project-description {
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 20px;
}
.project-technologies {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
.tech-tag {
@include tag(6px 12px, var(--bg-secondary), var(--text-secondary));
font-size: map-get($font-sizes, xs);
font-weight: 500;
border-radius: 20px;
}
}
.project-features {
h4 {
font-size: map-get($font-sizes, base);
font-weight: 600;
margin-bottom: 12px;
color: var(--text-primary);
}
ul {
display: flex;
flex-direction: column;
gap: 8px;
li {
color: var(--text-secondary);
position: relative;
padding-left: 20px;
&::before {
content: '';
color: $primary-color;
position: absolute;
left: 0;
top: 0;
}
}
}
}
}
.project-footer {
padding: 0 24px 24px;
.project-links {
display: flex;
gap: 12px;
.project-link {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: $border-radius;
font-weight: 500;
border: 2px solid var(--border-color);
color: var(--text-secondary);
@include transition();
&:hover {
border-color: $primary-color;
color: $primary-color;
transform: translateY(-2px);
}
&.primary {
background: $primary-color;
border-color: $primary-color;
color: white;
&:hover {
background: $primary-hover;
border-color: $primary-hover;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,109 @@
// ========================
// SKILLS SECTION SCSS
// ========================
@use '../variables' as *;
@use '../mixins' as *;
.skills {
padding: var(--section-padding);
&-grid {
@include grid-responsive(300px, 32px);
margin-bottom: 60px;
}
.skill-category {
@include card(32px, $border-radius-large, light);
.category-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
.category-icon {
@include icon-container(60px, $primary-color, $border-radius);
}
.category-title {
font-size: map-get($font-sizes, lg);
font-weight: 700;
color: var(--text-primary);
}
}
.skills-list {
display: flex;
flex-direction: column;
gap: 20px;
.skill-item {
display: flex;
flex-direction: column;
gap: 8px;
.skill-header {
display: flex;
justify-content: space-between;
align-items: center;
.skill-name {
font-weight: 600;
color: var(--text-primary);
}
.skill-percentage {
font-size: map-get($font-sizes, sm);
color: var(--text-secondary);
}
}
.skill-bar {
@include progress-bar(8px);
.skill-progress {
height: 100%;
border-radius: 4px;
@include transition(width, $transition-slow);
}
}
}
}
}
.soft-skills {
text-align: center;
&-title {
font-size: map-get($font-sizes, xl);
font-weight: 700;
margin-bottom: 32px;
color: var(--text-primary);
}
&-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: center;
.soft-skill-tag {
@include tag(12px 20px, var(--bg-primary), var(--text-secondary));
display: flex;
align-items: center;
gap: 8px;
border: 2px solid var(--border-color);
border-radius: 50px;
font-weight: 500;
cursor: default;
&:hover {
border-color: $primary-color;
color: $primary-color;
transform: translateY(-2px);
}
}
}
}
}

View File

@@ -0,0 +1,6 @@
// ========================
// INDEX SCSS - Point d'entrée principal
// ========================
// Global styles uniquement - les composants sont dans main.scss
@use 'global';

View File

@@ -0,0 +1,15 @@
// ========================
// MAIN SCSS FILE
// ========================
// Base styles
@use 'global';
// Components
@use 'components/header';
@use 'components/hero';
@use 'components/about';
@use 'components/skills';
@use 'components/projects';
@use 'components/education';
@use 'components/contact';

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

7
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})