feat: Add GitHubStatsSection component for displaying GitHub statistics

This commit is contained in:
Félix MARQUET
2025-08-28 12:32:20 +00:00
parent 810362173b
commit a80d9ba72c
13 changed files with 767 additions and 480 deletions

View File

@@ -2,7 +2,7 @@
<html lang="fr">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="/V5.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="generator" content="React" />
<meta name="theme-color" content="#000000" />

View File

@@ -9,8 +9,8 @@
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/react": "19.1.11",
"@types/react-dom": "19.1.8",
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9",
"aos": "^2.3.4",
"browserslist": "^4.25.3",
"browserslist-useragent": "^4.0.0",
@@ -60,7 +60,7 @@
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@simonsmith/cypress-image-snapshot": "^10.0.2",
"@vitejs/plugin-react": "^5.0.1",
"@vitejs/plugin-react": "^5.0.2",
"prettier": "^3.6.2",
"vite": "^7.1.3",
"vite-plugin-svgr": "^4.5.0",

48
pnpm-lock.yaml generated
View File

@@ -22,16 +22,16 @@ importers:
version: 6.8.0
'@testing-library/react':
specifier: ^16.3.0
version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.8(@types/react@19.1.11))(@types/react@19.1.11)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@testing-library/user-event':
specifier: ^14.6.1
version: 14.6.1(@testing-library/dom@10.4.1)
'@types/react':
specifier: 19.1.11
version: 19.1.11
specifier: 19.1.12
version: 19.1.12
'@types/react-dom':
specifier: 19.1.8
version: 19.1.8(@types/react@19.1.11)
specifier: 19.1.9
version: 19.1.9(@types/react@19.1.12)
aos:
specifier: ^2.3.4
version: 2.3.4
@@ -94,8 +94,8 @@ importers:
specifier: ^10.0.2
version: 10.0.2(cypress@15.0.0)
'@vitejs/plugin-react':
specifier: ^5.0.1
version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.91.0))
specifier: ^5.0.2
version: 5.0.2(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.91.0))
prettier:
specifier: ^3.6.2
version: 3.6.2
@@ -539,8 +539,8 @@ packages:
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
engines: {node: '>= 10.0.0'}
'@rolldown/pluginutils@1.0.0-beta.32':
resolution: {integrity: sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==}
'@rolldown/pluginutils@1.0.0-beta.34':
resolution: {integrity: sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==}
'@rollup/pluginutils@5.2.0':
resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==}
@@ -889,13 +889,13 @@ packages:
'@types/pixelmatch@5.2.6':
resolution: {integrity: sha512-wC83uexE5KGuUODn6zkm9gMzTwdY5L0chiK+VrKcDfEjzxh1uadlWTvOmAbCpnM9zx/Ww3f8uKlYQVnO/TrqVg==}
'@types/react-dom@19.1.8':
resolution: {integrity: sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ==}
'@types/react-dom@19.1.9':
resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==}
peerDependencies:
'@types/react': ^19.0.0
'@types/react@19.1.11':
resolution: {integrity: sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==}
'@types/react@19.1.12':
resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==}
'@types/sinonjs__fake-timers@8.1.1':
resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==}
@@ -915,8 +915,8 @@ packages:
'@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
'@vitejs/plugin-react@5.0.1':
resolution: {integrity: sha512-DE4UNaBXwtVoDJ0ccBdLVjFTWL70NRuWNCxEieTI3lrq9ORB9aOCQEKstwDXBl87NvFdbqh/p7eINGyj0BthJA==}
'@vitejs/plugin-react@5.0.2':
resolution: {integrity: sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
@@ -2773,7 +2773,7 @@ snapshots:
'@parcel/watcher-win32-ia32': 2.5.1
'@parcel/watcher-win32-x64': 2.5.1
'@rolldown/pluginutils@1.0.0-beta.32': {}
'@rolldown/pluginutils@1.0.0-beta.34': {}
'@rollup/pluginutils@5.2.0(rollup@4.48.1)':
dependencies:
@@ -3025,15 +3025,15 @@ snapshots:
picocolors: 1.1.1
redent: 3.0.0
'@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.8(@types/react@19.1.11))(@types/react@19.1.11)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
'@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@babel/runtime': 7.28.3
'@testing-library/dom': 10.4.1
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
optionalDependencies:
'@types/react': 19.1.11
'@types/react-dom': 19.1.8(@types/react@19.1.11)
'@types/react': 19.1.12
'@types/react-dom': 19.1.9(@types/react@19.1.12)
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
dependencies:
@@ -3093,11 +3093,11 @@ snapshots:
dependencies:
'@types/node': 24.3.0
'@types/react-dom@19.1.8(@types/react@19.1.11)':
'@types/react-dom@19.1.9(@types/react@19.1.12)':
dependencies:
'@types/react': 19.1.11
'@types/react': 19.1.12
'@types/react@19.1.11':
'@types/react@19.1.12':
dependencies:
csstype: 3.1.3
@@ -3118,12 +3118,12 @@ snapshots:
'@types/node': 24.3.0
optional: true
'@vitejs/plugin-react@5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.91.0))':
'@vitejs/plugin-react@5.0.2(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.91.0))':
dependencies:
'@babel/core': 7.28.3
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.3)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.3)
'@rolldown/pluginutils': 1.0.0-beta.32
'@rolldown/pluginutils': 1.0.0-beta.34
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.91.0)

