first commit

This commit is contained in:
Dayron
2025-11-11 17:12:59 +01:00
commit 760849a5c2
24 changed files with 7124 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# 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?

73
README.md Normal file
View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

23
eslint.config.js Normal file
View File

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

13
index.html Normal file
View File

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

3497
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"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": {
"framer-motion": "^12.23.24",
"lucide-react": "^0.553.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"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",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.3",
"vite": "^7.2.2"
}
}

View File

1
public/vite.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 1.5 KiB

1452
src/App.css Normal file

File diff suppressed because it is too large Load Diff

49
src/App.tsx Normal file
View File

@@ -0,0 +1,49 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import Header from './components/Header';
import Hero from './components/Hero';
import About from './components/About';
import Skills from './components/Skills';
import Projects from './components/Projects';
import Education from './components/Education';
import Contact from './components/Contact';
import './App.css';
function App() {
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
const savedTheme = localStorage.getItem('darkMode');
if (savedTheme) {
setDarkMode(JSON.parse(savedTheme));
} else {
// Détection automatique du thème préféré de l'utilisateur
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 (
<div className={`app ${darkMode ? 'dark' : 'light'}`}>
<Header darkMode={darkMode} toggleDarkMode={toggleDarkMode} />
<main>
<Hero />
<About />
<Skills />
<Projects />
<Education />
<Contact />
</main>
</div>
);
}
export default App;

1
src/assets/react.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 4.0 KiB

140
src/components/About.tsx Normal file
View File

@@ -0,0 +1,140 @@
import { motion } from 'framer-motion';
import { User, Heart, Target, Coffee } from 'lucide-react';
const About = () => {
const stats = [
{ icon: <User size={24} />, value: "3ème", label: "Année d'études" },
{ icon: <Heart size={24} />, value: "100%", label: "Passion" },
{ icon: <Target size={24} />, value: "∞", label: "Objectifs" },
{ icon: <Coffee size={24} />, value: "☕", label: "Fuel quotidien" }
];
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
className="section-header"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">À propos de moi</h2>
<p className="section-subtitle">
Découvrez qui je suis et ce qui me passionne
</p>
</motion.div>
<div className="about-content">
<motion.div
className="about-text"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<motion.div className="about-card" variants={itemVariants}>
<h3>Mon parcours</h3>
<p>
Actuellement en 3ème année de <strong>Technologies de l'Informatique</strong> à la
HELHa de Tournai, je me passionne pour le développement d'applications et les
nouvelles technologies. Mon parcours m'a permis d'acquérir une solide base
technique et une approche méthodique du développement.
</p>
</motion.div>
<motion.div className="about-card" variants={itemVariants}>
<h3>Ma passion</h3>
<p>
Ce qui m'anime le plus, c'est la création d'solutions innovantes qui résolvent
des problèmes réels. J'aime particulièrement le développement mobile avec
<strong> Flutter</strong> et le développement web moderne avec
<strong> React</strong> et <strong> TypeScript</strong>.
</p>
</motion.div>
<motion.div className="about-card" variants={itemVariants}>
<h3>Mes objectifs</h3>
<p>
Je cherche constamment à améliorer mes compétences et à rester à jour avec
les dernières tendances technologiques. Mon objectif est de devenir un
développeur full-stack polyvalent et de contribuer à des projets qui ont
un impact positif.
</p>
</motion.div>
</motion.div>
<motion.div
className="about-stats"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{stats.map((stat, index) => (
<motion.div
key={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
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>En quelques mots</h3>
<p>
"La technologie n'est rien. Ce qui est important, c'est d'avoir la foi en les gens,
qu'ils soient fondamentalement bons et intelligents, et si vous leur donnez des outils,
ils feront des choses merveilleuses avec."
</p>
<cite>- Steve Jobs</cite>
</div>
</motion.div>
</div>
</section>
);
};
export default About;

341
src/components/Contact.tsx Normal file
View File

@@ -0,0 +1,341 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Mail, Phone, MapPin, Send, Github, Linkedin, MessageCircle, CheckCircle } from 'lucide-react';
const Contact = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Simulation d'envoi (remplacez par votre logique d'envoi réelle)
setTimeout(() => {
setIsSubmitting(false);
setIsSubmitted(true);
setFormData({ name: '', email: '', subject: '', message: '' });
// Reset du message de succès après 5 secondes
setTimeout(() => {
setIsSubmitted(false);
}, 5000);
}, 2000);
};
const contactInfo = [
{
icon: <Mail size={24} />,
title: "Email",
content: "dayron.vanleemput@example.com", // Remplacez par votre email
link: "mailto:dayron.vanleemput@example.com",
color: "#EA4335"
},
{
icon: <Phone size={24} />,
title: "Téléphone",
content: "+32 XXX XX XX XX", // Remplacez par votre numéro
link: "tel:+32XXXXXXXXX",
color: "#34A853"
},
{
icon: <MapPin size={24} />,
title: "Localisation",
content: "Tournai, Belgique",
link: "https://maps.google.com/?q=Tournai,Belgium",
color: "#4285F4"
}
];
const socialLinks = [
{
icon: <Github size={24} />,
name: "GitHub",
url: "https://github.com/dayronvanleemput", // Remplacez par votre profil
color: "#333"
},
{
icon: <Linkedin size={24} />,
name: "LinkedIn",
url: "https://linkedin.com/in/dayronvanleemput", // Remplacez par votre profil
color: "#0077B5"
}
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.3
}
}
};
const itemVariants = {
hidden: { opacity: 0, y: 50 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.25, 0.1, 0.25, 1] as const
}
}
};
return (
<section id="contact" className="contact">
<div className="container">
<motion.div
className="section-header"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">Contactez-moi</h2>
<p className="section-subtitle">
Une question, un projet ou simplement envie d'échanger ? N'hésitez pas à me contacter !
</p>
</motion.div>
<motion.div
className="contact-content"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{/* Informations de contact */}
<motion.div className="contact-info" variants={itemVariants}>
<div className="contact-intro">
<h3>
<MessageCircle size={24} />
Restons en contact
</h3>
<p>
Je suis toujours intéressé par de nouveaux projets, des collaborations
ou simplement des discussions autour de la technologie. N'hésitez pas
à me contacter !
</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>Retrouvez-moi aussi sur :</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>Envoyez-moi un message</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} />
Message envoyé avec succès ! Je vous répondrai bientôt.
</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">Nom complet</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
placeholder="Votre nom"
/>
</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">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">Sujet</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleChange}
required
placeholder="Objet de votre message"
/>
</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">Message</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
required
rows={6}
placeholder="Votre 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" />
Envoi en cours...
</>
) : (
<>
<Send size={20} />
Envoyer le message
</>
)}
</motion.button>
</form>
</motion.div>
</motion.div>
{/* Footer */}
<motion.footer
className="contact-footer"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.5 }}
viewport={{ once: true }}
>
<p>
© 2025 Dayron Van Leemput. Développé avec ❤️ en React et TypeScript.
</p>
</motion.footer>
</div>
</section>
);
};
export default Contact;

