first commit

This commit is contained in:
gpatruno
2026-04-18 00:57:07 +02:00
commit 5c423bae47
37 changed files with 4850 additions and 0 deletions
+24
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?
+16
View File
@@ -0,0 +1,16 @@
# React + 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 [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## 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 using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
+29
View File
@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Portfolio</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+2671
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
{
"name": "portfolio",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-icons": "^5.6.0",
"react-router-dom": "^7.14.1"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"vite": "^8.0.4"
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

+184
View File
@@ -0,0 +1,184 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}
+20
View File
@@ -0,0 +1,20 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { SiteLayout } from './layouts/SiteLayout.jsx'
import { Home } from './pages/Home.jsx'
import { Profil } from './pages/Profil.jsx'
import { Formations } from './pages/Formations.jsx'
import { Projets } from './pages/Projets.jsx'
export default function App() {
return (
<Routes>
<Route element={<SiteLayout />}>
<Route path="/" element={<Home />} />
<Route path="/profil" element={<Profil />} />
<Route path="/formations" element={<Formations />} />
<Route path="/projets" element={<Projets />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+1
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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

+52
View File
@@ -0,0 +1,52 @@
export function ContentStack({ panel1, panel1b, panel2, panel3 }) {
return (
<div className="content-stack" aria-label="Contenu principal">
<section
className={[
'content-stack__panel',
panel1 ? 'content-stack__panel--padded' : null,
]
.filter(Boolean)
.join(' ')}
aria-label="Bloc 1"
>
{panel1}
</section>
{panel1b != null ? (
<section
className={[
'content-stack__panel',
'content-stack__panel--padded',
].join(' ')}
aria-label="Bloc 2"
>
{panel1b}
</section>
) : null}
<section
className={[
'content-stack__panel',
panel2 ? 'content-stack__panel--padded' : null,
]
.filter(Boolean)
.join(' ')}
aria-label={
panel3 != null || panel1b != null ? 'Bloc 3' : 'Bloc 2'
}
>
{panel2}
</section>
{panel3 != null ? (
<section
className={[
'content-stack__panel',
'content-stack__panel--padded',
].join(' ')}
aria-label="Bloc 4"
>
{panel3}
</section>
) : null}
</div>
)
}
+121
View File
@@ -0,0 +1,121 @@
/**
* Frise centrée : un point par événement, ordre chronologique (plus ancien en haut).
* Chaque entrée peut avoir une description optionnelle.
*/
const EVENTS = [
{
period: '2018',
sequence: 0,
side: 'left',
title: 'Bac techno STI2D SIN',
description: 'Don Bosco',
},
{
period: '2019',
sequence: 1,
side: 'right',
title: 'Formation à l’étranger en anglais',
description: '1 mois — Canada',
},
{
period: '2020-2021',
sequence: 0,
side: 'left',
title: 'CAP Mécanique auto',
description: 'AF13',
},
{
period: '2021-2022',
sequence: 0,
side: 'right',
title: 'CAP Plomberie chauffagerie',
},
{
period: '2022-2025',
sequence: 0,
side: 'left',
title: 'Administrateur système\nDéveloppeur application',
description: 'OpenMotion',
},
{
period: '2022-2026',
sequence: 0,
side: 'right',
title: 'Installation et configuration de serveurs personnels',
description: ' de projets personnels et professionnels.',
},
]
function parsePeriod(period) {
const m = String(period).match(/^(\d{4})(?:-(\d{4}))?$/)
if (!m) return { start: 0, end: 0 }
const start = Number.parseInt(m[1], 10)
const end = m[2] ? Number.parseInt(m[2], 10) : start
return { start, end }
}
function compareChronological(a, b) {
const pa = parsePeriod(a.period)
const pb = parsePeriod(b.period)
if (pa.start !== pb.start) return pa.start - pb.start
if (pa.end !== pb.end) return pa.end - pb.end
return (a.sequence ?? 0) - (b.sequence ?? 0)
}
const SORTED_EVENTS = [...EVENTS].sort(compareChronological)
function TimelineCard({ period, title, description, side }) {
return (
<article
className={`formations-timeline__card formations-timeline__card--${side}`}
>
<time className="formations-timeline__period" dateTime={period}>
{period}
</time>
<h3 className="formations-timeline__card-title">{title}</h3>
{description ? (
<p className="formations-timeline__desc">{description}</p>
) : null}
</article>
)
}
export function FormationsTimeline() {
return (
<div className="formations-timeline">
<div className="formations-timeline__rows">
<div className="formations-timeline__line" aria-hidden />
{SORTED_EVENTS.map((event) => (
<div
key={`${event.period}-${event.side}-${event.sequence}-${event.title}`}
className="formations-timeline__row"
>
<div className="formations-timeline__cell formations-timeline__cell--left">
{event.side === 'left' ? (
<TimelineCard
side="left"
period={event.period}
title={event.title}
description={event.description}
/>
) : null}
</div>
<div className="formations-timeline__node-wrap">
<span className="formations-timeline__node" aria-hidden />
</div>
<div className="formations-timeline__cell formations-timeline__cell--right">
{event.side === 'right' ? (
<TimelineCard
side="right"
period={event.period}
title={event.title}
description={event.description}
/>
) : null}
</div>
</div>
))}
</div>
</div>
)
}
@@ -0,0 +1,14 @@
import { INTRO } from './introText.js'
export function HomeAutobiographiePanel() {
return (
<div className="home-intro home-intro--profil-box">
<section className="home-intro__section" aria-labelledby="home-autobio-title">
<h2 className="home-intro__heading" id="home-autobio-title">
Autobiographie
</h2>
<p className="home-intro__text">{INTRO}</p>
</section>
</div>
)
}
+32
View File
@@ -0,0 +1,32 @@
const ENTRIES = [
{ period: '2018', title: 'Bac techno STI2D SIN' },
{ period: '2019-2020', title: 'CAP Mecanique Auto' },
{ period: '2020-2021', title: 'CAP Plomberie Chauffagerie' },
{
period: '2022-2025',
title: 'Administrateur système\nDéveloppeur application',
description: 'OpenMotion',
},
]
export function HomeEducationTimeline() {
return (
<div className="home-timeline">
<h2 className="home-timeline__heading">Parcours</h2>
<ol className="home-timeline__list">
{ENTRIES.map((entry) => (
<li key={`${entry.period}-${entry.title}`} className="home-timeline__item">
<span className="home-timeline__marker" aria-hidden />
<div className="home-timeline__body">
<span className="home-timeline__period">{entry.period}</span>
<p className="home-timeline__title">{entry.title}</p>
{entry.description ? (
<p className="home-timeline__desc">{entry.description}</p>
) : null}
</div>
</li>
))}
</ol>
</div>
)
}
+30
View File
@@ -0,0 +1,30 @@
import { INTRO } from './introText.js'
const TAGS = [
{ label: 'Curieux', tone: 'pos' },
{ label: 'Polyvalent', tone: 'pos' },
{ label: 'Autodidacte', tone: 'pos' },
{ label: 'Logique', tone: 'pos' },
{ label: 'Persévérance', tone: 'pos' },
{ label: "Esprit d'Analyse", tone: 'pos' },
{ label: 'Autonomie', tone: 'pos' },
{ label: "Capacité d'Adaptation", tone: 'pos' },
{ label: 'Impatient', tone: 'neg' },
{ label: 'Besoin de comprendre', tone: 'neg' },
{ label: 'Pédagogie', tone: 'neg' },
]
export function HomeIntroPanel() {
return (
<div className="home-intro">
<p className="home-intro__text">{INTRO}</p>
<ul className="home-intro__tags" aria-label="Traits">
{TAGS.map(({ label, tone }) => (
<li key={label}>
<span className={`home-tag home-tag--${tone}`}>{label}</span>
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1,28 @@
import { SKILLS } from '../data/skills.js'
export function HomeProfilCompetencesPanel() {
return (
<div className="home-intro home-intro--profil-box">
<section
className="home-profil-traits"
aria-labelledby="home-profil-competences-title"
>
<h2 className="home-intro__heading" id="home-profil-competences-title">
Compétence
</h2>
<ul className="home-profil-traits__list">
{SKILLS.map(({ label, cat, desc }) => (
<li key={label} className="home-profil-traits__row">
<span
className={`home-profil-traits__label home-skill home-skill--${cat}`}
>
{label}
</span>
<p className="home-profil-traits__desc">{desc}</p>
</li>
))}
</ul>
</section>
</div>
)
}
@@ -0,0 +1,28 @@
import { PROFIL_TRAITS } from '../data/profilTraits.js'
export function HomeProfilQualitesPanel() {
return (
<div className="home-intro home-intro--profil-box">
<section
className="home-profil-traits"
aria-labelledby="home-profil-traits-title"
>
<h2 className="home-intro__heading" id="home-profil-traits-title">
Qualité / Défauts
</h2>
<ul className="home-profil-traits__list">
{PROFIL_TRAITS.map(({ label, tone, desc }) => (
<li key={label} className="home-profil-traits__row">
<span
className={`home-profil-traits__label home-tag home-tag--${tone}`}
>
{label}
</span>
<p className="home-profil-traits__desc">{desc}</p>
</li>
))}
</ul>
</section>
</div>
)
}
+75
View File
@@ -0,0 +1,75 @@
import { createElement } from 'react'
import { FaGithub, FaLinkedinIn } from 'react-icons/fa6'
import { SiTryhackme } from 'react-icons/si'
const SOCIAL = [
{
label: 'LinkedIn',
href: 'https://www.linkedin.com/',
Icon: FaLinkedinIn,
},
{
label: 'GitHub',
href: 'https://github.com/',
Icon: FaGithub,
},
{
label: 'TryHackMe',
href: 'https://tryhackme.com/',
Icon: SiTryhackme,
},
]
export function HomeSidebar({ layout = 'vertical' }) {
const isHorizontal = layout === 'horizontal'
return (
<div
className={[
'home-sidebar__inner',
isHorizontal ? 'home-sidebar__inner--horizontal' : null,
]
.filter(Boolean)
.join(' ')}
>
<img
className="home-sidebar__photo"
src="https://www.thispersondoesnotexist.com"
alt=""
width={280}
height={280}
loading="lazy"
decoding="async"
/>
<div className="home-sidebar__cluster">
<p className="home-sidebar__name">Prénom Nom</p>
<ul className="home-sidebar__social" aria-label="Liens sociaux">
{SOCIAL.map(({ label, href, Icon }) => (
<li key={label}>
<a
className="home-sidebar__social-link"
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={label}
>
{createElement(Icon, {
'aria-hidden': true,
className: 'home-sidebar__social-icon',
})}
</a>
</li>
))}
</ul>
</div>
<div className="home-sidebar__extra">
<ul className="home-sidebar__extra-list">
<li>Marseille</li>
<li>Voiture</li>
<li>Francais</li>
<li>Anglais B1</li>
</ul>
</div>
</div>
)
}
+18
View File
@@ -0,0 +1,18 @@
import { SKILLS } from '../data/skills.js'
export function HomeSkillsBlock() {
return (
<section className="home-skills" aria-labelledby="home-skills-title">
<h2 className="home-skills__title" id="home-skills-title">
Compétence
</h2>
<ul className="home-skills__list">
{SKILLS.map(({ label, cat }) => (
<li key={label}>
<span className={`home-skill home-skill--${cat}`}>{label}</span>
</li>
))}
</ul>
</section>
)
}
+53
View File
@@ -0,0 +1,53 @@
import { PROJETS } from '../data/projets.js'
const SKILL_CATS = ['green', 'blue', 'yellow', 'red']
export function ProjetsGrid() {
return (
<ul className="projets-grid">
{PROJETS.map((projet, index) => (
<li key={projet.id} className="projets-grid__cell">
<article className="projet-card" aria-labelledby={`projet-title-${projet.id}`}>
<div className="projet-card__media">
<img
src={projet.imageSrc}
alt={projet.imageAlt}
className="projet-card__img"
loading="lazy"
decoding="async"
referrerPolicy="no-referrer"
/>
</div>
<div className="projet-card__body">
<h2 className="projet-card__title" id={`projet-title-${projet.id}`}>
{projet.title}
</h2>
<ul className="projet-card__skills" aria-label="Compétences">
{projet.skills.map((skill, i) => (
<li key={skill}>
<span
className={`home-skill home-skill--${
SKILL_CATS[(index + i) % SKILL_CATS.length]
}`}
>
{skill}
</span>
</li>
))}
</ul>
<p className="projet-card__desc">{projet.description}</p>
<a
href={projet.link}
className="projet-card__link"
target="_blank"
rel="noopener noreferrer"
>
{projet.linkLabel}
</a>
</div>
</article>
</li>
))}
</ul>
)
}
+29
View File
@@ -0,0 +1,29 @@
import { NavLink } from 'react-router-dom'
function getNavLinkClassName({ isActive }) {
return ['topnav__link', isActive ? 'is-active' : null].filter(Boolean).join(' ')
}
export function TopNav() {
return (
<header className="topnav">
<div className="topnav__inner">
<nav className="topnav__nav" aria-label="Sommaire">
<NavLink to="/" className={getNavLinkClassName} end>
Accueil
</NavLink>
<NavLink to="/profil" className={getNavLinkClassName}>
Profil
</NavLink>
<NavLink to="/formations" className={getNavLinkClassName}>
Formations
</NavLink>
<NavLink to="/projets" className={getNavLinkClassName}>
Projets
</NavLink>
</nav>
</div>
</header>
)
}
+2
View File
@@ -0,0 +1,2 @@
export const INTRO =
"Passionné d'informatique et d'électronique depuis petit et curieux de nature. J'ai toujours aimé apprendre en créant mes propres systèmes. J'accomplis des projets et des architectures informatiques pour me simplifier la vie. Polyvalent j'effectue également la réparation de divers équipements électroniques."
+52
View File
@@ -0,0 +1,52 @@
export const PROFIL_TRAITS = [
{
label: 'La Logique',
tone: 'pos',
desc: 'Capacité à décomposer un problème complexe en étapes simples.',
},
{
label: 'La Curiosité',
tone: 'pos',
desc: "S'intéresser facilement, aimer apprendre de nouvelles choses.",
},
{
label: "L'Autonomie",
tone: 'pos',
desc: 'Savoir trouver la solution dans une documentation ou sur un forum sans aide extérieure.',
},
{
label: 'La Persévérance',
tone: 'pos',
desc: 'Ne pas abandonner facilement.',
},
{
label: "L'Esprit d'Analyse",
tone: 'pos',
desc: "Savoir anticiper les failles ou les besoins futurs d'un système.",
},
{
label: "La Capacité d'Adaptation",
tone: 'pos',
desc: "Changer de projet rapidement sans difficulté. s'adapter à de nouvelles technologies.",
},
{
label: 'Polyvalent',
tone: 'pos',
desc: "Administration système, développement logiciel, réparation d'équipements électroniques, modification de hardware, Mécanique, Plomberie, Electricité.",
},
{
label: "L'Impatience",
tone: 'neg',
desc: 'L\'envie que les choses avancent, que l\'attente est une perte de temps.',
},
{
label: 'Pédagogie',
tone: 'neg',
desc: "Capacité à expliquer un problème technique complexe à un utilisateur qui n'y connaît rien.",
},
{
label: 'Besoin de comprendre',
tone: 'neg',
desc: 'Ne pas se contenter de voir que quelque chose fonctionne, mais comprendre comment ça fonctionne.',
},
]
+114
View File
@@ -0,0 +1,114 @@
/** Cartes projet (page Projets) : titre, compétences, visuel, texte, lien externe. */
export const PROJETS = [
{
id: 'Self-Hosting',
title: 'Self Hosting',
skills: [ 'Linux', 'Bash', 'Dockers', 'Sécurité'],
imageSrc: 'https://picsum.photos/seed/pdfredact/640/360',
imageAlt: 'Home Lab, Serveurs personnels',
description:
'Installation et configuration de serveurs personnels pour des projets personnels et professionnels.',
link: 'https://pdfbox.apache.org/',
linkLabel: 'Apache PDFBox',
},
{
id: 'androidpayload',
title: 'androidpayload',
skills: ['Linux', 'Bash', 'Java', 'Hardware'],
imageSrc: 'https://www.thispersondoesnotexist.com/',
imageAlt: 'Visuel du projet androidpayload',
description:
'Application pour récupérer tous les droits sur un appareil Android (ROOT).',
link: 'https://github.com/topjohnwu/Magisk',
linkLabel: 'Magisk sur GitHub',
},
{
id: 'homelab-stack',
title: 'HomelabStack',
skills: ['Docker', 'Linux', 'Ansible', 'Nginx'],
imageSrc: 'https://picsum.photos/seed/homelab/640/360',
imageAlt: 'Schéma de services conteneurisés',
description:
'Automatisation du déploiement de services auto-hébergés, reverse proxy et sauvegardes planifiées.',
link: 'https://github.com/ansible/ansible',
linkLabel: 'Ansible sur GitHub',
},
{
id: 'capteurs-maison',
title: 'CapteursMaison',
skills: ['C', 'MQTT', 'ESP32', 'Électronique'],
imageSrc: 'https://picsum.photos/seed/iotcapteurs/640/360',
imageAlt: 'Maquette capteurs connectés',
description:
'Firmware embarqué et passerelle MQTT pour relevés de température et humidité en temps réel.',
link: 'https://mqtt.org/',
linkLabel: 'Protocole MQTT',
},
{
id: 'veille-marche',
title: 'VeilleMarché',
skills: ['Python', 'REST', 'SQLite', 'CLI'],
imageSrc: 'https://picsum.photos/seed/chartscli/640/360',
imageAlt: 'Courbes et terminal',
description:
'Outil en ligne de commande pour agréger des cours, historiser en local et déclencher des alertes.',
link: 'https://docs.python.org/3/library/argparse.html',
linkLabel: 'argparse (Python)',
},
{
id: 'photo-archiver',
title: 'PhotoArchiver',
skills: ['Rust', 'CLI', 'Filesystem', 'EXIF'],
imageSrc: 'https://picsum.photos/seed/photoarch/640/360',
imageAlt: 'Dossiers et vignettes photo',
description:
'Indexation dune photothèque locale, détection de doublons par empreinte et renommage par date EXIF.',
link: 'https://exiftool.org/',
linkLabel: 'ExifTool',
},
{
id: 'net-pulse',
title: 'NetPulse',
skills: ['Go', 'Prometheus', 'Grafana', 'HTTP'],
imageSrc: 'https://picsum.photos/seed/netpulse/640/360',
imageAlt: 'Graphiques de métriques réseau',
description:
'Sonde légère et tableaux de bord pour suivre latence, codes HTTP et disponibilité des services du lab.',
link: 'https://prometheus.io/',
linkLabel: 'Prometheus',
},
{
id: 'bench-auto',
title: 'BenchAuto',
skills: ['Bash', 'Jenkins', 'Git', 'curl'],
imageSrc: 'https://picsum.photos/seed/benchauto/640/360',
imageAlt: 'Pipeline et rapports de tests',
description:
'Jobs planifiés pour enchaîner scénarios de charge, collecter les métriques et publier un rapport HTML.',
link: 'https://www.jenkins.io/',
linkLabel: 'Jenkins',
},
{
id: 'mesh-relay',
title: 'MeshRelay',
skills: ['Zigbee', 'Node', 'MQTT', 'Home Assistant'],
imageSrc: 'https://picsum.photos/seed/meshrelay/640/360',
imageAlt: 'Passerelle et objets connectés',
description:
'Automatisations domotiques : capteurs Zigbee, scénarios jour/nuit et notifications sur événements.',
link: 'https://www.home-assistant.io/',
linkLabel: 'Home Assistant',
},
{
id: 'sync-vault',
title: 'SyncVault',
skills: ['rsync', 'S3', 'LUKS', 'Cron'],
imageSrc: 'https://picsum.photos/seed/syncvault/640/360',
imageAlt: 'Sauvegarde et chiffrement',
description:
'Volumes chiffrés, sauvegardes incrémentielles locales et miroir distant compatible stockage objet.',
link: 'https://rclone.org/',
linkLabel: 'rclone',
},
]
+43
View File
@@ -0,0 +1,43 @@
/** Compétences : couleur + courte phrase (page Profil). */
export const SKILLS = [
{
label: 'Linux',
cat: 'green',
desc: 'Utilisé en entreprise et quotidiennement. j\'utilise principalement ArchLinux ou Debian, j\'effectue installation et configuration complète d\'un OS, développement d\'applications Linux, etc.',
},
{
label: 'Bash',
cat: 'green',
desc: 'Utilisé en entreprise et quotidiennement. Je conçois et modifie des scripts ainsi que des services système, ces outils me permettent l\'automatisation de tâches répétitives, la gestion des sauvegardes, l\'analyse de données et l\'administration de serveurs à distance.. ',
},
{
label: 'Python',
cat: 'green',
desc: 'Utilisé en entreprise et quotidiennement. pour des projets nécessitant une grande flexibilité : reconnaissance d\'image,api, ia, trading cryptomonnaie, etc.',
},
{
label: 'Réseau Système',
cat: 'blue',
desc: 'Utilisation au quotidien et en entreprise. Mise en place de serveur privé avec des services privé et publique (vpn, docker, etc), avec réseau de serveur de backup, etc.',
},
{
label: 'PHP',
cat: 'blue',
desc: 'Utilisé en entreprise avec un framework privé, ce back-office fait office de centre de contrôle pour un système complet gérant les serveurs, les bases de données et les applications mobiles.',
},
{
label: 'Hardware',
cat: 'yellow',
desc: 'Utilisé en entreprise et au domicile. Diagnostic, réparation et modification de fonctionnement.',
},
{
label: 'JAVA',
cat: 'yellow',
desc: 'Utilisée en entreprise, cette application mobile remplace l\'interface utilisateur Android, Elle permet la gestion des droits d\'accès à l\'appareil ainsi que le suivi d\'activités pour faciliter la maintenance et l\'accompagnement de l\'intervenant. intègre également des fonctionnalité comme : trajets GPS, comptage de détections par reconnaissance d\'image, calendrier des missions...',
},
{
label: 'Assembleur',
cat: 'red',
desc: 'Utilisé en entreprise et au domicile lecture de partitions et modification de cette derniere, afin de modifier le comportement d\'appareils android, modification de comportement de logicel : jeux video, etc.',
},
]
+1021
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
import { Outlet } from 'react-router-dom'
import { TopNav } from '../components/TopNav.jsx'
export function SiteLayout() {
return (
<div className="app-shell">
<TopNav />
<main className="app-main">
<Outlet />
</main>
</div>
)
}
+13
View File
@@ -0,0 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)
+16
View File
@@ -0,0 +1,16 @@
import { FormationsTimeline } from '../components/FormationsTimeline.jsx'
export function Formations() {
return (
<div className="page page--stack-only page--formations">
<div className="content-stack" aria-label="Contenu principal">
<section
className="content-stack__panel content-stack__panel--padded"
aria-label="Parcours"
>
<FormationsTimeline />
</section>
</div>
</div>
)
}
+5
View File
@@ -0,0 +1,5 @@
import { HomePageView } from './HomePageView.jsx'
export function Home() {
return <HomePageView />
}
+43
View File
@@ -0,0 +1,43 @@
import { ContentStack } from '../components/ContentStack.jsx'
import { HomeAutobiographiePanel } from '../components/HomeAutobiographiePanel.jsx'
import { HomeEducationTimeline } from '../components/HomeEducationTimeline.jsx'
import { HomeIntroPanel } from '../components/HomeIntroPanel.jsx'
import { HomeProfilCompetencesPanel } from '../components/HomeProfilCompetencesPanel.jsx'
import { HomeProfilQualitesPanel } from '../components/HomeProfilQualitesPanel.jsx'
import { HomeSkillsBlock } from '../components/HomeSkillsBlock.jsx'
import { HomeSidebar } from '../components/HomeSidebar.jsx'
/** Accueil : sidebar à gauche. Profil (`variant="profil"`) : bandeau profil horizontal au-dessus du contenu. */
export function HomePageView({ variant = 'home' }) {
const isProfil = variant === 'profil'
return (
<div
className={['page', 'page--home', isProfil ? 'page--home-profil' : null]
.filter(Boolean)
.join(' ')}
>
<aside
className={['home-sidebar', isProfil ? 'home-sidebar--horizontal' : null]
.filter(Boolean)
.join(' ')}
aria-label="Profil"
>
<HomeSidebar layout={isProfil ? 'horizontal' : 'vertical'} />
</aside>
<div className="home-main">
<ContentStack
panel1={
isProfil ? <HomeAutobiographiePanel /> : <HomeIntroPanel />
}
panel1b={
isProfil ? <HomeProfilCompetencesPanel /> : <HomeSkillsBlock />
}
panel2={
isProfil ? <HomeProfilQualitesPanel /> : <HomeEducationTimeline />
}
/>
</div>
</div>
)
}
+5
View File
@@ -0,0 +1,5 @@
import { HomePageView } from './HomePageView.jsx'
export function Profil() {
return <HomePageView variant="profil" />
}
+16
View File
@@ -0,0 +1,16 @@
import { ProjetsGrid } from '../components/ProjetsGrid.jsx'
export function Projets() {
return (
<div className="page page--stack-only page--projets">
<div className="content-stack" aria-label="Contenu principal">
<section
className="content-stack__panel content-stack__panel--padded"
aria-label="Liste des projets"
>
<ProjetsGrid />
</section>
</div>
</div>
)
}
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: true,
},
preview: {
host: true,
},
})