BIN
public/V5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

View File

@@ -1,59 +1,102 @@
{
"about.title": "About me",
"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",
"projects.Projet robot.description": "Robot project with the Modelec ISEN club for the French robotics cup (Development and deployment on Raspberry Pi)",
"projects.MercuryCloud.description": "Game server and VPS hosting project. Technical support position, administrator of VPS, Game and web services.",
"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.",
"cv.title": "My resume",
"cv.path": "/CV-Felix-MARQUET-English.pdf",
"nav.about": "About me",
"nav.projects": "My projects",
"nav.cv": "My resume",
"skills.beginner": "Beginner",
"skills.intermediate": "Intermediate",
"skills.expert": "Expert",
"skills.examples.C.title": "C Projects",
"skills.examples.C.description": "End-of-first-year project at ISEN, basic algorithms, creation of a REST API.",
"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, 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",
"skills.examples.PHP.description": "Web development with PHP, AJAX, PostgreSQL.",
"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 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",
"skills.examples.Go.description": "Introduction to the language, creation of a REST API with Fiber, Go Gin, and the standard library, professional projects.",
"skills.examples.Docker.title": "Containerization",
"skills.examples.Docker.description": "Container deployment, Docker Compose configuration, image creation, high-availability service deployment via Swarm and Kubernetes.",
"skills.examples.Rust.title": "Rust Projects",
"skills.examples.Rust.description": "Learning the language, creation of a REST API with Ntex, rewrite of the GitHub NTFY API in Rust.",
"skills.examples.React.title": "React Development",
"skills.examples.React.description": "This portfolio, Modelec website, Studysen mobile application, professional projects.",
"contact.title": "Contact",
"contact.subtitle": "A question? A project? Feel free to contact me!",
"contact.info.title": "Contact information",
"contact.info.email": "Email",
"contact.info.status": "Status",
"contact.info.statusValue": "ISEN student - Work-study at Horoquartz",
"contact.info.response": "Response",
"contact.info.responseValue": "Usually within 24h",
"contact.subjects.title": "Topics of interest",
"contact.subjects.list": [
"about": {
"title": "About me",
"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",
"Front end starter.description": "My personal starter for front end projects",
"Project C - ISEN CIR 1.description": "End of 1st year project at ISEN Nantes",
"Projet robot.description": "Robot project with the Modelec ISEN club for the French robotics cup (Development and deployment on Raspberry Pi)",
"MercuryCloud.description": "Game server and VPS hosting project. Technical support position, administrator of VPS, Game and web services.",
"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.",
"Github NTFY.description": "Notification project for github and dockerhub releases that sends notifications on ntfy, gotify and discord.",
"Projet C++ - ISEN CIPA 3.description": "Fish school behavior simulation game project in C++ with the SLD2 library, with multiplayer support.",
"Alternance Horoquartz.description": "Development of an update system for Horoquartz products."
},
"cv": {
"title": "My resume",
"path": "/CV-Felix-MARQUET-English.pdf"
},
"nav": {
"about": "About me",
"projects": "My projects",
"cv": "My resume",
"contact": "Contact",
"experience": "Experience",
"github": "GitHub"
},
"skills": {
"beginner": "Beginner",
"intermediate": "Intermediate",
"expert": "Expert",
"examples": {
"C": {
"title": "C Projects",
"description": "End-of-first-year project at ISEN, basic algorithms, creation of a REST API."
},
"C++": {
"title": "C++ Projects",
"description": "Tower Defense in Qt6, fish school simulation with SDL2 featuring multiplayer support, basic algorithms."
},
"Admin Système": {
"title": "System Administration",
"description": "Configuration of 3 DELL servers under Proxmox, virtualization, maintenance of virtual machines, deployment of applications in Azure."
},
"Python": {
"title": "Python Projects",
"description": "GitHub NTFY for GitHub and DockerHub release notifications, course projects."
},
"PHP": {
"title": "PHP Projects",
"description": "Web development with PHP, AJAX, PostgreSQL."
},
"HTML/CSS": {
"title": "Front-end development",
"description": "Custom front-end starter, base project, various web projects."
},
"JS/TS": {
"title": "JavaScript/TypeScript",
"description": "React development, this portfolio, NodeJS API with Express, creation of the Studysen mobile application with React Native, professional projects."
},
"Linux": {
"title": "Linux Administration",
"description": "Server configuration, system administration, Ansible playbooks."
},
"Go": {
"title": "Go Projects",
"description": "Introduction to the language, creation of a REST API with Fiber, Go Gin, and the standard library, professional projects."
},
"Docker": {
"title": "Containerization",
"description": "Container deployment, Docker Compose configuration, image creation, high-availability service deployment via Swarm and Kubernetes."
},
"Rust": {
"title": "Rust Projects",
"description": "Learning the language, creation of a REST API with Ntex, rewrite of the GitHub NTFY API in Rust."
},
"React": {
"title": "React Development",
"description": "This portfolio, Modelec website, Studysen mobile application, professional projects."
}
}
},
"contact": {
"title": "Contact",
"subtitle": "A question? A project? Feel free to contact me!",
"info": {
"title": "Contact information",
"email": "Email",
"status": "Status",
"statusValue": "ISEN student - Work-study at Horoquartz",
"response": "Response",
"responseValue": "Usually within 24h"
},
"subjects.title": "Topics of interest",
"subjects.list": [
"Web Development",
"System Administration",
"DevOps",
@@ -63,19 +106,20 @@
"Collaboration",
"Internship/Job"
],
"contact.form.title": "Send me a message",
"contact.form.name": "Full name",
"contact.form.namePlaceholder": "Your name",
"contact.form.email": "Email",
"contact.form.emailPlaceholder": "your.email@example.com",
"contact.form.subject": "Subject",
"contact.form.subjectPlaceholder": "Subject of your message",
"contact.form.message": "Message",
"contact.form.messagePlaceholder": "Your message...",
"contact.form.send": "Send message",
"contact.form.sending": "Sending...",
"contact.form.success": "Message sent successfully! I will reply as soon as possible.",
"contact.form.error": "An error occurred. Please try again later.",
"form.title": "Send me a message",
"form.name": "Full name",
"form.namePlaceholder": "Your name",
"form.email": "Email",
"form.emailPlaceholder": "your.email@example.com",
"form.subject": "Subject",
"form.subjectPlaceholder": "Subject of your message",
"form.message": "Message",
"form.messagePlaceholder": "Your message...",
"form.send": "Send message",
"form.sending": "Sending...",
"form.success": "Message sent successfully! I will reply as soon as possible.",
"form.error": "An error occurred. Please try again later."
},
"experience": {
"title": "Career & Experience",
"description": "My professional and academic journey",
@@ -144,5 +188,16 @@
"technologies": ["C++", "QT", "Raspberry Pi", "Linux"]
}
]
},
"github": {
"title": "GitHub Stats",
"description": "An overview of my activity and contributions on GitHub",
"stars": "Stars",
"forks": "Forks",
"commits": "Commits",
"languages": "Most used languages",
"legend": "Contribution calendar",
"totalCommits": "Total commits",
"activity": "Recent activity"
}
}