View File

@@ -0,0 +1,270 @@
import { motion } from 'framer-motion';
import { GraduationCap, Calendar, MapPin, Award, BookOpen, Target } from 'lucide-react';
const Education = () => {
const education = [
{
id: 1,
degree: "Bachelier en Technologies de l'Informatique",
school: "HELHa - Haute École Louvain en Hainaut",
location: "Tournai, Belgique",
period: "2023 - 2026",
currentYear: "3ème année",
status: "En cours",
description: "Formation complète en développement logiciel, programmation, bases de données, réseaux et gestion de projets informatiques.",
highlights: [
"Programmation orientée objet (Java, C#)",
"Développement web (HTML, CSS, JavaScript, React)",
"Développement mobile (Flutter, Dart)",
"Bases de données et SQL",
"Gestion de projets",
"Réseaux et systèmes"
],
color: "#4CAF50",
icon: <GraduationCap size={24} />
}
];
const certifications = [
{
title: "Développement Mobile Flutter",
provider: "Formation autodidacte",
date: "2024",
skills: ["Dart", "Flutter", "Firebase", "API REST"],
color: "#2196F3"
},
{
title: "React & TypeScript",
provider: "Projets personnels",
date: "2024",
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
className="section-header"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">Formation & Apprentissage</h2>
<p className="section-subtitle">
Mon parcours académique et mes apprentissages continus
</p>
</motion.div>
<div className="education-content">
{/* Formation principale */}
<motion.div
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} />
Formations complémentaires & Autodidacte
</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>Objectifs d'apprentissage 2025</h3>
<div className="goals-grid">
{[
{ goal: "Maîtriser Firebase et les services cloud", progress: 60 },
{ goal: "Approfondir Spring Boot pour le backend", progress: 30 },
{ goal: "Apprendre Docker et les conteneurs", progress: 20 },
{ goal: "Développer mes compétences en UI/UX", progress: 45 }
].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;

120
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,120 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Menu, X, Sun, Moon } from 'lucide-react';
interface HeaderProps {
darkMode: boolean;
toggleDarkMode: () => void;
}
const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const menuItems = [
{ name: 'Accueil', href: '#hero' },
{ name: 'À propos', href: '#about' },
{ name: 'Compétences', href: '#skills' },
{ name: 'Projets', href: '#projects' },
{ name: 'Formation', href: '#education' },
{ name: 'Contact', href: '#contact' }
];
const scrollToSection = (href: string) => {
const element = document.querySelector(href);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
setIsMenuOpen(false);
};
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 }}
>
<a href="#hero" onClick={(e) => { e.preventDefault(); scrollToSection('#hero'); }}>
Dayron Van Leemput
</a>
</motion.div>
{/* Navigation desktop */}
<ul className="nav-menu desktop-menu">
{menuItems.map((item, index) => (
<motion.li
key={item.name}
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();
scrollToSection(item.href);
}}
>
{item.name}
</a>
</motion.li>
))}
</ul>
<div className="nav-controls">
{/* Toggle thème */}
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={toggleDarkMode}
className="theme-toggle"
aria-label="Changer de thème"
>
{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="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.name}>
<a
href={item.href}
onClick={(e) => {
e.preventDefault();
scrollToSection(item.href);
}}
>
{item.name}
</a>
</li>
))}
</motion.ul>
</nav>
</motion.header>
);
};
export default Header;

