feat: Add LoadingScreen component with typing effect and cursor animation

feat: Enhance Menu component with new navigation items and hover effects

feat: Introduce ParticlesBackground component for visual effects

refactor: Remove unused ProgressBar component

feat: Update ProjectCard component to include modal for project details

feat: Create TimelineSection component to display work and education history

fix: Update i18n translations for about section and project descriptions

style: Add custom animations and utility classes in CSS
This commit is contained in:
Félix MARQUET
2025-08-25 11:01:04 +00:00
parent be02c5a0b8
commit 191158f197
15 changed files with 1732 additions and 91 deletions

441
package-lock.json generated
View File

@@ -23,7 +23,9 @@
"react-github-btn": "^1.4.0",
"react-i18next": "^15.6.1",
"react-icons": "^5.5.0",
"react-particles": "^2.12.2",
"sass": "^1.86.0",
"tsparticles-slim": "^2.12.0",
"typescript": "^5.8.3",
"web-vitals": "^5.1.0",
"yarn": "^1.22.22"
@@ -4903,6 +4905,33 @@
"license": "MIT",
"peer": true
},
"node_modules/react-particles": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/react-particles/-/react-particles-2.12.2.tgz",
"integrity": "sha512-Bo9DdrBRPFy8uiT7BA8P36Rdmz6GhB/RG9kkWUyZGIsS8AxWyuUjpVxw9Lr23K0LVE2aenAJ0vnqEbbLDpBgQw==",
"deprecated": "@tsparticles/react is the new version, please use that",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"hasInstallScript": true,
"dependencies": {
"tsparticles-engine": "^2.12.0"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -5500,6 +5529,418 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tsparticles-basic": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-basic/-/tsparticles-basic-2.12.0.tgz",
"integrity": "sha512-pN6FBpL0UsIUXjYbiui5+IVsbIItbQGOlwyGV55g6IYJBgdTNXgFX0HRYZGE9ZZ9psEXqzqwLM37zvWnb5AG9g==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"dependencies": {
"tsparticles-engine": "^2.12.0",
"tsparticles-move-base": "^2.12.0",
"tsparticles-shape-circle": "^2.12.0",
"tsparticles-updater-color": "^2.12.0",
"tsparticles-updater-opacity": "^2.12.0",
"tsparticles-updater-out-modes": "^2.12.0",
"tsparticles-updater-size": "^2.12.0"
}
},
"node_modules/tsparticles-engine": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-engine/-/tsparticles-engine-2.12.0.tgz",
"integrity": "sha512-ZjDIYex6jBJ4iMc9+z0uPe7SgBnmb6l+EJm83MPIsOny9lPpetMsnw/8YJ3xdxn8hV+S3myTpTN1CkOVmFv0QQ==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"hasInstallScript": true
},
"node_modules/tsparticles-interaction-external-attract": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-external-attract/-/tsparticles-interaction-external-attract-2.12.0.tgz",
"integrity": "sha512-0roC6D1QkFqMVomcMlTaBrNVjVOpyNzxIUsjMfshk2wUZDAvTNTuWQdUpmsLS4EeSTDN3rzlGNnIuuUQqyBU5w==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-interaction-external-bounce": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-external-bounce/-/tsparticles-interaction-external-bounce-2.12.0.tgz",
"integrity": "sha512-MMcqKLnQMJ30hubORtdq+4QMldQ3+gJu0bBYsQr9BsThsh8/V0xHc1iokZobqHYVP5tV77mbFBD8Z7iSCf0TMQ==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-interaction-external-bubble": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-external-bubble/-/tsparticles-interaction-external-bubble-2.12.0.tgz",
"integrity": "sha512-5kImCSCZlLNccXOHPIi2Yn+rQWTX3sEa/xCHwXW19uHxtILVJlnAweayc8+Zgmb7mo0DscBtWVFXHPxrVPFDUA==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-interaction-external-connect": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-external-connect/-/tsparticles-interaction-external-connect-2.12.0.tgz",
"integrity": "sha512-ymzmFPXz6AaA1LAOL5Ihuy7YSQEW8MzuSJzbd0ES13U8XjiU3HlFqlH6WGT1KvXNw6WYoqrZt0T3fKxBW3/C3A==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-interaction-external-grab": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-external-grab/-/tsparticles-interaction-external-grab-2.12.0.tgz",
"integrity": "sha512-iQF/A947hSfDNqAjr49PRjyQaeRkYgTYpfNmAf+EfME8RsbapeP/BSyF6mTy0UAFC0hK2A2Hwgw72eT78yhXeQ==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-interaction-external-pause": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-external-pause/-/tsparticles-interaction-external-pause-2.12.0.tgz",
"integrity": "sha512-4SUikNpsFROHnRqniL+uX2E388YTtfRWqqqZxRhY0BrijH4z04Aii3YqaGhJxfrwDKkTQlIoM2GbFT552QZWjw==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-interaction-external-push": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-external-push/-/tsparticles-interaction-external-push-2.12.0.tgz",
"integrity": "sha512-kqs3V0dgDKgMoeqbdg+cKH2F+DTrvfCMrPF1MCCUpBCqBiH+TRQpJNNC86EZYHfNUeeLuIM3ttWwIkk2hllR/Q==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-interaction-external-remove": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-external-remove/-/tsparticles-interaction-external-remove-2.12.0.tgz",
"integrity": "sha512-2eNIrv4m1WB2VfSVj46V2L/J9hNEZnMgFc+A+qmy66C8KzDN1G8aJUAf1inW8JVc0lmo5+WKhzex4X0ZSMghBg==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-interaction-external-repulse": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-external-repulse/-/tsparticles-interaction-external-repulse-2.12.0.tgz",
"integrity": "sha512-rSzdnmgljeBCj5FPp4AtGxOG9TmTsK3AjQW0vlyd1aG2O5kSqFjR+FuT7rfdSk9LEJGH5SjPFE6cwbuy51uEWA==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-interaction-external-slow": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-external-slow/-/tsparticles-interaction-external-slow-2.12.0.tgz",
"integrity": "sha512-2IKdMC3om7DttqyroMtO//xNdF0NvJL/Lx7LDo08VpfTgJJozxU+JAUT8XVT7urxhaDzbxSSIROc79epESROtA==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-interaction-particles-attract": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-particles-attract/-/tsparticles-interaction-particles-attract-2.12.0.tgz",
"integrity": "sha512-Hl8qwuwF9aLq3FOkAW+Zomu7Gb8IKs6Y3tFQUQScDmrrSCaeRt2EGklAiwgxwgntmqzL7hbMWNx06CHHcUQKdQ==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-interaction-particles-collisions": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-particles-collisions/-/tsparticles-interaction-particles-collisions-2.12.0.tgz",
"integrity": "sha512-Se9nPWlyPxdsnHgR6ap4YUImAu3W5MeGKJaQMiQpm1vW8lSMOUejI1n1ioIaQth9weKGKnD9rvcNn76sFlzGBA==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-interaction-particles-links": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-interaction-particles-links/-/tsparticles-interaction-particles-links-2.12.0.tgz",
"integrity": "sha512-e7I8gRs4rmKfcsHONXMkJnymRWpxHmeaJIo4g2NaDRjIgeb2AcJSWKWZvrsoLnm7zvaf/cMQlbN6vQwCixYq3A==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-move-base": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-move-base/-/tsparticles-move-base-2.12.0.tgz",
"integrity": "sha512-oSogCDougIImq+iRtIFJD0YFArlorSi8IW3HD2gO3USkH+aNn3ZqZNTqp321uB08K34HpS263DTbhLHa/D6BWw==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-move-parallax": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-move-parallax/-/tsparticles-move-parallax-2.12.0.tgz",
"integrity": "sha512-58CYXaX8Ih5rNtYhpnH0YwU4Ks7gVZMREGUJtmjhuYN+OFr9FVdF3oDIJ9N6gY5a5AnAKz8f5j5qpucoPRcYrQ==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-particles.js": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-particles.js/-/tsparticles-particles.js-2.12.0.tgz",
"integrity": "sha512-LyOuvYdhbUScmA4iDgV3LxA0HzY1DnOwQUy3NrPYO393S2YwdDjdwMod6Btq7EBUjg9FVIh+sZRizgV5elV2dg==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-plugin-easing-quad": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-plugin-easing-quad/-/tsparticles-plugin-easing-quad-2.12.0.tgz",
"integrity": "sha512-2mNqez5pydDewMIUWaUhY5cNQ80IUOYiujwG6qx9spTq1D6EEPLbRNAEL8/ecPdn2j1Um3iWSx6lo340rPkv4Q==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-shape-circle": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-shape-circle/-/tsparticles-shape-circle-2.12.0.tgz",
"integrity": "sha512-L6OngbAlbadG7b783x16ns3+SZ7i0SSB66M8xGa5/k+YcY7zm8zG0uPt1Hd+xQDR2aNA3RngVM10O23/Lwk65Q==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-shape-image": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-shape-image/-/tsparticles-shape-image-2.12.0.tgz",
"integrity": "sha512-iCkSdUVa40DxhkkYjYuYHr9MJGVw+QnQuN5UC+e/yBgJQY+1tQL8UH0+YU/h0GHTzh5Sm+y+g51gOFxHt1dj7Q==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-shape-line": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-shape-line/-/tsparticles-shape-line-2.12.0.tgz",
"integrity": "sha512-RcpKmmpKlk+R8mM5wA2v64Lv1jvXtU4SrBDv3vbdRodKbKaWGGzymzav1Q0hYyDyUZgplEK/a5ZwrfrOwmgYGA==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-shape-polygon": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-shape-polygon/-/tsparticles-shape-polygon-2.12.0.tgz",
"integrity": "sha512-5YEy7HVMt1Obxd/jnlsjajchAlYMr9eRZWN+lSjcFSH6Ibra7h59YuJVnwxOxAobpijGxsNiBX0PuGQnB47pmA==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-shape-square": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-shape-square/-/tsparticles-shape-square-2.12.0.tgz",
"integrity": "sha512-33vfajHqmlODKaUzyPI/aVhnAOT09V7nfEPNl8DD0cfiNikEuPkbFqgJezJuE55ebtVo7BZPDA9o7GYbWxQNuw==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-shape-star": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-shape-star/-/tsparticles-shape-star-2.12.0.tgz",
"integrity": "sha512-4sfG/BBqm2qBnPLASl2L5aBfCx86cmZLXeh49Un+TIR1F5Qh4XUFsahgVOG0vkZQa+rOsZPEH04xY5feWmj90g==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-shape-text": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-shape-text/-/tsparticles-shape-text-2.12.0.tgz",
"integrity": "sha512-v2/FCA+hyTbDqp2ymFOe97h/NFb2eezECMrdirHWew3E3qlvj9S/xBibjbpZva2gnXcasBwxn0+LxKbgGdP0rA==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-slim": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-slim/-/tsparticles-slim-2.12.0.tgz",
"integrity": "sha512-27w9aGAAAPKHvP4LHzWFpyqu7wKyulayyaZ/L6Tuuejy4KP4BBEB4rY5GG91yvAPsLtr6rwWAn3yS+uxnBDpkA==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"dependencies": {
"tsparticles-basic": "^2.12.0",
"tsparticles-engine": "^2.12.0",
"tsparticles-interaction-external-attract": "^2.12.0",
"tsparticles-interaction-external-bounce": "^2.12.0",
"tsparticles-interaction-external-bubble": "^2.12.0",
"tsparticles-interaction-external-connect": "^2.12.0",
"tsparticles-interaction-external-grab": "^2.12.0",
"tsparticles-interaction-external-pause": "^2.12.0",
"tsparticles-interaction-external-push": "^2.12.0",
"tsparticles-interaction-external-remove": "^2.12.0",
"tsparticles-interaction-external-repulse": "^2.12.0",
"tsparticles-interaction-external-slow": "^2.12.0",
"tsparticles-interaction-particles-attract": "^2.12.0",
"tsparticles-interaction-particles-collisions": "^2.12.0",
"tsparticles-interaction-particles-links": "^2.12.0",
"tsparticles-move-base": "^2.12.0",
"tsparticles-move-parallax": "^2.12.0",
"tsparticles-particles.js": "^2.12.0",
"tsparticles-plugin-easing-quad": "^2.12.0",
"tsparticles-shape-circle": "^2.12.0",
"tsparticles-shape-image": "^2.12.0",
"tsparticles-shape-line": "^2.12.0",
"tsparticles-shape-polygon": "^2.12.0",
"tsparticles-shape-square": "^2.12.0",
"tsparticles-shape-star": "^2.12.0",
"tsparticles-shape-text": "^2.12.0",
"tsparticles-updater-color": "^2.12.0",
"tsparticles-updater-life": "^2.12.0",
"tsparticles-updater-opacity": "^2.12.0",
"tsparticles-updater-out-modes": "^2.12.0",
"tsparticles-updater-rotate": "^2.12.0",
"tsparticles-updater-size": "^2.12.0",
"tsparticles-updater-stroke-color": "^2.12.0"
}
},
"node_modules/tsparticles-updater-color": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-updater-color/-/tsparticles-updater-color-2.12.0.tgz",
"integrity": "sha512-KcG3a8zd0f8CTiOrylXGChBrjhKcchvDJjx9sp5qpwQK61JlNojNCU35xoaSk2eEHeOvFjh0o3CXWUmYPUcBTQ==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-updater-life": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-updater-life/-/tsparticles-updater-life-2.12.0.tgz",
"integrity": "sha512-J7RWGHAZkowBHpcLpmjKsxwnZZJ94oGEL2w+wvW1/+ZLmAiFFF6UgU0rHMC5CbHJT4IPx9cbkYMEHsBkcRJ0Bw==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-updater-opacity": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-updater-opacity/-/tsparticles-updater-opacity-2.12.0.tgz",
"integrity": "sha512-YUjMsgHdaYi4HN89LLogboYcCi1o9VGo21upoqxq19yRy0hRCtx2NhH22iHF/i5WrX6jqshN0iuiiNefC53CsA==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-updater-out-modes": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-updater-out-modes/-/tsparticles-updater-out-modes-2.12.0.tgz",
"integrity": "sha512-owBp4Gk0JNlSrmp12XVEeBroDhLZU+Uq3szbWlHGSfcR88W4c/0bt0FiH5bHUqORIkw+m8O56hCjbqwj69kpOQ==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-updater-rotate": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-updater-rotate/-/tsparticles-updater-rotate-2.12.0.tgz",
"integrity": "sha512-waOFlGFmEZOzsQg4C4VSejNVXGf4dMf3fsnQrEROASGf1FCd8B6WcZau7JtXSTFw0OUGuk8UGz36ETWN72DkCw==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-updater-size": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-updater-size/-/tsparticles-updater-size-2.12.0.tgz",
"integrity": "sha512-B0yRdEDd/qZXCGDL/ussHfx5YJ9UhTqNvmS5X2rR2hiZhBAE2fmsXLeWkdtF2QusjPeEqFDxrkGiLOsh6poqRA==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tsparticles-updater-stroke-color": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/tsparticles-updater-stroke-color/-/tsparticles-updater-stroke-color-2.12.0.tgz",
"integrity": "sha512-MPou1ZDxsuVq6SN1fbX+aI5yrs6FyP2iPCqqttpNbWyL+R6fik1rL0ab/x02B57liDXqGKYomIbBQVP3zUTW1A==",
"deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name",
"dependencies": {
"tsparticles-engine": "^2.12.0"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",

View File

@@ -18,7 +18,9 @@
"react-github-btn": "^1.4.0",
"react-i18next": "^15.6.1",
"react-icons": "^5.5.0",
"react-particles": "^2.12.2",
"sass": "^1.86.0",
"tsparticles-slim": "^2.12.0",
"typescript": "^5.8.3",
"web-vitals": "^5.1.0",
"yarn": "^1.22.22"

View File

@@ -10,6 +10,12 @@ import Project from "./components/Project.tsx";
import Footer from "./components/Footer.tsx";
import CV from "./components/CV.tsx";
import Menu from "./components/Menu.tsx";
import LoadingScreen from "./components/LoadingScreen.tsx";
import ParticlesBackground from "./components/ParticlesBackground.tsx";
import HomelabSection from "./components/HomelabSection.tsx";
import GitHubStatsSection from "./components/GitHubStatsSection.tsx";
import ContactSection from "./components/ContactSection.tsx";
import TimelineSection from "./components/TimelineSection.tsx";
import data from "./assets/DATA.ts";
import { useTranslation } from "react-i18next";
import i18n from './i18n.js';
@@ -17,6 +23,8 @@ import {createRoot} from "react-dom/client";
function App() {
const [theme, setTheme] = useState("light");
const [isLoading, setIsLoading] = useState(true);
const [showContent, setShowContent] = useState(false);
useEffect(() => {
// Detect if the user has their system in dark mode
@@ -33,15 +41,27 @@ function App() {
document.documentElement.classList.toggle("dark");
}
const handleLoadingComplete = () => {
setTimeout(() => {
setIsLoading(false);
setTimeout(() => setShowContent(true), 100);
}, 500);
}
useTranslation();
const toggleLanguage = () => {
i18n.changeLanguage(i18n.language === "fr" ? "en" : "fr");
}
if (isLoading) {
return <LoadingScreen onLoadingComplete={handleLoadingComplete} />;
}
return (
<div className="min-h-screen py-10 bg-gray-100 dark:bg-gray-900 m-0" data-testid="root">
<div className="mobile-margin">
<ParticlesBackground isDark={theme === "dark"} />
<div className={`mobile-margin transition-all duration-1000 ${showContent ? 'animate-fadeInUp' : 'opacity-0'}`}>
<Menu />
<div data-aos="face-down" data-aos-duration="800" id="top">
<Card name={data.name} title={data.title} social={data.social} />
@@ -50,8 +70,16 @@ function App() {
<div data-aos="fade-up" data-aos-duration="800" data-aos-delay="400">
<About />
<Skills skills={data.skills} />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="experience" />
<TimelineSection />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="homelab" />
<HomelabSection />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="github" />
<GitHubStatsSection />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="projects" />
<Project projects={data.projects} />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="contact" />
<ContactSection />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="cv" />
<CV />
<Footer />

View File

@@ -95,7 +95,7 @@ const Felix = {
{
title: "Github NTFY",
description: "Projet de notification pour les releases github et dockerhub qui envoie des notifications sur ntfy, gotify et discord",
tags: ["Python", "Docker", "Github Actions"],
tags: ["Python", "Rust", "Nuxt", "Docker", "Github Actions"],
link: "https://github.com/BreizhHardware/ntfy_alerts"
},
{
@@ -103,6 +103,12 @@ const Felix = {
description: "Projet de jeu de simulation de comportement de banc de poisson en C++ avec la librairie SLD2, avec support du multijoueur.",
tags: ["C++", "SDL2", "Multiplayer"],
link: "https://github.com/BreizhHardware/bloubloulespoissons"
},
{
title: "Alternance Horoquartz",
description: "Développement d'un système de mise à jour pour les produits Horoquartz",
tags: ["Node.js", "Go", "PostgreSQL", "Docker", "Kubernetes", "Azure"],
link: "https://www.horoquartz.com/"
}
]
};

View File

@@ -0,0 +1,236 @@
import React, { useState } from 'react';
import { FaPaperPlane, FaUser, FaEnvelope, FaCommentDots, FaCheckCircle, FaSpinner } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
interface FormData {
name: string;
email: string;
subject: string;
message: string;
}
interface FormStatus {
type: 'success' | 'error' | 'loading' | null;
message: string;
}
const ContactSection: React.FC = () => {
const { t } = useTranslation();
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
subject: '',
message: ''
});
const [status, setStatus] = useState<FormStatus>({ type: null, message: '' });
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus({ type: 'loading', message: 'Envoi en cours...' });
// Simulation d'envoi (en production, remplacer par un vrai service)
setTimeout(() => {
setStatus({
type: 'success',
message: 'Message envoyé avec succès ! Je vous répondrai dans les plus brefs délais.'
});
setFormData({ name: '', email: '', subject: '', message: '' });
}, 2000);
};
const isFormValid = formData.name && formData.email && formData.subject && formData.message;
return (
<div className="max-w-4xl mx-auto mt-16" id="contact">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4">
📧 Contact
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
Une question ? Un projet ? N'hésitez pas à me contacter !
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Informations de contact */}
<div className="space-y-8">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-6">
Informations de contact
</h3>
<div className="space-y-4">
<div className="flex items-center space-x-3">
<FaEnvelope className="text-blue-500 text-lg" />
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">Email</p>
<p className="text-gray-800 dark:text-gray-200">felix.marquet@isen-ouest.yncrea.fr</p>
</div>
</div>
<div className="flex items-center space-x-3">
<FaUser className="text-green-500 text-lg" />
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">Statut</p>
<p className="text-gray-800 dark:text-gray-200">Étudiant ISEN - Alternant chez Horoquartz</p>
</div>
</div>
<div className="flex items-center space-x-3">
<FaCommentDots className="text-purple-500 text-lg" />
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">Réponse</p>
<p className="text-gray-800 dark:text-gray-200">Généralement sous 24h</p>
</div>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-4">
Sujets d'intérêt
</h3>
<div className="flex flex-wrap gap-2">
{[
'Développement Web',
'Administration Système',
'DevOps',
'Alternance',
'Projets Open Source',
'Homelab',
'Collaboration',
'Stage/Emploi'
].map((subject, index) => (
<span
key={index}
className="px-3 py-1 text-sm bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full animate-fadeInUp"
style={{ animationDelay: `${index * 100}ms` }}
>
{subject}
</span>
))}
</div>
</div>
</div>
{/* Formulaire de contact */}
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-6">
Envoyez-moi un message
</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Nom complet
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-200 transition-all"
placeholder="Votre nom"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-200 transition-all"
placeholder="votre.email@exemple.com"
required
/>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Sujet
</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-200 transition-all"
placeholder="Objet de votre message"
required
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Message
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleInputChange}
rows={5}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-200 transition-all resize-none"
placeholder="Votre message..."
required
/>
</div>
<button
type="submit"
disabled={!isFormValid || status.type === 'loading'}
className={`w-full flex items-center justify-center space-x-2 py-3 px-4 rounded-md text-white font-medium transition-all ${
isFormValid && status.type !== 'loading'
? 'bg-blue-600 hover:bg-blue-700 active:transform active:scale-95'
: 'bg-gray-400 cursor-not-allowed'
}`}
>
{status.type === 'loading' ? (
<>
<FaSpinner className="animate-spin" />
<span>Envoi en cours...</span>
</>
) : (
<>
<FaPaperPlane />
<span>Envoyer le message</span>
</>
)}
</button>
</form>
{/* Status message */}
{status.type && (
<div className={`mt-4 p-3 rounded-md flex items-center space-x-2 animate-fadeInUp ${
status.type === 'success' ? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200' :
status.type === 'error' ? 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200' :
'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
}`}>
{status.type === 'success' && <FaCheckCircle />}
{status.type === 'loading' && <FaSpinner className="animate-spin" />}
<span className="text-sm">{status.message}</span>
</div>
)}
</div>
</div>
</div>
);
};
export default ContactSection;

View File

@@ -0,0 +1,160 @@
import React, { useState, useEffect } from 'react';
import { FaGithub, FaStar, FaCodeBranch, FaCode, FaCalendarAlt } from 'react-icons/fa';
interface GitHubStats {
totalRepos: number;
totalStars: number;
totalForks: number;
totalCommits: number;
languages: { [key: string]: number };
}
const GitHubStatsSection: React.FC = () => {
const [stats, setStats] = useState<GitHubStats | null>(null);
const [loading, setLoading] = useState(true);
// Données statiques en attendant l'API GitHub
const mockStats: GitHubStats = {
totalRepos: 25,
totalStars: 47,
totalForks: 12,
totalCommits: 580,
languages: {
'C++': 35,
'Python': 25,
'JavaScript': 20,
'C': 15,
'Go': 5
}
};
useEffect(() => {
// Simulation du chargement
setTimeout(() => {
setStats(mockStats);
setLoading(false);
}, 1500);
}, []);
const topLanguages = stats ? Object.entries(stats.languages).sort((a, b) => b[1] - a[1]).slice(0, 5) : [];
if (loading) {
return (
<div className="max-w-4xl mx-auto mt-16">
<div className="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-lg">
<div className="animate-pulse">
<div className="h-8 bg-gray-300 dark:bg-gray-600 rounded w-1/3 mx-auto mb-6"></div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-20 bg-gray-300 dark:bg-gray-600 rounded"></div>
))}
</div>
</div>
</div>
</div>
);
}
return (
<div className="max-w-4xl mx-auto mt-16">
<div className="text-center mb-8">
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4 flex items-center justify-center gap-3">
<FaGithub className="text-gray-800 dark:text-gray-200" />
Statistiques GitHub
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
Mon activité open source et contributions
</p>
</div>
{stats && (
<div className="space-y-8">
{/* Stats principales */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover text-center">
<FaCode className="text-3xl text-blue-500 mx-auto mb-3" />
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">{stats.totalRepos}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Repositories</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover text-center">
<FaStar className="text-3xl text-yellow-500 mx-auto mb-3" />
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">{stats.totalStars}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Stars</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover text-center">
<FaCodeBranch className="text-3xl text-green-500 mx-auto mb-3" />
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">{stats.totalForks}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Forks</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover text-center">
<FaCalendarAlt className="text-3xl text-purple-500 mx-auto mb-3" />
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">{stats.totalCommits}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Commits</p>
</div>
</div>
{/* Langages les plus utilisés */}
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-6 text-center">
Langages les plus utilisés
</h3>
<div className="space-y-4">
{topLanguages.map(([language, percentage], index) => (
<div key={language} className="animate-slideInLeft" style={{ animationDelay: `${index * 100}ms` }}>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">{language}</span>
<span className="text-sm text-gray-500 dark:text-gray-400">{percentage}%</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full progress-fill transition-all duration-1000 ease-out"
style={{
width: `${percentage}%`,
animationDelay: `${index * 200}ms`
}}
/>
</div>
</div>
))}
</div>
</div>
{/* GitHub Graph simulé */}
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-6 text-center">
Activité des 12 derniers mois
</h3>
<div className="grid grid-cols-12 gap-1 max-w-3xl mx-auto">
{Array.from({ length: 365 }, (_, i) => (
<div
key={i}
className={`w-3 h-3 rounded-sm ${
Math.random() > 0.7
? 'bg-green-500'
: Math.random() > 0.5
? 'bg-green-300'
: Math.random() > 0.3
? 'bg-green-200'
: 'bg-gray-200 dark:bg-gray-700'
} animate-fadeInUp`}
style={{ animationDelay: `${i * 2}ms` }}
title={`Jour ${i + 1}`}
/>
))}
</div>
<div className="text-center mt-4">
<span className="text-sm text-gray-500 dark:text-gray-400">
Plus sombre = Plus d'activité
</span>
</div>
</div>
</div>
)}
</div>
);
};
export default GitHubStatsSection;