View File

@@ -1,80 +1,125 @@
{
"about.title": "A propos de moi",
"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 3 serveur, 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",
"projects.Projet robot.description": "Projet de robot avec le club Modelec ISEN pour la coupe de france de robotique (Developpement et déploiment sur Raspberry Pi)",
"projects.MercuryCloud.description": "Projet d'herbergeur de serveur de jeu et VPS. Poste de support technique, administrateur des service VPS, Game et web.",
"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",
"nav.projects": "Mes projets",
"nav.cv": "Mon CV",
"skills.beginner": "Débutant",
"skills.intermediate": "Intermédiaire",
"skills.expert": "Expert",
"skills.examples.C.title": "Projets C",
"skills.examples.C.description": "Projet de fin de 1ère année à l'ISEN, algorithmes de base, création d'une api REST",
"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, 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",
"skills.examples.PHP.description": "Développements web avec PHP, AJAX, postgreSQL",
"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 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",
"skills.examples.Go.description": "Initiation au langage, Création d'une api REST avec Fiber, Go Gin et la librairie standard, projets professionnels",
"skills.examples.Docker.title": "Containerisation",
"skills.examples.Docker.description": "Déploiement de conteneurs, configuration Docker Compose, création d'images, déploiement de services en haute disponibilité via Swarm et Kubernetes",
"contact.title": "Contact",
"contact.subtitle": "Une question ? Un projet ? N'hésitez pas à me contacter !",
"contact.info.title": "Informations de contact",
"contact.info.email": "Email",
"contact.info.status": "Statut",
"contact.info.statusValue": "Étudiant ISEN - Alternant chez Horoquartz",
"contact.info.response": "Réponse",
"contact.info.responseValue": "Généralement sous 24h",
"contact.subjects.title": "Sujets d'intérêt",
"contact.subjects.list": [
"Développement Web",
"Administration Système",
"DevOps",
"Alternance",
"Projets Open Source",
"Homelab",
"Collaboration",
"Stage/Emploi"
],
"contact.form.title": "Envoyez-moi un message",
"contact.form.name": "Nom complet",
"contact.form.namePlaceholder": "Votre nom",
"contact.form.email": "Email",
"contact.form.emailPlaceholder": "votre.email@exemple.com",
"contact.form.subject": "Sujet",
"contact.form.subjectPlaceholder": "Objet de votre message",
"contact.form.message": "Message",
"contact.form.messagePlaceholder": "Votre message...",
"contact.form.send": "Envoyer le message",
"contact.form.sending": "Envoi en cours...",
"contact.form.success": "Message envoyé avec succès ! Je vous répondrai dans les plus brefs délais.",
"contact.form.error": "Une erreur est survenue. Veuillez réessayer plus tard.",
"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 Studysen, projets professionnels",
"about": {
"title": "A propos de moi",
"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 3 serveur, 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",
"Front end starter.description": "Mon starter personnel pour projet front end",
"Project C - ISEN CIR 1.description": "Projet de fin de 1ere année à l'ISEN Nantes",
"Projet robot.description": "Projet de robot avec le club Modelec ISEN pour la coupe de france de robotique (Developpement et déploiment sur Raspberry Pi)",
"MercuryCloud.description": "Projet d'herbergeur de serveur de jeu et VPS. Poste de support technique, administrateur des service VPS, Game et web.",
"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.",
"Github NTFY.description": "Projet de notification pour les releases github et dockerhub qui envoie des notifications sur ntfy, gotify et discord.",
"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.",
"Alternance Horoquartz.description": "Développement d'un système de mise à jour pour les produits Horoquartz."
},
"cv": {
"title": "Mon CV",
"path": "/CV-Felix-MARQUET.pdf"
},
"nav": {
"about": "A propos de moi",
"projects": "Mes projets",
"cv": "Mon CV",
"contact": "Contact",
"experience": "Expérience",
"github": "GitHub"
},
"skills": {
"beginner": "Débutant",
"intermediate": "Intermédiaire",
"expert": "Expert",
"examples": {
"C": {
"title": "Projets C",
"description": "Projet de fin de 1ère année à l'ISEN, algorithmes de base, création d'une api REST"
},
"C++": {
"title": "Projets C++",
"description": "Tower Defense en Qt6, simulation de banc de poissons avec SDL2 avec support du multijoueur, algorithmes de base"
},
"Admin Système": {
"title": "Administration Système",
"description": "Configuration de 3 serveurs DELL sous Proxmox, virtualisation, maintenance des machines virtuelles, deploiement d'applications dans Azure"
},
"Python": {
"title": "Projets Python",
"description": "Github NTFY pour les notifications des releases github et dockerhub, projets de cours"
},
"PHP": {
"title": "Projets PHP",
"description": "Développements web avec PHP, AJAX, postgreSQL"
},
"HTML/CSS": {
"title": "Développement Front-end",
"description": "Front-end starter personnalisé, projet de base, divers projets web"
},
"JS/TS": {
"title": "JavaScript/TypeScript",
"description": "Développement React, ce portfolio, api NodeJS avec Express, Création d'une application mobile Studysen avec React Native, projets professionnels"
},
"Linux": {
"title": "Administration Linux",
"description": "Configuration de serveurs, administration système, Ansible playbooks"
},
"Go": {
"title": "Projets Go",
"description": "Initiation au langage, Création d'une api REST avec Fiber, Go Gin et la librairie standard, projets professionnels"
},
"Docker": {
"title": "Containerisation",
"description": "Déploiement de conteneurs, configuration Docker Compose, création d'images, déploiement de services en haute disponibilité via Swarm et Kubernetes"
},
"Rust": {
"title": "Projets Rust",
"description": "Apprentissage du langage, création d'une api REST avec Ntex, récriture de l'api github NTFY en Rust"
},
"React": {
"title": "Développement React",
"description": "Ce portfolio, site web de Modelec, application mobile Studysen, projets professionnels"
}
}
},
"contact": {
"title": "Contact",
"subtitle": "Une question ? Un projet ? N'hésitez pas à me contacter !",
"info": {
"title": "Informations de contact",
"email": "Email",
"status": "Statut",
"statusValue": "Étudiant ISEN - Alternant chez Horoquartz",
"response": "Réponse",
"responseValue": "Généralement sous 24h"
},
"subjects.title": "Sujets d'intérêt",
"subjects.list": [
"Développement Web",
"Administration Système",
"DevOps",
"Alternance",
"Projets Open Source",
"Homelab",
"Collaboration",
"Stage/Emploi"
],
"form.title": "Envoyez-moi un message",
"form.name": "Nom complet",
"form.namePlaceholder": "Votre nom",
"form.email": "Email",
"form.emailPlaceholder": "votre.email@exemple.com",
"form.subject": "Sujet",
"form.subjectPlaceholder": "Objet de votre message",
"form.message": "Message",
"form.messagePlaceholder": "Votre message...",
"form.send": "Envoyer le message",
"form.sending": "Envoi en cours...",
"form.success": "Message envoyé avec succès ! Je vous répondrai dans les plus brefs délais.",
"form.error": "Une erreur est survenue. Veuillez réessayer plus tard."
},
"experience": {
"title": "Parcours & Expérience",
"description": "Mon évolution professionnelle et académique",
@@ -143,5 +188,16 @@
"technologies": ["C++", "QT", "Raspberry Pi", "Linux"]
}
]
},
"github": {
"title": "Statistiques GitHub",
"description": "Un aperçu de mon activité et de mes contributions sur GitHub",
"stars": "Étoiles",
"forks": "Forks",
"commits": "Commits",
"languages": "Langages les plus utilisés",
"legend": "Calendrier des contributions",
"totalCommits": "Total des commits",
"activity": "Activité récente"
}
}