159
src/components/Hero.tsx Normal file
View File

@@ -0,0 +1,159 @@
import { motion } from 'framer-motion';
import { Download, Github, Linkedin, Mail } from 'lucide-react';
const Hero = () => {
const handleDownloadCV = () => {
// Ici, vous pouvez ajouter le lien vers votre CV
const link = document.createElement('a');
link.href = '/cv-dayron-van-leemput.pdf'; // Ajoutez votre CV dans le dossier public
link.download = 'CV-Dayron-Van-Leemput.pdf';
link.click();
};
return (
<section id="hero" className="hero">
<div className="hero-content">
<motion.div
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 }}
>
Dayron Van Leemput
</motion.h1>
<motion.h2
className="hero-subtitle"
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
>
Étudiant en Technologies de l'Informatique
</motion.h2>
<motion.p
className="hero-description"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.8 }}
>
Bac 3 à la HELHa de Tournai | Jeune développeur passionné par les nouvelles technologies
et le développement d'applications innovantes
</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élécharger mon CV
</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} />
Me contacter
</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://github.com/dayronvanleemput" // 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://linkedin.com/in/dayronvanleemput" // 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
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 }}
>
{/* Vous pouvez remplacer ceci par votre photo */}
<div className="avatar-placeholder">
<span>DV</span>
</div>
</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;

250
src/components/Projects.tsx Normal file
View File

