first commit
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
73
README.md
Normal 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
23
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
index.html
Normal file
13
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="/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
3497
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
public/cv-dayron-van-leemput.pdf
Normal file
0
public/cv-dayron-van-leemput.pdf
Normal file
1
public/vite.svg
Normal file
1
public/vite.svg
Normal 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
1452
src/App.css
Normal file
File diff suppressed because it is too large
Load Diff
49
src/App.tsx
Normal file
49
src/App.tsx
Normal 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
1
src/assets/react.svg
Normal 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
140
src/components/About.tsx
Normal 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
341
src/components/Contact.tsx
Normal 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;
|
||||||
270
src/components/Education.tsx
Normal file
270
src/components/Education.tsx
Normal 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
120
src/components/Header.tsx
Normal 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
159
src/components/Hero.tsx
Normal 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
250
src/components/Projects.tsx
Normal 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
191
src/components/Skills.tsx
Normal 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
410
src/index.css
Normal 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
10
src/main.tsx
Normal 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
28
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
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
tsconfig.node.json
Normal file
26
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
vite.config.ts
Normal file
7
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