View File

@@ -3,21 +3,22 @@ import { FaSun, FaMoon} from "react-icons/fa";
import ReactDOM from "react-dom";
import './output.css';
import './other.css';
import Card from "./components/Card.tsx";
import About from "./components/About.tsx";
import Skills from "./components/Skills.tsx";
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 ContactSection from "./components/ContactSection.tsx";
import TimelineSection from "./components/TimelineSection.tsx";
import data from "./assets/DATA.ts";
import Card from "./components/Card";
import About from "./components/About";
import Skills from "./components/Skills";
import Project from "./components/Project";
import Footer from "./components/Footer";
import CV from "./components/CV";
import Menu from "./components/Menu";
import LoadingScreen from "./components/LoadingScreen";
import ParticlesBackground from "./components/ParticlesBackground";
import ContactSection from "./components/ContactSection";
import TimelineSection from "./components/TimelineSection";
import data from "./assets/DATA";
import { useTranslation } from "react-i18next";
import i18n from './i18n.js';
import {createRoot} from "react-dom/client";
import GitHubStatsSection from 'components/GitHubStatsSection';
function App() {
// Initialise le thème depuis localStorage ou détecte le thème système
@@ -78,7 +79,12 @@ function App() {
<About />
<Skills skills={data.skills} />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="experience" />
<TimelineSection experience={data.experience} />
<TimelineSection experience={data.experience.map(item => ({
...item,
type: item.type as "work" | "education" | "achievement"
}))} />
<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" />
@@ -99,8 +105,10 @@ function App() {
}
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
if (container) {
const root = createRoot(container);
root.render(<App />);
}
// ReactDOM.render(<App />, document.getElementById('root'));
export default App;

View File

@@ -12,6 +12,7 @@ interface Social {
interface CardProps {
name: string;
title: string;
social: Social;
}

View File

@@ -0,0 +1,234 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { FaGithub, FaStar, FaCodeBranch, FaCode, FaCalendarAlt } from 'react-icons/fa';
interface GitHubStats {
totalRepos: number;
totalStars: number;
totalForks: number;
totalCommits: number;
languages: { [key: string]: number };
contributions: Array<{ date: string; count: number }>;
}
const GitHubStatsSection: React.FC = () => {
const [stats, setStats] = useState<GitHubStats | null>(null);
const [loading, setLoading] = useState(true);
const { t } = useTranslation();
useEffect(() => {
const fetchStats = async () => {
try {
const username = "BreizhHardware"; // à adapter si besoin
const token = import.meta.env.VITE_GITHUB_TOKEN;
// Infos utilisateur REST
const userRes = await fetch(`https://api.github.com/users/${username}`, {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github.v3+json",
},
});
const userData = await userRes.json();
// Repos pour stars/forks REST
const reposRes = await fetch(`https://api.github.com/users/${username}/repos?per_page=100`, {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github.v3+json",
},
});
const reposData = await reposRes.json();
// Calculs REST
const totalStars = reposData.reduce((acc: number, repo: any) => acc + repo.stargazers_count, 0);
const totalForks = reposData.reduce((acc: number, repo: any) => acc + repo.forks_count, 0);
// Langages
const langCount: { [key: string]: number } = {};
reposData.forEach((repo: any) => {
if (repo.language) {
langCount[repo.language] = (langCount[repo.language] || 0) + 1;
}
});
const totalRepos = userData.public_repos;
// GraphQL pour contributions
const today = new Date();
const lastYear = new Date(today);
lastYear.setFullYear(today.getFullYear() - 1);
const fromDate = lastYear.toISOString(); // format complet ISO 8601
const toDate = today.toISOString();
const graphQLQuery = {
query: `
query {
user(login: "${username}") {
contributionsCollection(from: \"${fromDate}\", to: \"${toDate}\") {
contributionCalendar {
totalContributions
weeks {
contributionDays {
date
contributionCount
}
}
}
}
}
}
`
};
const graphQLRes = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(graphQLQuery),
});
const graphQLData = await graphQLRes.json();
const calendar = graphQLData.data.user.contributionsCollection.contributionCalendar;
const totalCommits = calendar.totalContributions;
// Flatten days
const contributions: Array<{ date: string; count: number }> = [];
calendar.weeks.forEach((week: any) => {
week.contributionDays.forEach((day: any) => {
contributions.push({ date: day.date, count: day.contributionCount });
});
});
setStats({
totalRepos,
totalStars,
totalForks,
totalCommits,
languages: langCount,
contributions,
});
} catch (e) {
setStats(null);
}
setLoading(false);
};
fetchStats();
}, []);
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">
{t('github.title')}
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
{t('github.description')}
</p>
</div>
{stats && (
<div className="space-y-8">
<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">{t('github.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">{t('github.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">{t('github.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 !== -1 ? stats.totalCommits : "N/A"}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">{t('github.commits')}</p>
</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-6 text-center">
{t('github.languages')}
</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>
<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">
{t('github.activity')}
</h3>
{stats.contributions && (
<div className="grid grid-cols-12 gap-1 max-w-3xl mx-auto">
{stats.contributions.map((day, i) => {
let color = 'bg-gray-200 dark:bg-gray-700';
if (day.count > 10) color = 'bg-green-500';
else if (day.count > 5) color = 'bg-green-400';
else if (day.count > 2) color = 'bg-green-300';
else if (day.count > 0) color = 'bg-green-200';
return (
<div
key={day.date}
className={`w-3 h-3 rounded-sm ${color} animate-fadeInUp`}
style={{ animationDelay: `${i * 2}ms` }}
title={`${day.date}: ${day.count} commit${day.count > 1 ? 's' : ''}`}
/>
);
})}
</div>
)}
<div className="text-center mt-4">
<span className="text-sm text-gray-500 dark:text-gray-400">
{t('github.legend')}<br/>
{t('github.totalCommits')} : {stats.totalCommits}
</span>
</div>
</div>
</div>
)}
</div>
);
};
export default GitHubStatsSection;