@@ -0,0 +1,250 @@
import { motion } from 'framer-motion';
import { ExternalLink, Github, Calendar, MapPin } from 'lucide-react';
const Projects = () => {
const projects = [
{
id: 1,
title: "Travel Mate",
description: "Application mobile conçue pour simplifier l'organisation de voyages de groupe. Elle permet de centraliser toutes les informations importantes d'un voyage : planification, gestion des dépenses, découverte d'activités et coordination entre les participants.",
status: "Bientôt disponible sur App Store et Play Store",
technologies: ["Dart", "Flutter", "Firebase"],
features: [
"Planification de voyage collaborative",
"Gestion des dépenses partagées",
"Découverte d'activités locales",
"Coordination en temps réel"
],
color: "#4CAF50",
icon: <MapPin size={24} />,
links: {
github: "#", // Remplacez par votre lien GitHub
demo: "#"
},
image: "/travel-mate-preview.png" // Ajoutez votre image dans le dossier public
},
{
id: 2,
title: "Portfolio Web",
description: "Site web personnel moderne et responsive développé avec React et TypeScript. Inclut des animations fluides, un mode sombre/clair et une architecture modulaire.",
status: "Projet actuel",
technologies: ["React", "TypeScript", "Framer Motion", "CSS3"],
features: [
"Design responsive",
"Animations fluides",
"Mode sombre/clair",
"Performance optimisée"
],
color: "#2196F3",
icon: <ExternalLink size={24} />,
links: {
github: "https://github.com/dayronvanleemput/portfolio", // Remplacez par votre lien
demo: "https://dayronvanleemput.dev" // 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
className="section-header"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">Mes Projets</h2>
<p className="section-subtitle">
Découvrez les projets sur lesquels j'ai travaillé et qui me tiennent à cœur
</p>
</motion.div>
<motion.div
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>Fonctionnalités principales :</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.github !== "#" && (
<motion.a
href={project.links.github}
target="_blank"
rel="noopener noreferrer"
className="project-link"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
<Github size={20} />
Code
</motion.a>
)}
{project.links.demo !== "#" && (
<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} />
Voir le projet
</motion.a>
)}
</div>
</div>
</motion.div>
))}
</motion.div>
{/* Section des projets futurs */}
<motion.div
className="future-projects"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
viewport={{ once: true }}
>
<h3 className="future-title">Projets à venir</h3>
<div className="future-projects-grid">
{[
{
title: "Application de gestion de tâches",
description: "Une app de productivité avec synchronisation cloud",
tech: "Flutter • Firebase • Bloc"
},
{
title: "API REST e-commerce",
description: "Backend complet pour application e-commerce",
tech: "Java • Spring Boot • PostgreSQL"
},
{
title: "Dashboard analytique",
description: "Interface web pour visualiser des données",
tech: "React • D3.js • TypeScript"
}
].map((futureProject, index) => (
<motion.div
key={index}
className="future-project-card"
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{
scale: 1.02,
y: -2,
transition: { duration: 0.2 }
}}
viewport={{ once: true }}
>
<div className="future-project-icon">
<Calendar size={20} />
</div>
<h4>{futureProject.title}</h4>
<p>{futureProject.description}</p>
<span className="future-tech">{futureProject.tech}</span>
</motion.div>
))}
</div>
</motion.div>
</div>
</section>
);
};
export default Projects;

191
src/components/Skills.tsx Normal file
View File

