feat: Initialize backend with Express and MySQL, and restructure frontend with new routing and language support.
This commit is contained in:
26
frontend/.gitignore
vendored
Normal file
26
frontend/.gitignore
vendored
Normal 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
153
frontend/README.md
Normal 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
23
frontend/eslint.config.js
Normal 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
13
frontend/index.html
Normal 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
3957
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
frontend/package.json
Normal file
35
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1309
frontend/public/Dayron_Van_Leemput_CV.pdf
Normal file
1309
frontend/public/Dayron_Van_Leemput_CV.pdf
Normal file
File diff suppressed because it is too large
Load Diff
BIN
frontend/public/personnes.png
Normal file
BIN
frontend/public/personnes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
72
frontend/src/App.tsx
Normal file
72
frontend/src/App.tsx
Normal 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;
|
||||
BIN
frontend/src/assets/app_icon.png
Normal file
BIN
frontend/src/assets/app_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 694 KiB |
BIN
frontend/src/assets/dvl.jpg
Executable file
BIN
frontend/src/assets/dvl.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
136
frontend/src/components/About.tsx
Normal file
136
frontend/src/components/About.tsx
Normal 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;
|
||||
381
frontend/src/components/Contact.tsx
Normal file
381
frontend/src/components/Contact.tsx
Normal 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;
|
||||
291
frontend/src/components/Education.tsx
Normal file
291
frontend/src/components/Education.tsx
Normal 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;
|
||||
28
frontend/src/components/Footer.tsx
Normal file
28
frontend/src/components/Footer.tsx
Normal 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;
|
||||
159
frontend/src/components/Header.tsx
Normal file
159
frontend/src/components/Header.tsx
Normal 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;
|
||||
165
frontend/src/components/Hero.tsx
Normal file
165
frontend/src/components/Hero.tsx
Normal 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;
|
||||
26
frontend/src/components/Home.tsx
Normal file
26
frontend/src/components/Home.tsx
Normal 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;
|
||||
148
frontend/src/components/Policies.tsx
Normal file
148
frontend/src/components/Policies.tsx
Normal 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;
|
||||
221
frontend/src/components/Projects.tsx
Normal file
221
frontend/src/components/Projects.tsx
Normal 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;
|
||||
18
frontend/src/components/ScrollToTop.tsx
Normal file
18
frontend/src/components/ScrollToTop.tsx
Normal 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;
|
||||
199
frontend/src/components/Skills.tsx
Normal file
199
frontend/src/components/Skills.tsx
Normal 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;
|
||||
271
frontend/src/components/TravelMate.tsx
Normal file
271
frontend/src/components/TravelMate.tsx
Normal 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;
|
||||
495
frontend/src/contexts/LanguageContext.tsx
Normal file
495
frontend/src/contexts/LanguageContext.tsx
Normal 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
10
frontend/src/main.tsx
Normal 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>,
|
||||
)
|
||||
121
frontend/src/services/emailService.ts
Normal file
121
frontend/src/services/emailService.ts
Normal 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'
|
||||
);
|
||||
};
|
||||
137
frontend/src/styles/README.md
Normal file
137
frontend/src/styles/README.md
Normal 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
|
||||
```
|
||||
406
frontend/src/styles/_global.scss
Normal file
406
frontend/src/styles/_global.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
233
frontend/src/styles/_mixins.scss
Normal file
233
frontend/src/styles/_mixins.scss
Normal 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;
|
||||
}
|
||||
95
frontend/src/styles/_variables.scss
Normal file
95
frontend/src/styles/_variables.scss
Normal 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)
|
||||
);
|
||||
112
frontend/src/styles/components/_about.scss
Normal file
112
frontend/src/styles/components/_about.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
244
frontend/src/styles/components/_contact.scss
Normal file
244
frontend/src/styles/components/_contact.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
292
frontend/src/styles/components/_education.scss
Normal file
292
frontend/src/styles/components/_education.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
156
frontend/src/styles/components/_header.scss
Normal file
156
frontend/src/styles/components/_header.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
209
frontend/src/styles/components/_hero.scss
Normal file
209
frontend/src/styles/components/_hero.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
154
frontend/src/styles/components/_projects.scss
Normal file
154
frontend/src/styles/components/_projects.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
frontend/src/styles/components/_skills.scss
Normal file
109
frontend/src/styles/components/_skills.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
frontend/src/styles/index.scss
Normal file
6
frontend/src/styles/index.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
// ========================
|
||||
// INDEX SCSS - Point d'entrée principal
|
||||
// ========================
|
||||
|
||||
// Global styles uniquement - les composants sont dans main.scss
|
||||
@use 'global';
|
||||
15
frontend/src/styles/main.scss
Normal file
15
frontend/src/styles/main.scss
Normal 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';
|
||||
28
frontend/tsconfig.app.json
Normal file
28
frontend/tsconfig.app.json
Normal 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
7
frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
frontend/tsconfig.node.json
Normal file
26
frontend/tsconfig.node.json
Normal 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
7
frontend/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in New Issue
Block a user