View File

@@ -1,216 +0,0 @@
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

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
interface LoadingScreenProps {
onLoadingComplete: () => void;
@@ -9,24 +9,162 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({ onLoadingComplete }) => {
const [showCursor, setShowCursor] = useState(true);
const fullText = "Félix MARQUET";
const [isComplete, setIsComplete] = useState(false);
const [assetsLoaded, setAssetsLoaded] = useState(false);
const [loadingProgress, setLoadingProgress] = useState(0);
const [currentAsset, setCurrentAsset] = useState('Initialisation...');
const typingRef = useRef<{
currentIndex: number;
timeout: NodeJS.Timeout | null;
isTyping: boolean;
}>({
currentIndex: 0,
timeout: null,
isTyping: false
});
useEffect(() => {
let currentIndex = 0;
const typingInterval = setInterval(() => {
if (currentIndex <= fullText.length) {
setDisplayText(fullText.substring(0, currentIndex));
currentIndex++;
let loadedCount = 0;
let totalCount = 0;
const updateProgress = () => {
const progress = totalCount > 0 ? (loadedCount / totalCount) * 100 : 0;
setLoadingProgress(progress);
if (loadedCount === totalCount && totalCount > 0) {
setAssetsLoaded(true);
setCurrentAsset('Prêt !');
}
};
const incrementLoaded = (assetName?: string) => {
loadedCount++;
if (assetName) {
setCurrentAsset(`Chargé: ${assetName}`);
}
updateProgress();
};
const checkAssets = async () => {
const assetsToCheck = [
{ url: 'https://1.gravatar.com/avatar/4d43af207280d1d23e2a2905577c7b6167723fec2d33f946cc86f114c1a85b8d?size=256', name: 'Photo de profil' },
{ url: '/V5.png', name: 'Favicon' },
{ url: '/CV-Felix-MARQUET.pdf', name: 'CV Français' },
{ url: '/CV-Felix-MARQUET-English.pdf', name: 'CV Anglais' },
{ url: '/locales/fr/translation.json', name: 'Traductions FR' },
{ url: '/locales/en/translation.json', name: 'Traductions EN' }
];
totalCount = assetsToCheck.length + 1;
setCurrentAsset('Chargement des polices...');
try {
await document.fonts.ready;
incrementLoaded('Polices');
} catch (error) {
incrementLoaded('Polices');
}
for (const asset of assetsToCheck) {
setCurrentAsset(`Chargement: ${asset.name}...`);
if (asset.url.endsWith('.json')) {
try {
const response = await fetch(asset.url);
if (response.ok) {
incrementLoaded(asset.name);
} else {
incrementLoaded(asset.name);
}
} catch (error) {
incrementLoaded(asset.name);
}
} else if (asset.url.endsWith('.pdf')) {
try {
const response = await fetch(asset.url, { method: 'HEAD' });
incrementLoaded(asset.name);
} catch (error) {
incrementLoaded(asset.name);
}
} else {
await new Promise<void>((resolve) => {
const img = new Image();
img.onload = () => {
incrementLoaded(asset.name);
resolve();
};
img.onerror = () => {
incrementLoaded(asset.name);
resolve();
};
img.src = asset.url;
});
}
}
setTimeout(() => {
if (!assetsLoaded) {
loadedCount = totalCount;
setAssetsLoaded(true);
setLoadingProgress(100);
setCurrentAsset('Prêt !');
}
}, 5000);
};
const timeout = setTimeout(() => {
checkAssets();
}, 100);
return () => clearTimeout(timeout);
}, [assetsLoaded]);
const startTyping = () => {
if (typingRef.current.isTyping) return;
typingRef.current.isTyping = true;
const typeNextChar = () => {
if (typingRef.current.currentIndex <= fullText.length) {
setDisplayText(fullText.substring(0, typingRef.current.currentIndex));
typingRef.current.currentIndex++;
const delay = assetsLoaded ? 30 : 150;
typingRef.current.timeout = setTimeout(typeNextChar, delay);
} else {
clearInterval(typingInterval);
setIsComplete(true);
typingRef.current.isTyping = false;
const finalDelay = assetsLoaded ? 200 : 800;
setTimeout(() => {
onLoadingComplete();
}, 1000);
}, finalDelay);
}
}, 150);
};
return () => clearInterval(typingInterval);
}, [fullText, onLoadingComplete]);
typeNextChar();
};
useEffect(() => {
startTyping();
return () => {
if (typingRef.current.timeout) {
clearTimeout(typingRef.current.timeout);
}
};
}, []);
useEffect(() => {
if (assetsLoaded && typingRef.current.isTyping && !isComplete) {
if (typingRef.current.timeout) {
clearTimeout(typingRef.current.timeout);
}
typingRef.current.isTyping = false;
setTimeout(() => startTyping(), 10);
}
}, [assetsLoaded, isComplete]);
useEffect(() => {
const cursorInterval = setInterval(() => {
@@ -43,12 +181,15 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({ onLoadingComplete }) => {
{displayText}
{showCursor && !isComplete && <span className="animate-pulse">|</span>}
</div>
<div className="flex space-x-2 justify-center">
<div className="flex space-x-2 justify-center mb-4">
<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>
<p className="text-white/80 text-lg">
{assetsLoaded ? 'Prêt !' : currentAsset}
</p>
</div>
</div>
);

View File

@@ -9,9 +9,10 @@ function Menu() {
<ul className="flex justify-around">
<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="#experience" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.experience')}</a></li>
<li><a href="#github" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.github')}</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="#contact" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.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

@@ -17,6 +17,7 @@
--color-green-50: oklch(98.2% 0.018 155.826);
--color-green-100: oklch(96.2% 0.044 156.743);
--color-green-200: oklch(92.5% 0.084 155.995);
--color-green-300: oklch(87.1% 0.15 154.449);
--color-green-500: oklch(72.3% 0.219 149.579);
--color-green-800: oklch(44.8% 0.119 151.328);
--color-green-900: oklch(39.3% 0.095 152.535);
@@ -46,6 +47,7 @@
--color-white: #fff;
--spacing: 0.25rem;
--container-xs: 20rem;
--container-3xl: 48rem;
--container-4xl: 56rem;
--container-6xl: 72rem;
--text-xs: 0.75rem;
@@ -71,9 +73,11 @@
--font-weight-bold: 700;
--font-weight-extrabold: 800;
--leading-relaxed: 1.625;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--animate-spin: spin 1s linear infinite;
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--animate-bounce: bounce 1s infinite;
@@ -397,9 +401,18 @@
.h-6 {
height: calc(var(--spacing) * 6);
}
.h-8 {
height: calc(var(--spacing) * 8);
}
.h-20 {
height: calc(var(--spacing) * 20);
}
.h-64 {
height: calc(var(--spacing) * 64);
}
.h-full {
height: 100%;
}
.min-h-screen {
min-height: 100vh;
}
@@ -409,6 +422,9 @@
.w-1 {
width: calc(var(--spacing) * 1);
}
.w-1\/3 {
width: calc(1/3 * 100%);
}
.w-3 {
width: calc(var(--spacing) * 3);
}
@@ -430,12 +446,12 @@
.w-full {
width: 100%;
}
.max-w-3xl {
max-width: var(--container-3xl);
}
.max-w-4xl {
max-width: var(--container-4xl);
}
.max-w-6xl {
max-width: var(--container-6xl);
}
.max-w-xs {
max-width: var(--container-xs);
}
@@ -470,6 +486,12 @@
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-12 {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.flex-col {
flex-direction: column;
}
@@ -500,6 +522,9 @@
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.gap-3 {
gap: calc(var(--spacing) * 3);
}
.gap-4 {
gap: calc(var(--spacing) * 4);
}
@@ -526,13 +551,6 @@
margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));
}
}
.space-y-6 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
}
}
.space-y-8 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
@@ -571,6 +589,9 @@
.overflow-hidden {
overflow: hidden;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-full {
border-radius: calc(infinity * 1px);
}
@@ -580,6 +601,9 @@
.rounded-md {
border-radius: var(--radius-md);
}
.rounded-sm {
border-radius: var(--radius-sm);
}
.rounded-xl {
border-radius: var(--radius-xl);
}
@@ -642,9 +666,6 @@
.bg-gray-400 {
background-color: var(--color-gray-400);
}
.bg-gray-500 {
background-color: var(--color-gray-500);
}
.bg-gray-600 {
background-color: var(--color-gray-600);
}
@@ -654,6 +675,12 @@
.bg-green-100 {
background-color: var(--color-green-100);
}
.bg-green-200 {
background-color: var(--color-green-200);
}
.bg-green-300 {
background-color: var(--color-green-300);
}
.bg-green-500 {
background-color: var(--color-green-500);
}
@@ -663,19 +690,21 @@
.bg-red-100 {
background-color: var(--color-red-100);
}
.bg-red-500 {
background-color: var(--color-red-500);
}
.bg-white {
background-color: var(--color-white);
}
.bg-yellow-500 {
background-color: var(--color-yellow-500);
}
.bg-gradient-to-br {
--tw-gradient-position: to bottom right in oklab;
background-image: linear-gradient(var(--tw-gradient-stops));
}
.bg-gradient-to-r {
--tw-gradient-position: to right in oklab;
background-image: linear-gradient(var(--tw-gradient-stops));
}
.from-blue-500 {
--tw-gradient-from: var(--color-blue-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-blue-900 {
--tw-gradient-from: var(--color-blue-900);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@@ -689,6 +718,10 @@
--tw-gradient-to: var(--color-indigo-900);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-purple-500 {
--tw-gradient-to: var(--color-purple-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.p-1 {
padding: calc(var(--spacing) * 1);
}
@@ -707,6 +740,9 @@
.p-6 {
padding: calc(var(--spacing) * 6);
}
.p-8 {
padding: calc(var(--spacing) * 8);
}
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
@@ -719,9 +755,6 @@
.px-5 {
padding-inline: calc(var(--spacing) * 5);
}
.px-6 {
padding-inline: calc(var(--spacing) * 6);
}
.py-1 {
padding-block: calc(var(--spacing) * 1);
}
@@ -834,9 +867,6 @@
.text-green-800 {
color: var(--color-green-800);
}
.text-orange-500 {
color: var(--color-orange-500);
}
.text-purple-500 {
color: var(--color-purple-500);
}
@@ -855,6 +885,9 @@
color: color-mix(in oklab, var(--color-white) 80%, transparent);
}
}
.text-yellow-500 {
color: var(--color-yellow-500);
}
.opacity-0 {
opacity: 0%;
}
@@ -866,10 +899,6 @@
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-sm {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-xl {
--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -908,6 +937,10 @@
--tw-duration: 1000ms;
transition-duration: 1000ms;
}
.ease-out {
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
}
.hover\:bg-blue-700 {
&:hover {
@media (hover: hover) {
@@ -929,13 +962,6 @@
}
}
}
.hover\:text-gray-800 {
&:hover {
@media (hover: hover) {
color: var(--color-gray-800);
}
}
}
.hover\:text-white {
&:hover {
@media (hover: hover) {
@@ -1033,14 +1059,9 @@
width: calc(3/4 * 100%);
}
}
.md\:grid-cols-2 {
.md\:grid-cols-4 {
@media (width >= 48rem) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.md\:grid-cols-3 {
@media (width >= 48rem) {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.md\:flex-row {
@@ -1090,16 +1111,6 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.lg\:grid-cols-3 {
@media (width >= 64rem) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.lg\:grid-cols-4 {
@media (width >= 64rem) {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.dark\:border-gray-600 {
&:where(.dark, .dark *) {
border-color: var(--color-gray-600);
@@ -1238,15 +1249,6 @@
}
}
}
.dark\:hover\:text-gray-200 {
&:where(.dark, .dark *) {
&:hover {
@media (hover: hover) {
color: var(--color-gray-200);
}
}
}
}
.dark\:hover\:text-gray-800 {
&:where(.dark, .dark *) {
&:hover {
@@ -1442,6 +1444,10 @@
syntax: "*";
inherits: false;
}
@property --tw-ease {
syntax: "*";
inherits: false;
}
@property --tw-scale-x {
syntax: "*";
inherits: false;
@@ -1541,6 +1547,7 @@
--tw-backdrop-saturate: initial;
--tw-backdrop-sepia: initial;
--tw-duration: initial;
--tw-ease: initial;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-scale-z: 1;