View File

@@ -0,0 +1,216 @@
import React, { useState } from 'react';
import { FaServer, FaDocker, FaLinux, FaNetworkWired, FaHdd, FaMicrochip } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
interface ServerSpec {
name: string;
model: string;
cpu: string;
ram: string;
storage: string;
os: string;
services: string[];
status: 'online' | 'offline' | 'maintenance';
}
const HomelabSection: React.FC = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<'overview' | 'servers' | 'services'>('overview');
const servers: ServerSpec[] = [
{
name: "DELL T320",
model: "PowerEdge T320",
cpu: "Intel Xeon E5-2400 series",
ram: "32GB DDR3",
storage: "4x 1TB HDD RAID 10",
os: "Proxmox VE",
services: ["Proxmox", "Docker", "VM Management", "Backup Server"],
status: 'online'
},
{
name: "DELL T330",
model: "PowerEdge T330",
cpu: "Intel Xeon E3-1200 series",
ram: "64GB DDR4",
storage: "6x 2TB HDD + 2x 500GB SSD",
os: "Proxmox VE",
services: ["Media Server", "Development Environment", "CI/CD", "Monitoring"],
status: 'online'
}
];
const services = [
{ name: "Proxmox Cluster", icon: <FaServer />, description: "Virtualisation et orchestration" },
{ name: "Docker Swarm", icon: <FaDocker />, description: "Conteneurisation des services" },
{ name: "Monitoring Stack", icon: <FaNetworkWired />, description: "Grafana + Prometheus + Alertmanager" },
{ name: "Backup Solution", icon: <FaHdd />, description: "Sauvegarde automatisée 3-2-1" },
{ name: "Development Lab", icon: <FaMicrochip />, description: "Environnements de développement isolés" },
{ name: "Media Center", icon: <FaLinux />, description: "Streaming et gestion multimédia" }
];
const getStatusColor = (status: string) => {
switch (status) {
case 'online': return 'bg-green-500';
case 'offline': return 'bg-red-500';
case 'maintenance': return 'bg-yellow-500';
default: return 'bg-gray-500';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'online': return 'En ligne';
case 'offline': return 'Hors ligne';
case 'maintenance': return 'Maintenance';
default: return 'Inconnu';
}
};
return (
<div className="max-w-6xl mx-auto mt-16">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4">
🏠 Mon Homelab
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
Infrastructure personnelle et laboratoire de développement
</p>
</div>
{/* Navigation tabs */}
<div className="flex justify-center mb-8">
<div className="bg-gray-200 dark:bg-gray-700 rounded-lg p-1">
{['overview', 'servers', 'services'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab as any)}
className={`px-6 py-2 rounded-md text-sm font-medium transition-all ${
activeTab === tab
? 'bg-white dark:bg-gray-600 text-gray-800 dark:text-gray-200 shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
}`}
>
{tab === 'overview' ? 'Vue d\'ensemble' : tab === 'servers' ? 'Serveurs' : 'Services'}
</button>
))}
</div>
</div>
{/* Content based on active tab */}
<div className="animate-fadeInUp">
{activeTab === 'overview' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
<div className="flex items-center justify-between">
<div>
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">2</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Serveurs</p>
</div>
<FaServer className="text-blue-500 text-2xl" />
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
<div className="flex items-center justify-between">
<div>
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">96GB</p>
<p className="text-sm text-gray-600 dark:text-gray-400">RAM Total</p>
</div>
<FaMicrochip className="text-green-500 text-2xl" />
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
<div className="flex items-center justify-between">
<div>
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">16TB</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Stockage</p>
</div>
<FaHdd className="text-purple-500 text-2xl" />
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
<div className="flex items-center justify-between">
<div>
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">24/7</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Uptime</p>
</div>
<FaNetworkWired className="text-orange-500 text-2xl" />
</div>
</div>
</div>
)}
{activeTab === 'servers' && (
<div className="space-y-6">
{servers.map((server, index) => (
<div key={index} className="bg-white dark:bg-gray-800 rounded-lg shadow-lg card-hover overflow-hidden">
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<FaServer className="text-2xl text-blue-500" />
<div>
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200">{server.name}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">{server.model}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<div className={`w-3 h-3 rounded-full ${getStatusColor(server.status)}`}></div>
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
{getStatusText(server.status)}
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">CPU</p>
<p className="text-sm text-gray-800 dark:text-gray-200">{server.cpu}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">RAM</p>
<p className="text-sm text-gray-800 dark:text-gray-200">{server.ram}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">Stockage</p>
<p className="text-sm text-gray-800 dark:text-gray-200">{server.storage}</p>
</div>
</div>
<div>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Services</p>
<div className="flex flex-wrap gap-2">
{server.services.map((service, idx) => (
<span key={idx} className="px-3 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full">
{service}
</span>
))}
</div>
</div>
</div>
</div>
))}
</div>
)}
{activeTab === 'services' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{services.map((service, index) => (
<div key={index} className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
<div className="flex items-center space-x-3 mb-3">
<div className="text-2xl text-blue-500">{service.icon}</div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200">{service.name}</h3>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">{service.description}</p>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default HomelabSection;

View File

@@ -0,0 +1,57 @@
import React, { useState, useEffect } from 'react';
interface LoadingScreenProps {
onLoadingComplete: () => void;
}
const LoadingScreen: React.FC<LoadingScreenProps> = ({ onLoadingComplete }) => {
const [displayText, setDisplayText] = useState('');
const [showCursor, setShowCursor] = useState(true);
const fullText = "Félix MARQUET";
const [isComplete, setIsComplete] = useState(false);
useEffect(() => {
let currentIndex = 0;
const typingInterval = setInterval(() => {
if (currentIndex <= fullText.length) {
setDisplayText(fullText.substring(0, currentIndex));
currentIndex++;
} else {
clearInterval(typingInterval);
setIsComplete(true);
setTimeout(() => {
onLoadingComplete();
}, 1000);
}
}, 150);
return () => clearInterval(typingInterval);
}, [fullText, onLoadingComplete]);
useEffect(() => {
const cursorInterval = setInterval(() => {
setShowCursor(prev => !prev);
}, 500);
return () => clearInterval(cursorInterval);
}, []);
return (
<div className="fixed inset-0 bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 flex items-center justify-center z-50 transition-all duration-1000">
<div className="text-center">
<div className="text-4xl md:text-6xl font-bold text-white mb-8">
{displayText}
{showCursor && !isComplete && <span className="animate-pulse">|</span>}
</div>
<div className="flex space-x-2 justify-center">
<div className="w-3 h-3 bg-white rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
<div className="w-3 h-3 bg-white rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
<div className="w-3 h-3 bg-white rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
</div>
<p className="text-white/80 mt-4 text-lg">Chargement du portfolio...</p>
</div>
</div>
);
};
export default LoadingScreen;

View File

@@ -5,12 +5,15 @@ import {useTranslation} from "react-i18next";
function Menu() {
const { t } = useTranslation();
return (
<nav className="fixed top-0 w-full bg-gray-100 dark:bg-gray-900 z-50 shadow-md shadow-gray-800 dark:shadow-gray-200 menu-hidden">
<nav className="fixed top-0 w-full bg-gray-100 dark:bg-gray-900 z-50 shadow-md shadow-gray-800 dark:shadow-gray-200 menu-hidden backdrop-blur-sm">
<ul className="flex justify-around">
<li><a href="#top" className="text-gray-800 dark:text-gray-200">Félix MARQUET</a></li>
<li><a href="#about" className="text-gray-800 dark:text-gray-200">{t('nav.about')}</a></li>
<li><a href="#projects" className="text-gray-800 dark:text-gray-200">{t('nav.projects')}</a></li>
<li><a href="#cv" className="text-gray-800 dark:text-gray-200">{t('nav.cv')}</a></li>
<li><a href="#top" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">Félix MARQUET</a></li>
<li><a href="#about" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.about')}</a></li>
<li><a href="#experience" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">Expérience</a></li>
<li><a href="#homelab" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">Homelab</a></li>
<li><a href="#projects" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.projects')}</a></li>
<li><a href="#contact" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">Contact</a></li>
<li><a href="#cv" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.cv')}</a></li>
</ul>
</nav>
)

View File

@@ -0,0 +1,29 @@
import React from 'react';
interface ParticlesBackgroundProps {
isDark: boolean;
}
const ParticlesBackground: React.FC<ParticlesBackgroundProps> = ({ isDark }) => {
return (
<div className="absolute inset-0 -z-10">
{/* Placeholder pour les particules - nous utiliserons une version CSS simple */}
<div className="particles-container">
{Array.from({ length: 20 }).map((_, i) => (
<div
key={i}
className={`absolute w-1 h-1 rounded-full ${isDark ? 'bg-white' : 'bg-gray-600'} animate-float`}
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 5}s`,
animationDuration: `${3 + Math.random() * 4}s`,
}}
/>
))}
</div>
</div>
);
};
export default ParticlesBackground;

View File

@@ -1,47 +0,0 @@
import React from "react";
interface ProgressBarProps {
bgcolor: string;
completed: number;
className?: string;
}
const ProgressBar = (props: ProgressBarProps) => {
const { bgcolor, completed } = props;
const completedPercentage = completed + "%";
const containerStyles = {
height: 30,
width: '100%',
backgroundColor: "#cacdd2",
borderRadius: 50,
borderWidth: 2,
marginTop: 5
}
const fillerStyles = {
height: '100%',
width: completedPercentage,
backgroundColor: bgcolor,
borderRadius: 'inherit',
textAlign: 'right' as 'right',
transition: 'width 1s ease-in-out',
}
const labelStyles = {
padding: 5,
color: 'white',
fontWeight: 'bold'
}
return (
<div style={containerStyles}>
<div style={fillerStyles}>
<span style={labelStyles}>{`${completed}%`}</span>
</div>
</div>
);
};
export default ProgressBar;

View File

@@ -1,6 +1,6 @@
// @ts-ignore
import React from 'react';
import { FaExternalLinkAlt} from "react-icons/fa";
import React, { useState } from 'react';
import { FaExternalLinkAlt, FaTimes, FaGithub, FaCalendarAlt, FaCode} from "react-icons/fa";
import GitHubButton from 'react-github-btn';
import {useTranslation} from "react-i18next";
@@ -8,40 +8,195 @@ interface Project {
title: string;
link: string;
tags: string[];
description: string;
}
interface ProjectCardProps {
project: Project;
}
const ProjectCard = ({ project }: ProjectCardProps) => {
const ProjectModal: React.FC<{ project: Project; isOpen: boolean; onClose: () => void }> = ({
project,
isOpen,
onClose
}) => {
const { t } = useTranslation();
const { title, link, tags } = project;
if (!isOpen) return null;
return (
<div className="group w-full sm:w-5/12 m-4 mx-auto p-6 rounded-xl border-2 border-gray-300 dark:border-gray-700">
<a href={link}>
<h1 className="text-xl text-center font-bold dark:text-gray-200">
{title}{" "}
<FaExternalLinkAlt className="inline align-baseline" />
</h1>
</a>
<hr className="my-4" />
<p className="dark:text-gray-300">{t(`projects.${project.title}.description`)}</p>
<div className="mt-4 mb-8 flex flex-wrap justify-center items-center gap-2">
{project.tags.map((tag) => (
<div className="px-4 py-1 border-2 rounded-full dark:text-gray-300">{tag}</div>
))}
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={onClose}>
<div
className="bg-white dark:bg-gray-800 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto animate-fadeInUp"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-2xl font-bold text-gray-800 dark:text-gray-200">
{project.title}
</h2>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
>
<FaTimes size={24} />
</button>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{/* Description détaillée */}
<div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center">
<FaCode className="mr-2" />
Description du projet
</h3>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
{t(`projects.${project.title}.description`)}
</p>
</div>
{/* Technologies */}
<div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3">
Technologies utilisées
</h3>
<div className="flex flex-wrap gap-2">
{project.tags.map((tag, index) => (
<span
key={index}
className="px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full text-sm animate-slideInLeft"
style={{ animationDelay: `${index * 100}ms` }}
>
{tag}
</span>
))}
</div>
</div>
{/* Détails spécifiques au projet */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-2">
Objectifs du projet
</h4>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li> Développement d'une solution complète</li>
<li> Application des meilleures pratiques</li>
<li> Travail en équipe et gestion de projet</li>
</ul>
</div>
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-2">
Compétences acquises
</h4>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li> Architecture et conception</li>
<li> Développement full-stack</li>
<li> Tests et déploiement</li>
</ul>
</div>
</div>
{/* Actions */}
<div className="flex flex-wrap gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<a
href={project.link}
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<FaExternalLinkAlt />
<span>Voir le projet</span>
</a>
{(project.title !== "MercuryCloud" && project.title !== "Alternance Horoquartz") && (
<div className="flex space-x-2">
<GitHubButton
href={project.link}
data-color-scheme="no-preference: light; light: light; dark: dark;"
data-icon="octicon-star"
data-size="large"
data-show-count="true"
>
Star
</GitHubButton>
<GitHubButton
href={project.link + "/fork"}
data-color-scheme="no-preference: light; light: light; dark: dark;"
data-icon="octicon-repo-forked"
data-size="large"
data-show-count="true"
>
Fork
</GitHubButton>
</div>
)}
</div>
</div>
</div>
{title !== "MercuryCloud" && (
<div className="w-full text-center">
<GitHubButton href={link} data-color-scheme="no-preference: light; light: light; dark: dark;" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star ntkme/github-buttons on GitHub">Star</GitHubButton>
{" "}
<GitHubButton href={link + "/fork"} data-color-scheme="no-preference: light; light: light; dark: dark;" data-icon="octicon-repo-forked" data-size="large" data-show-count="true" aria-label="Fork ntkme/github-buttons on GitHub">Fork</GitHubButton>
</div>
)}
</div>
);
};
const ProjectCard = ({ project }: ProjectCardProps) => {
const { t } = useTranslation();
const { title, link, tags } = project;
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<>
<div className="group w-full sm:w-5/12 m-4 mx-auto p-6 rounded-xl border-2 border-gray-300 dark:border-gray-700 card-hover cursor-pointer transition-all duration-300"
onClick={() => setIsModalOpen(true)}
>
<div className="flex items-center justify-between mb-4">
<h1 className="text-xl font-bold dark:text-gray-200 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{title}
</h1>
<FaExternalLinkAlt className="text-gray-400 group-hover:text-blue-500 transition-colors" />
</div>
<hr className="my-4" />
<p className="dark:text-gray-300 text-sm line-clamp-3 mb-4">
{t(`projects.${project.title}.description`)}
</p>
<div className="flex flex-wrap gap-2 mb-4">
{project.tags.slice(0, 3).map((tag, index) => (
<span key={index} className="px-3 py-1 text-xs border-2 rounded-full dark:text-gray-300 bg-gray-50 dark:bg-gray-700 border-gray-200 dark:border-gray-600">
{tag}
</span>
))}
{project.tags.length > 3 && (
<span className="px-3 py-1 text-xs text-gray-500 dark:text-gray-400">
+{project.tags.length - 3} autres
</span>
)}
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-blue-600 dark:text-blue-400 font-medium">
Voir les détails
</span>
{(title !== "MercuryCloud" && title !== "Alternance Horoquartz") && (
<div className="flex items-center space-x-2">
<FaGithub className="text-gray-600 dark:text-gray-400" />
<span className="text-xs text-gray-500 dark:text-gray-400">Open Source</span>
</div>
)}
</div>
</div>
<ProjectModal
project={project}
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
/>
</>
);
};
export default ProjectCard;

View File

@@ -0,0 +1,220 @@
import React from 'react';
import { FaBriefcase, FaGraduationCap, FaCalendarAlt, FaMapMarkerAlt } from 'react-icons/fa';
interface TimelineItem {
id: number;
type: 'work' | 'education' | 'achievement';
title: string;
organization: string;
location: string;
startDate: string;
endDate: string;
description: string[];
technologies?: string[];
current?: boolean;
}
const TimelineSection: React.FC = () => {
const timelineData: TimelineItem[] = [
{
id: 1,
type: 'work',
title: 'Développeur - Alternant',
organization: 'Horoquartz',
location: 'Cesson-Sévigné, France',
startDate: '2024',
endDate: 'Présent',
current: true,
description: [
'Développement d\'un système de mise à jour pour les produits Horoquartz',
'Conception et implémentation d\'APIs REST avec Node.js et Go',
'Gestion des bases de données PostgreSQL',
'Déploiement avec Docker et Kubernetes sur Azure'
],
technologies: ['Node.js', 'Go', 'PostgreSQL', 'Docker', 'Kubernetes', 'Azure']
},
{
id: 2,
type: 'education',
title: 'ISEN Nantes - Cycle Ingénieur',
organization: 'Institut Supérieur de l\'Électronique et du Numérique',
location: 'Nantes, France',
startDate: '2022',
endDate: '2025',
current: true,
description: [
'Formation d\'ingénieur en informatique et nouvelles technologies',
'Spécialisation en développement logiciel et administration système',
'Projets en équipe et gestion de projet'
],
technologies: ['C', 'C++', 'Python', 'Linux', 'Réseaux']
},
{
id: 3,
type: 'work',
title: 'Support Technique & Admin VPS',
organization: 'MercuryCloud',
location: 'Remote',
startDate: '2021',
endDate: '2024',
description: [
'Support technique pour serveurs de jeu et VPS',
'Administration des services CPanel et Plesk',
'Gestion du système WHMCS pour la facturation',
'Virtualisation et maintenance des serveurs Linux'
],
technologies: ['Linux', 'Virtualisation', 'CPanel', 'Plesk', 'WHMCS']
},
{
id: 4,
type: 'achievement',
title: 'Projet Robot - Coupe de France de Robotique',
organization: 'Club Modelec ISEN',
location: 'Nantes, France',
startDate: '2023',
endDate: '2023',
description: [
'Développement du système de contrôle du robot',
'Interface utilisateur avec QT',
'Déploiement sur Raspberry Pi',
'Travail en équipe multidisciplinaire'
],
technologies: ['C++', 'QT', 'Raspberry Pi', 'Linux']
},
{
id: 5,
type: 'education',
title: 'Classes Préparatoires Intégrées',
organization: 'ISEN Nantes',
location: 'Nantes, France',
startDate: '2020',
endDate: '2022',
description: [
'Mathématiques, Physique, Informatique',
'Bases de la programmation en C',
'Introduction aux systèmes embarqués'
],
technologies: ['C', 'Mathématiques', 'Physique', 'Électronique']
}
];
const getIcon = (type: string) => {
switch (type) {
case 'work':
return <FaBriefcase className="text-blue-500" />;
case 'education':
return <FaGraduationCap className="text-green-500" />;
case 'achievement':
return <FaCalendarAlt className="text-purple-500" />;
default:
return <FaBriefcase className="text-gray-500" />;
}
};
const getBackgroundColor = (type: string) => {
switch (type) {
case 'work':
return 'bg-blue-50 dark:bg-blue-900/20 border-l-blue-500';
case 'education':
return 'bg-green-50 dark:bg-green-900/20 border-l-green-500';
case 'achievement':
return 'bg-purple-50 dark:bg-purple-900/20 border-l-purple-500';
default:
return 'bg-gray-50 dark:bg-gray-900/20 border-l-gray-500';
}
};
return (
<div className="max-w-4xl mx-auto mt-16" id="experience">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4">
🎯 Parcours & Expérience
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
Mon évolution professionnelle et académique
</p>
</div>
<div className="relative">
{/* Ligne verticale */}
<div className="absolute left-4 md:left-8 top-0 bottom-0 w-0.5 bg-gray-300 dark:bg-gray-600"></div>
<div className="space-y-8">
{timelineData.map((item, index) => (
<div
key={item.id}
className={`relative pl-12 md:pl-20 animate-fadeInUp`}
style={{ animationDelay: `${index * 200}ms` }}
>
{/* Icône */}
<div className="absolute left-1 md:left-5 w-6 h-6 bg-white dark:bg-gray-800 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center">
<div className="text-sm">
{getIcon(item.type)}
</div>
</div>
{/* Contenu */}
<div className={`bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover border-l-4 ${getBackgroundColor(item.type)}`}>
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-3">
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200">
{item.title}
</h3>
<div className="flex items-center space-x-4 text-sm text-gray-600 dark:text-gray-400 mt-2 md:mt-0">
{item.current && (
<span className="px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded-full text-xs">
En cours
</span>
)}
<span className="flex items-center space-x-1">
<FaCalendarAlt className="text-xs" />
<span>{item.startDate} - {item.endDate}</span>
</span>
</div>
</div>
<div className="flex items-center space-x-2 mb-3">
<h4 className="text-lg font-semibold text-gray-700 dark:text-gray-300">
{item.organization}
</h4>
<span className="flex items-center text-sm text-gray-500 dark:text-gray-400">
<FaMapMarkerAlt className="mr-1" />
{item.location}
</span>
</div>
<ul className="space-y-1 mb-4">
{item.description.map((desc, idx) => (
<li key={idx} className="text-gray-600 dark:text-gray-400 flex items-start">
<span className="text-blue-500 mr-2 mt-1.5"></span>
<span>{desc}</span>
</li>
))}
</ul>
{item.technologies && (
<div>
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Technologies utilisées :
</h5>
<div className="flex flex-wrap gap-2">
{item.technologies.map((tech, idx) => (
<span
key={idx}
className="px-3 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full"
>
{tech}
</span>
))}
</div>
</div>
)}
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default TimelineSection;

View File

@@ -8,8 +8,8 @@ i18n
fr: {
translation: {
"about.title": "A propos de moi",
"about.description": "Je suis étudiant en 3e année à l'ISEN Nantes en alternance chez Horoquartz. Je suis passionné par l'informatique. J'ai appris à coder en autodidacte et je suis actuellement en train d'apprendre le Rust et le Go. Je suis également passionné par l'électronique et le hardware. Je possède un homelab composé de 3serveur, un DELL T320, un DELL T330 et un Dell Precision T3610 les 3 sous proxmox.",
"card.title": "Etudiant en 3e année a l'ISEN Nantes en alternance chez Horoquartz",
"about.description": "Je suis étudiant en 4e année à l'ISEN Nantes en alternance chez Horoquartz. Je suis passionné par l'informatique. J'ai appris à coder en autodidacte et je suis actuellement en train d'apprendre le Rust et le Go. Je suis également passionné par l'électronique et le hardware. Je possède un homelab composé de 3serveur, un DELL T320, un DELL T330 et un Dell Precision T3610 les 3 sous proxmox.",
"card.title": "Etudiant en 4e année a l'ISEN Nantes en alternance chez Horoquartz",
"projects.title": "Mes projets",
"projects.Front end starter.description": "Mon starter personnel pour projet front end",
"projects.Project C - ISEN CIR 1.description": "Projet de fin de 1ere année à l'ISEN Nantes",
@@ -18,6 +18,7 @@ i18n
"projects.Projet C++ - ISEN CIR 2.description": "Projet de fin de 4e semestre à l'ISEN Nantes. Création d'un jeu de type Tower Defense en C++ avec la librairie QT6.",
"projects.Github NTFY.description": "Projet de notification pour les releases github et dockerhub qui envoie des notifications sur ntfy, gotify et discord.",
"projects.Projet C++ - ISEN CIPA 3.description": "Projet de jeu de simulation de comportement de banc de poisson en C++ avec la librairie SLD2, avec support du multijoueur.",
"projects.Alternance Horoquartz.description": "Développement d'un système de mise à jour pour les produits Horoquartz.",
"cv.title": "Mon CV",
"cv.path": "/CV-Felix-MARQUET.pdf",
"nav.about": "A propos de moi",
@@ -31,7 +32,7 @@ i18n
"skills.examples.C++.title": "Projets C++",
"skills.examples.C++.description": "Tower Defense en Qt6, simulation de banc de poissons avec SDL2 avec support du multijoueur, algorithmes de base",
"skills.examples.Admin Système.title": "Administration Système",
"skills.examples.Admin Système.description": "Configuration de 3 serveurs DELL sous Proxmox, virtualisation, maintenance des machines virtuelles",
"skills.examples.Admin Système.description": "Configuration de 3 serveurs DELL sous Proxmox, virtualisation, maintenance des machines virtuelles, deploiement d'applications dans Azure",
"skills.examples.Python.title": "Projets Python",
"skills.examples.Python.description": "Github NTFY pour les notifications des releases github et dockerhub, projets de cours",
"skills.examples.PHP.title": "Projets PHP",
@@ -39,7 +40,7 @@ i18n
"skills.examples.HTML/CSS.title": "Développement Front-end",
"skills.examples.HTML/CSS.description": "Front-end starter personnalisé, projet de base, divers projets web",
"skills.examples.JS/TS.title": "JavaScript/TypeScript",
"skills.examples.JS/TS.description": "Développement React, ce portfolio, api NodeJS avec Express, Création d'une application mobile ISEN Orbit avec React Native, projets professionnels",
"skills.examples.JS/TS.description": "Développement React, ce portfolio, api NodeJS avec Express, Création d'une application mobile Studysen avec React Native, projets professionnels",
"skills.examples.Linux.title": "Administration Linux",
"skills.examples.Linux.description": "Configuration de serveurs, administration système, Ansible playbooks",
"skills.examples.Go.title": "Projets Go",
@@ -49,15 +50,15 @@ i18n
"skills.examples.Rust.title": "Projets Rust",
"skills.examples.Rust.description": "Apprentissage du langage, création d'une api REST avec Ntex, récriture de l'api github NTFY en Rust",
"skills.examples.React.title": "Développement React",
"skills.examples.React.description": "Ce portfolio, site web de Modelec, application mobile ISEN Orbit, projets professionnels",
"skills.examples.React.description": "Ce portfolio, site web de Modelec, application mobile Studysen, projets professionnels",
},
},
en: {
translation: {
"cv.title": "My CV",
"about.title": "About me",
"about.description": "I am a third-year student at ISEN Nantes, currently in a work-study program at Horoquartz. I am passionate about computer science. I learned to code on my own and am currently learning Rust and Go. I am also passionate about electronics and hardware. I have a homelab consisting of 3 servers: a DELL T320, a DELL T330, and a Dell Precision T3610, all running Proxmox.",
"card.title": "Third year student at ISEN Nantes in work-study program at Horoquartz",
"about.description": "I am a fourth-year student at ISEN Nantes, currently in a work-study program at Horoquartz. I am passionate about computer science. I learned to code on my own and am currently learning Rust and Go. I am also passionate about electronics and hardware. I have a homelab consisting of 3 servers: a DELL T320, a DELL T330, and a Dell Precision T3610, all running Proxmox.",
"card.title": "Fourth year student at ISEN Nantes in work-study program at Horoquartz",
"projects.title": "My projects",
"projects.Front end starter.description": "My personal starter for front end projects",
"projects.Project C - ISEN CIR 1.description": "End of 1st year project at ISEN Nantes",
@@ -66,9 +67,10 @@ i18n
"projects.Projet C++ - ISEN CIR 2.description": "End of 4th semester project at ISEN Nantes. Creation of a Tower Defense type game in C++ with the QT6 library.",
"projects.Github NTFY.description": "Notification project for github and dockerhub releases that sends notifications on ntfy, gotify and discord.",
"projects.Projet C++ - ISEN CIPA 3.description": "Fish school behavior simulation game project in C++ with the SLD2 library, with multiplayer support.",
"projects.Alternance Horoquartz.description": "Development of an update system for Horoquartz products.",
"nav.about": "About me",
"nav.projects": "My projects",
"nav.cv": "My CV",
"nav.cv": "My resume",
"cv.path": "/CV-Felix-MARQUET-English.pdf",
"skills.beginner": "Beginner",
"skills.intermediate": "Intermediate",
@@ -78,7 +80,7 @@ i18n
"skills.examples.C++.title": "C++ Projects",
"skills.examples.C++.description": "Tower Defense in Qt6, fish school simulation with SDL2 featuring multiplayer support, basic algorithms.",
"skills.examples.Admin Système.title": "System Administration",
"skills.examples.Admin Système.description": "Configuration of 3 DELL servers under Proxmox, virtualization, maintenance of virtual machines.",
"skills.examples.Admin Système.description": "Configuration of 3 DELL servers under Proxmox, virtualization, maintenance of virtual machines, deployment of applications in Azure.",
"skills.examples.Python.title": "Python Projects",
"skills.examples.Python.description": "GitHub NTFY for GitHub and DockerHub release notifications, course projects.",
"skills.examples.PHP.title": "PHP Projects",
@@ -86,7 +88,7 @@ i18n
"skills.examples.HTML/CSS.title": "Front-end development",
"skills.examples.HTML/CSS.description": "Custom front-end starter, base project, various web projects.",
"skills.examples.JS/TS.title": "JavaScript/TypeScript",
"skills.examples.JS/TS.description": "React development, this portfolio, NodeJS API with Express, creation of the ISEN Orbit mobile application with React Native, professional projects.",
"skills.examples.JS/TS.description": "React development, this portfolio, NodeJS API with Express, creation of the studysen mobile application with React Native, professional projects.",
"skills.examples.Linux.title": "Linux Administration",
"skills.examples.Linux.description": "Server configuration, system administration, Ansible playbooks.",
"skills.examples.Go.title": "Go Projects",
@@ -96,7 +98,7 @@ i18n
"skills.examples.Rust.title": "Rust Projects",
"skills.examples.Rust.description": "Learning the language, creation of a REST API with Ntex, rewrit of the GitHub NTFY API in Rust.",
"skills.examples.React.title": "React Development",
"skills.examples.React.description": "This portfolio, Modelec website, ISEN Orbit mobile application, professional projects."
"skills.examples.React.description": "This portfolio, Modelec website, Studysen mobile application, professional projects."
},
},
},

View File

@@ -1,3 +1,137 @@
/* Animations personnalisées */
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
33% { transform: translateY(-10px) rotate(120deg); }
66% { transform: translateY(10px) rotate(240deg); }
}
@keyframes typewriter {
from { width: 0; }
to { width: 100%; }
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes shimmer {
0% { background-position: -1000px 0; }
100% { background-position: 1000px 0; }
}
/* Utility classes */
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-fadeInUp {
animation: fadeInUp 0.6s ease-out forwards;
}
.animate-slideInLeft {
animation: slideInLeft 0.8s ease-out forwards;
}
.animate-slideInRight {
animation: slideInRight 0.8s ease-out forwards;
}
.animate-shimmer {
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
background-size: 1000px 100%;
animation: shimmer 2s infinite;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Smooth scroll */
html {
scroll-behavior: smooth;
}
/* Loading transition */
.loading-exit {
opacity: 0;
transform: scale(0.9);
transition: all 0.5s ease-in-out;
}
/* Hover effects */
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-8px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.dark .card-hover:hover {
box-shadow: 0 20px 25px -5px rgba(255, 255, 255, 0.1), 0 10px 10px -5px rgba(255, 255, 255, 0.04);
}
/* Progress bar animation */
.progress-fill {
transition: width 1.5s ease-in-out;
background: linear-gradient(90deg, #3b82f6, #8b5cf6, #06b6d4);
background-size: 200% 100%;
animation: shimmer 3s ease-in-out infinite;
}
/* Modal backdrop blur */
.modal-backdrop {
backdrop-filter: blur(8px);
}
/* Navigation responsive */
@media (max-width: 1024px) {
.menu-hidden ul {
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.75rem;
}
.menu-hidden li a {
font-size: 0.875rem;
padding: 0.5rem;
}
}
@media (max-width: 768px) {
.menu-hidden {
display: none;
@@ -12,5 +146,4 @@
margin-left: 10em;
margin-right: 10em;
}
}