@@ -0,0 +1,191 @@
import { motion } from 'framer-motion';
import { Code, Database, Smartphone, Globe, Server, Wrench } from 'lucide-react';
const Skills = () => {
const skillCategories = [
{
icon: <Smartphone size={32} />,
title: "Mobile",
color: "#4FC3F7",
skills: [
{ name: "Dart", level: 85, color: "#0175C2" },
{ name: "Flutter", level: 80, color: "#02569B" }
]
},
{
icon: <Globe size={32} />,
title: "Frontend",
color: "#42A5F5",
skills: [
{ name: "React", level: 75, color: "#61DAFB" },
{ name: "TypeScript", level: 70, color: "#3178C6" },
{ name: "JavaScript", level: 80, color: "#F7DF1E" }
]
},
{
icon: <Server size={32} />,
title: "Backend",
color: "#66BB6A",
skills: [
{ name: "Java", level: 75, color: "#ED8B00" },
{ name: "C#", level: 65, color: "#239120" }
]
},
{
icon: <Database size={32} />,
title: "Outils & Autres",
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
className="section-header"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<h2 className="section-title">Compétences & Technologies</h2>
<p className="section-subtitle">
Les technologies que je maîtrise et avec lesquelles j'aime travailler
</p>
</motion.div>
<motion.div
className="skills-grid"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{skillCategories.map((category, categoryIndex) => (
<motion.div
key={category.title}
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">Autres compétences</h3>
<div className="soft-skills-grid">
{[
{ name: "Résolution de problèmes", icon: <Wrench size={20} /> },
{ name: "Travail en équipe", icon: <Code size={20} /> },
{ name: "Apprentissage continu", icon: <Database size={20} /> },
{ name: "Communication", icon: <Globe size={20} /> }
].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;

410
src/index.css Normal file
View File

@@ -0,0 +1,410 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
/* Importation des polices Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
/* Reset CSS moderne */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Variables CSS globales */
:root {
/* Polices */
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
/* Tailles de police responsive */
--fs-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
--fs-sm: clamp(0.875rem, 0.8rem + 0.375vw, 1rem);
--fs-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--fs-lg: clamp(1.125rem, 1.05rem + 0.375vw, 1.25rem);
--fs-xl: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
--fs-2xl: clamp(1.5rem, 1.35rem + 0.75vw, 2rem);
--fs-3xl: clamp(2rem, 1.75rem + 1.25vw, 2.5rem);
--fs-4xl: clamp(2.5rem, 2rem + 2.5vw, 4rem);
/* Espacements */
--space-xs: clamp(0.25rem, 0.2rem + 0.25vw, 0.5rem);
--space-sm: clamp(0.5rem, 0.4rem + 0.5vw, 1rem);
--space-md: clamp(1rem, 0.8rem + 1vw, 2rem);
--space-lg: clamp(2rem, 1.5rem + 2.5vw, 4rem);
--space-xl: clamp(4rem, 3rem + 5vw, 8rem);
/* Z-index */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
}
/* Configuration du document */
html {
scroll-behavior: smooth;
scroll-padding-top: 80px;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
body {
font-family: var(--font-family);
font-size: var(--fs-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;
}
/* 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;
}
/* Amélioration de l'accessibilité */
/* Masquer visuellement mais garder accessible aux lecteurs d'écran */
.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;
}
/* Focus visible amélioré */
.focus-outline {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
border-radius: 4px;
}
/* Amélioration de la sélection de texte */
::selection {
background-color: var(--primary-color);
color: white;
text-shadow: none;
}
::-moz-selection {
background-color: var(--primary-color);
color: white;
text-shadow: none;
}
/* Amélioration des scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--text-muted);
border-radius: 4px;
transition: background-color 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
/* Support des scrollbars Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--text-muted) var(--bg-secondary);
}
/* 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 écrans très larges */
@media (min-width: 1440px) {
.container {
max-width: 1400px;
}
}
/* Support pour les très petits écrans */
@media (max-width: 320px) {
:root {
--container-padding: 0 12px;
}
}
/* Amélioration des performances */
.gpu-accelerated {
transform: translateZ(0);
-webkit-transform: translateZ(0);
will-change: transform;
}
/* Classes utilitaires */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.not-sr-only {
position: static;
width: auto;
height: auto;
padding: 0;
margin: 0;
overflow: visible;
clip: auto;
white-space: normal;
}
/* 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;
}
}
/* Amélioration du loading */
.loading {
pointer-events: none;
opacity: 0.6;
}
/* Animations globales */
@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);
}
}
/* 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;
}

10
src/main.tsx Normal file
View File

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

28
tsconfig.app.json Normal file
View File

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

7
tsconfig.json Normal file
View File

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

26
tsconfig.node.json Normal file
View File

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

7
vite.config.ts Normal file
View File

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