refactor: remove GitHubStatsSection component and its mock data

fix: remove Homelab link from Menu component

i18n: update TimelineSection to use translation keys for experience items

i18n: switch to loading translations from backend instead of hardcoded

style: add custom variant for dark mode in index.css

style: clean up unused CSS variables and classes in output.css

config: update tailwind.config.js to use ES module syntax

config: include env.d.ts in tsconfig.json for type definitions
This commit is contained in:
Félix MARQUET
2025-08-26 20:22:08 +00:00
parent 46a1b5acdc
commit efc218f445
18 changed files with 493 additions and 6867 deletions

1
.gitignore vendored
View File

@@ -49,3 +49,4 @@ dist
/cypress/fixtures/__tests__/require-vanilla-test.js /cypress/fixtures/__tests__/require-vanilla-test.js
/src/components/SkillCard.js /src/components/SkillCard.js
/src/components/Skills.js /src/components/Skills.js
.env

12
env.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_GITHUB_TOKEN: string;
readonly VITE_EMAILJS_SERVICE_ID: string;
readonly VITE_EMAILJS_TEMPLATE_ID: string;
readonly VITE_EMAILJS_USER_ID: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -7,7 +7,6 @@
<meta name="generator" content="React" /> <meta name="generator" content="React" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="darkreader" content="dark" /> <meta name="darkreader" content="dark" />
<script defer data-domain="mrqt.fr" src="https://plausible.mrqt.fr/js/script.outbound-links.js"></script>
<meta <meta
name="description" name="description"
content="Portfolio de Félix MARQUET" content="Portfolio de Félix MARQUET"

6434
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@emailjs/browser": "^4.4.1",
"@tailwindcss/cli": "^4.1.12", "@tailwindcss/cli": "^4.1.12",
"@tailwindcss/vite": "^4.1.12", "@tailwindcss/vite": "^4.1.12",
"@testing-library/jest-dom": "^6.6.4", "@testing-library/jest-dom": "^6.6.4",
@@ -15,6 +16,7 @@
"browserslist-useragent": "^4.0.0", "browserslist-useragent": "^4.0.0",
"cypress": "^14.5.3", "cypress": "^14.5.3",
"i18next": "^25.3.4", "i18next": "^25.3.4",
"i18next-http-backend": "^3.0.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-github-btn": "^1.4.0", "react-github-btn": "^1.4.0",

61
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@emailjs/browser':
specifier: ^4.4.1
version: 4.4.1
'@tailwindcss/cli': '@tailwindcss/cli':
specifier: ^4.1.12 specifier: ^4.1.12
version: 4.1.12 version: 4.1.12
@@ -44,6 +47,9 @@ importers:
i18next: i18next:
specifier: ^25.3.4 specifier: ^25.3.4
version: 25.4.2(typescript@5.9.2) version: 25.4.2(typescript@5.9.2)
i18next-http-backend:
specifier: ^3.0.2
version: 3.0.2
react: react:
specifier: 19.1.0 specifier: 19.1.0
version: 19.1.0 version: 19.1.0
@@ -247,6 +253,10 @@ packages:
'@cypress/xvfb@1.2.4': '@cypress/xvfb@1.2.4':
resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==}
'@emailjs/browser@4.4.1':
resolution: {integrity: sha512-DGSlP9sPvyFba3to2A50kDtZ+pXVp/0rhmqs2LmbMS3I5J8FSOgLwzY2Xb4qfKlOVHh29EAutLYwe5yuEZmEFg==}
engines: {node: '>=14.0.0'}
'@esbuild/aix-ppc64@0.25.9': '@esbuild/aix-ppc64@0.25.9':
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1117,6 +1127,9 @@ packages:
typescript: typescript:
optional: true optional: true
cross-fetch@4.0.0:
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
cross-spawn@7.0.6: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -1392,6 +1405,9 @@ packages:
resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==}
engines: {node: '>=8.12.0'} engines: {node: '>=8.12.0'}
i18next-http-backend@3.0.2:
resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==}
i18next@25.4.2: i18next@25.4.2:
resolution: {integrity: sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==} resolution: {integrity: sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==}
peerDependencies: peerDependencies:
@@ -1709,6 +1725,15 @@ packages:
node-addon-api@7.1.1: node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-releases@2.0.19: node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
@@ -2047,6 +2072,9 @@ packages:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
engines: {node: '>=16'} engines: {node: '>=16'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tree-kill@1.2.2: tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true hasBin: true
@@ -2308,6 +2336,12 @@ packages:
web-vitals@5.1.0: web-vitals@5.1.0:
resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==} resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which@2.0.2: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -2549,6 +2583,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@emailjs/browser@4.4.1': {}
'@esbuild/aix-ppc64@0.25.9': '@esbuild/aix-ppc64@0.25.9':
optional: true optional: true
@@ -3267,6 +3303,12 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.9.2 typescript: 5.9.2
cross-fetch@4.0.0:
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@@ -3602,6 +3644,12 @@ snapshots:
human-signals@1.1.1: {} human-signals@1.1.1: {}
i18next-http-backend@3.0.2:
dependencies:
cross-fetch: 4.0.0
transitivePeerDependencies:
- encoding
i18next@25.4.2(typescript@5.9.2): i18next@25.4.2(typescript@5.9.2):
dependencies: dependencies:
'@babel/runtime': 7.28.3 '@babel/runtime': 7.28.3
@@ -3874,6 +3922,10 @@ snapshots:
node-addon-api@7.1.1: {} node-addon-api@7.1.1: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-releases@2.0.19: {} node-releases@2.0.19: {}
npm-run-path@4.0.1: npm-run-path@4.0.1:
@@ -4210,6 +4262,8 @@ snapshots:
dependencies: dependencies:
tldts: 6.1.86 tldts: 6.1.86
tr46@0.0.3: {}
tree-kill@1.2.2: {} tree-kill@1.2.2: {}
tsconfck@3.1.6(typescript@5.9.2): tsconfck@3.1.6(typescript@5.9.2):
@@ -4465,6 +4519,13 @@ snapshots:
web-vitals@5.1.0: {} web-vitals@5.1.0: {}
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which@2.0.2: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0

View File

@@ -0,0 +1,148 @@
{
"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": [
"Web Development",
"System Administration",
"DevOps",
"Work-study",
"Open Source Projects",
"Homelab",
"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.",
"experience": {
"title": "Career & Experience",
"description": "My professional and academic journey",
"current": "Current",
"technologies": "Technologies used:",
"items": [
{
"type": "work",
"title": "Developer - Work-study",
"organization": "Horoquartz",
"location": "Saint-Herblain, France",
"startDate": "2024",
"endDate": "Current",
"current": true,
"description": [
"Development of an update system for Horoquartz products",
"Design and implementation of REST APIs with Node.js and Go",
"Database management with PostgreSQL",
"Deployment with Docker and Kubernetes on Azure"
],
"technologies": ["Node.js", "Go", "PostgreSQL", "Docker", "Kubernetes", "Azure"]
},
{
"type": "education",
"title": "ISEN Nantes - Engineering Program",
"organization": "Institut Supérieur de l'Électronique et du Numérique",
"location": "Nantes, France",
"startDate": "2022",
"endDate": "2027",
"current": true,
"description": [
"Engineering training in computer science and new technologies",
"Specialization in cybersecurity",
"Team projects and project management"
],
"technologies": ["C", "C++", "Python", "Linux", "Networks"]
},
{
"type": "work",
"title": "Technical Support & VPS Admin",
"organization": "MercuryCloud",
"location": "Remote",
"startDate": "2021",
"endDate": "Current",
"description": [
"Technical support for game servers and VPS",
"Administration of CPanel and Plesk services",
"Management of the WHMCS system for billing",
"Virtualization and maintenance of Linux servers"
],
"technologies": ["Linux", "Virtualization", "CPanel", "Plesk", "WHMCS"]
},
{
"type": "achievement",
"title": "Robot Project - French Robotics Cup",
"organization": "Club Modelec ISEN",
"location": "Nantes, France",
"startDate": "2023",
"endDate": "2023",
"description": [
"Development of the robot control system",
"User interface with QT",
"Deployment on Raspberry Pi",
"Multidisciplinary teamwork"
],
"technologies": ["C++", "QT", "Raspberry Pi", "Linux"]
}
]
}
}

View File

@@ -0,0 +1,147 @@
{
"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",
"experience": {
"title": "Parcours & Expérience",
"description": "Mon évolution professionnelle et académique",
"current": "En cours",
"technologies": "Technologies utilisées :",
"items": [
{
"type": "work",
"title": "Développeur - Alternant",
"organization": "Horoquartz",
"location": "Saint-Herblain, 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"]
},
{
"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": "2027",
"current": true,
"description": [
"Formation d'ingénieur en informatique et nouvelles technologies",
"Spécialisation en cybersécurité",
"Projets en équipe et gestion de projet"
],
"technologies": ["C", "C++", "Python", "Linux", "Réseaux"]
},
{
"type": "work",
"title": "Support Technique & Admin VPS",
"organization": "MercuryCloud",
"location": "Remote",
"startDate": "2021",
"endDate": "Présent",
"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"]
},
{
"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"]
}
]
}
}

View File

@@ -12,7 +12,6 @@ import CV from "./components/CV.tsx";
import Menu from "./components/Menu.tsx"; import Menu from "./components/Menu.tsx";
import LoadingScreen from "./components/LoadingScreen.tsx"; import LoadingScreen from "./components/LoadingScreen.tsx";
import ParticlesBackground from "./components/ParticlesBackground.tsx"; import ParticlesBackground from "./components/ParticlesBackground.tsx";
import GitHubStatsSection from "./components/GitHubStatsSection.tsx";
import ContactSection from "./components/ContactSection.tsx"; import ContactSection from "./components/ContactSection.tsx";
import TimelineSection from "./components/TimelineSection.tsx"; import TimelineSection from "./components/TimelineSection.tsx";
import data from "./assets/DATA.ts"; import data from "./assets/DATA.ts";
@@ -21,23 +20,32 @@ import i18n from './i18n.js';
import {createRoot} from "react-dom/client"; import {createRoot} from "react-dom/client";
function App() { function App() {
const [theme, setTheme] = useState("light"); // Initialise le thème depuis localStorage ou détecte le thème système
const [theme, setTheme] = useState(() => {
const savedTheme = localStorage.getItem("theme");
if (savedTheme) {
return savedTheme;
}
// Détecte le thème système si aucun thème sauvegardé
const userPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
return userPrefersDark ? "dark" : "light";
});
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [showContent, setShowContent] = useState(false); const [showContent, setShowContent] = useState(false);
useEffect(() => { useEffect(() => {
// Detect if the user has their system in dark mode // Synchronise la classe 'dark' avec l'état 'theme' et sauvegarde dans localStorage
const userPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const html = document.documentElement;
if (theme === "dark") {
if (userPrefersDark) { html.classList.add("dark");
setTheme("dark"); } else {
document.documentElement.classList.add("dark"); html.classList.remove("dark");
} }
}, []); localStorage.setItem("theme", theme);
}, [theme]);
const toggleTheme = () => { const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light"); setTheme(theme === "light" ? "dark" : "light");
document.documentElement.classList.toggle("dark");
} }
const handleLoadingComplete = () => { const handleLoadingComplete = () => {
@@ -71,8 +79,6 @@ function App() {
<Skills skills={data.skills} /> <Skills skills={data.skills} />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="experience" /> <hr className="text-gray-800 dark:text-gray-200 mt-4" id="experience" />
<TimelineSection experience={data.experience} /> <TimelineSection experience={data.experience} />
<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" /> <hr className="text-gray-800 dark:text-gray-200 mt-4" id="projects" />
<Project projects={data.projects} /> <Project projects={data.projects} />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="contact" /> <hr className="text-gray-800 dark:text-gray-200 mt-4" id="contact" />

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import emailjs from '@emailjs/browser';
import { FaPaperPlane, FaUser, FaEnvelope, FaCommentDots, FaCheckCircle, FaSpinner } from 'react-icons/fa'; import { FaPaperPlane, FaUser, FaEnvelope, FaCommentDots, FaCheckCircle, FaSpinner } from 'react-icons/fa';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -11,7 +12,6 @@ interface FormData {
interface FormStatus { interface FormStatus {
type: 'success' | 'error' | 'loading' | null; type: 'success' | 'error' | 'loading' | null;
message: string;
} }
const ContactSection: React.FC = () => { const ContactSection: React.FC = () => {
@@ -22,7 +22,7 @@ const ContactSection: React.FC = () => {
subject: '', subject: '',
message: '' message: ''
}); });
const [status, setStatus] = useState<FormStatus>({ type: null, message: '' }); const [status, setStatus] = useState<FormStatus>({ type: null });
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
@@ -34,16 +34,31 @@ const ContactSection: React.FC = () => {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setStatus({ type: 'loading', message: 'Envoi en cours...' }); setStatus({ type: 'loading' });
// Simulation d'envoi (en production, remplacer par un vrai service) const serviceId = import.meta.env.VITE_EMAILJS_SERVICE_ID;
setTimeout(() => { const templateId = import.meta.env.VITE_EMAILJS_TEMPLATE_ID;
const templateParams = {
name: formData.name,
email: formData.email,
subject: formData.subject,
message: formData.message
};
try {
await emailjs.init({publicKey: import.meta.env.VITE_EMAILJS_USER_ID});
await emailjs.send(serviceId, templateId, templateParams);
setStatus({ setStatus({
type: 'success', type: 'success'
message: 'Message envoyé avec succès ! Je vous répondrai dans les plus brefs délais.'
}); });
setFormData({ name: '', email: '', subject: '', message: '' }); setFormData({ name: '', email: '', subject: '', message: '' });
}, 2000); } catch (error) {
console.error('EmailJS Error:', error);
setStatus({
type: 'error'
});
}
}; };
const isFormValid = formData.name && formData.email && formData.subject && formData.message; const isFormValid = formData.name && formData.email && formData.subject && formData.message;
@@ -52,10 +67,10 @@ const ContactSection: React.FC = () => {
<div className="max-w-4xl mx-auto mt-16" id="contact"> <div className="max-w-4xl mx-auto mt-16" id="contact">
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4"> <h2 className="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4">
📧 Contact {t('contact.title')}
</h2> </h2>
<p className="text-lg text-gray-600 dark:text-gray-400"> <p className="text-lg text-gray-600 dark:text-gray-400">
Une question ? Un projet ? N'hésitez pas à me contacter ! {t('contact.subtitle')}
</p> </p>
</div> </div>
@@ -64,14 +79,14 @@ const ContactSection: React.FC = () => {
<div className="space-y-8"> <div className="space-y-8">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover"> <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"> <h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-6">
Informations de contact {t('contact.info.title')}
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<FaEnvelope className="text-blue-500 text-lg" /> <FaEnvelope className="text-blue-500 text-lg" />
<div> <div>
<p className="text-sm text-gray-600 dark:text-gray-400">Email</p> <p className="text-sm text-gray-600 dark:text-gray-400">{t('contact.info.email')}</p>
<p className="text-gray-800 dark:text-gray-200">felix.marquet@isen-ouest.yncrea.fr</p> <p className="text-gray-800 dark:text-gray-200">felix.marquet@isen-ouest.yncrea.fr</p>
</div> </div>
</div> </div>
@@ -79,16 +94,16 @@ const ContactSection: React.FC = () => {
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<FaUser className="text-green-500 text-lg" /> <FaUser className="text-green-500 text-lg" />
<div> <div>
<p className="text-sm text-gray-600 dark:text-gray-400">Statut</p> <p className="text-sm text-gray-600 dark:text-gray-400">{t('contact.info.status')}</p>
<p className="text-gray-800 dark:text-gray-200">Étudiant ISEN - Alternant chez Horoquartz</p> <p className="text-gray-800 dark:text-gray-200">{t('contact.info.statusValue')}</p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<FaCommentDots className="text-purple-500 text-lg" /> <FaCommentDots className="text-purple-500 text-lg" />
<div> <div>
<p className="text-sm text-gray-600 dark:text-gray-400">Réponse</p> <p className="text-sm text-gray-600 dark:text-gray-400">{t('contact.info.response')}</p>
<p className="text-gray-800 dark:text-gray-200">Généralement sous 24h</p> <p className="text-gray-800 dark:text-gray-200">{t('contact.info.responseValue')}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -96,19 +111,10 @@ const ContactSection: React.FC = () => {
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover"> <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"> <h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-4">
Sujets d'intérêt {t('contact.subjects.title')}
</h3> </h3>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{[ {(t('contact.subjects.list', { returnObjects: true }) as string[]).map((subject, index) => (
'Développement Web',
'Administration Système',
'DevOps',
'Alternance',
'Projets Open Source',
'Homelab',
'Collaboration',
'Stage/Emploi'
].map((subject, index) => (
<span <span
key={index} 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" 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"
@@ -124,13 +130,13 @@ const ContactSection: React.FC = () => {
{/* Formulaire de contact */} {/* Formulaire de contact */}
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover"> <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"> <h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-6">
Envoyez-moi un message {t('contact.form.title')}
</h3> </h3>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> <label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Nom complet {t('contact.form.name')}
</label> </label>
<input <input
type="text" type="text"
@@ -139,14 +145,14 @@ const ContactSection: React.FC = () => {
value={formData.name} value={formData.name}
onChange={handleInputChange} 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" 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" placeholder={t('contact.form.namePlaceholder')}
required required
/> />
</div> </div>
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> <label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email {t('contact.form.email')}
</label> </label>
<input <input
type="email" type="email"
@@ -155,14 +161,14 @@ const ContactSection: React.FC = () => {
value={formData.email} value={formData.email}
onChange={handleInputChange} 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" 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" placeholder={t('contact.form.emailPlaceholder')}
required required
/> />
</div> </div>
<div> <div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> <label htmlFor="subject" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Sujet {t('contact.form.subject')}
</label> </label>
<input <input
type="text" type="text"
@@ -171,14 +177,14 @@ const ContactSection: React.FC = () => {
value={formData.subject} value={formData.subject}
onChange={handleInputChange} 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" 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" placeholder={t('contact.form.subjectPlaceholder')}
required required
/> />
</div> </div>
<div> <div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> <label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Message {t('contact.form.message')}
</label> </label>
<textarea <textarea
id="message" id="message"
@@ -187,7 +193,7 @@ const ContactSection: React.FC = () => {
onChange={handleInputChange} onChange={handleInputChange}
rows={5} 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" 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..." placeholder={t('contact.form.messagePlaceholder')}
required required
/> />
</div> </div>
@@ -204,12 +210,12 @@ const ContactSection: React.FC = () => {
{status.type === 'loading' ? ( {status.type === 'loading' ? (
<> <>
<FaSpinner className="animate-spin" /> <FaSpinner className="animate-spin" />
<span>Envoi en cours...</span> <span>{t('contact.form.sending')}</span>
</> </>
) : ( ) : (
<> <>
<FaPaperPlane /> <FaPaperPlane />
<span>Envoyer le message</span> <span>{t('contact.form.send')}</span>
</> </>
)} )}
</button> </button>
@@ -224,7 +230,11 @@ const ContactSection: React.FC = () => {
}`}> }`}>
{status.type === 'success' && <FaCheckCircle />} {status.type === 'success' && <FaCheckCircle />}
{status.type === 'loading' && <FaSpinner className="animate-spin" />} {status.type === 'loading' && <FaSpinner className="animate-spin" />}
<span className="text-sm">{status.message}</span> <span className="text-sm">
{status.type === 'success' && t('contact.form.success')}
{status.type === 'error' && t('contact.form.error')}
{status.type === 'loading' && t('contact.form.sending')}
</span>
</div> </div>
)} )}
</div> </div>

View File

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

@@ -10,7 +10,6 @@ function Menu() {
<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="#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="#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">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="#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">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> <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>

View File

@@ -47,10 +47,10 @@ const TimelineSection: React.FC<{ experience: TimelineItem[] }> = ({ experience
<div className="max-w-4xl mx-auto mt-16" id="experience"> <div className="max-w-4xl mx-auto mt-16" id="experience">
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4"> <h2 className="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4">
🎯 Parcours & Expérience {t('experience.title')}
</h2> </h2>
<p className="text-lg text-gray-600 dark:text-gray-400"> <p className="text-lg text-gray-600 dark:text-gray-400">
Mon évolution professionnelle et académique {t('experience.description')}
</p> </p>
</div> </div>
@@ -73,39 +73,39 @@ const TimelineSection: React.FC<{ experience: TimelineItem[] }> = ({ experience
</div> </div>
{/* Contenu */} {/* 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={`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"> <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"> <h3 className="text-xl font-bold text-gray-800 dark:text-gray-200">
{item.title} {t(`experience.items.${index}.title`)}
</h3> </h3>
<div className="flex items-center space-x-4 text-sm text-gray-600 dark:text-gray-400 mt-2 md:mt-0"> <div className="flex items-center space-x-4 text-sm text-gray-600 dark:text-gray-400 mt-2 md:mt-0">
{item.current && ( {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"> <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 {t('experience.current')}
</span> </span>
)} )}
<span className="flex items-center space-x-1"> <span className="flex items-center space-x-1">
<FaCalendarAlt className="text-xs" /> <FaCalendarAlt className="text-xs" />
<span>{item.startDate} - {item.endDate}</span> <span>{t(`experience.items.${index}.startDate`)} - {t(`experience.items.${index}.endDate`)}</span>
</span> </span>
</div> </div>
</div> </div>
<div className="flex items-center space-x-2 mb-3"> <div className="flex items-center space-x-2 mb-3">
<h4 className="text-lg font-semibold text-gray-700 dark:text-gray-300"> <h4 className="text-lg font-semibold text-gray-700 dark:text-gray-300">
{item.organization} {t(`experience.items.${index}.organization`)}
</h4> </h4>
<span className="flex items-center text-sm text-gray-500 dark:text-gray-400"> <span className="flex items-center text-sm text-gray-500 dark:text-gray-400">
<FaMapMarkerAlt className="mr-1" /> <FaMapMarkerAlt className="mr-1" />
{item.location} {t(`experience.items.${index}.location`)}
</span> </span>
</div> </div>
<ul className="space-y-1 mb-4"> <ul className="space-y-1 mb-4">
{item.description.map((desc, idx) => ( {item.description.map((_, idx) => (
<li key={idx} className="text-gray-600 dark:text-gray-400 flex items-start"> <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 className="text-blue-500 mr-2 mt-1.5"></span>
<span>{desc}</span> <span>{t(`experience.items.${index}.description.${idx}`)}</span>
</li> </li>
))} ))}
</ul> </ul>
@@ -113,15 +113,15 @@ const TimelineSection: React.FC<{ experience: TimelineItem[] }> = ({ experience
{item.technologies && ( {item.technologies && (
<div> <div>
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <h5 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Technologies utilisées : {t('experience.technologies')}
</h5> </h5>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{item.technologies.map((tech, idx) => ( {item.technologies.map((_, idx) => (
<span <span
key={idx} 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" className="px-3 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full"
> >
{tech} {t(`experience.items.${index}.technologies.${idx}`)}
</span> </span>
))} ))}
</div> </div>

View File

@@ -1,112 +1,19 @@
import i18n from "i18next"; import i18n from "i18next";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import HttpBackend from "i18next-http-backend";
i18n i18n
.use(initReactI18next) .use(HttpBackend)
.init({ .use(initReactI18next)
resources: { .init({
fr: { lng: navigator.language.startsWith('fr') ? 'fr' : 'en',
translation: { fallbackLng: "en",
"about.title": "A propos de moi", interpolation: {
"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.", escapeValue: false
"card.title": "Etudiant en 4e année a l'ISEN Nantes en alternance chez Horoquartz", },
"projects.title": "Mes projets", backend: {
"projects.Front end starter.description": "Mon starter personnel pour projet front end", loadPath: "/locales/{{lng}}/translation.json"
"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",
"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",
},
},
en: {
translation: {
"cv.title": "My CV",
"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.",
"nav.about": "About me",
"nav.projects": "My projects",
"nav.cv": "My resume",
"cv.path": "/CV-Felix-MARQUET-English.pdf",
"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, rewrit 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."
},
},
},
lng: navigator.language.startsWith('fr') ? 'fr' : 'en',
fallbackLng: "en",
interpolation: {
escapeValue: false
},
});
export default i18n; export default i18n;

View File

@@ -1 +1,2 @@
@import "tailwindcss"; @import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

View File

@@ -17,7 +17,6 @@
--color-green-50: oklch(98.2% 0.018 155.826); --color-green-50: oklch(98.2% 0.018 155.826);
--color-green-100: oklch(96.2% 0.044 156.743); --color-green-100: oklch(96.2% 0.044 156.743);
--color-green-200: oklch(92.5% 0.084 155.995); --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-500: oklch(72.3% 0.219 149.579);
--color-green-800: oklch(44.8% 0.119 151.328); --color-green-800: oklch(44.8% 0.119 151.328);
--color-green-900: oklch(39.3% 0.095 152.535); --color-green-900: oklch(39.3% 0.095 152.535);
@@ -47,7 +46,6 @@
--color-white: #fff; --color-white: #fff;
--spacing: 0.25rem; --spacing: 0.25rem;
--container-xs: 20rem; --container-xs: 20rem;
--container-3xl: 48rem;
--container-4xl: 56rem; --container-4xl: 56rem;
--container-6xl: 72rem; --container-6xl: 72rem;
--text-xs: 0.75rem; --text-xs: 0.75rem;
@@ -73,11 +71,9 @@
--font-weight-bold: 700; --font-weight-bold: 700;
--font-weight-extrabold: 800; --font-weight-extrabold: 800;
--leading-relaxed: 1.625; --leading-relaxed: 1.625;
--radius-sm: 0.25rem;
--radius-md: 0.375rem; --radius-md: 0.375rem;
--radius-lg: 0.5rem; --radius-lg: 0.5rem;
--radius-xl: 0.75rem; --radius-xl: 0.75rem;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--animate-spin: spin 1s linear infinite; --animate-spin: spin 1s linear infinite;
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--animate-bounce: bounce 1s infinite; --animate-bounce: bounce 1s infinite;
@@ -401,18 +397,9 @@
.h-6 { .h-6 {
height: calc(var(--spacing) * 6); height: calc(var(--spacing) * 6);
} }
.h-8 {
height: calc(var(--spacing) * 8);
}
.h-20 {
height: calc(var(--spacing) * 20);
}
.h-64 { .h-64 {
height: calc(var(--spacing) * 64); height: calc(var(--spacing) * 64);
} }
.h-full {
height: 100%;
}
.min-h-screen { .min-h-screen {
min-height: 100vh; min-height: 100vh;
} }
@@ -422,9 +409,6 @@
.w-1 { .w-1 {
width: calc(var(--spacing) * 1); width: calc(var(--spacing) * 1);
} }
.w-1\/3 {
width: calc(1/3 * 100%);
}
.w-3 { .w-3 {
width: calc(var(--spacing) * 3); width: calc(var(--spacing) * 3);
} }
@@ -446,9 +430,6 @@
.w-full { .w-full {
width: 100%; width: 100%;
} }
.max-w-3xl {
max-width: var(--container-3xl);
}
.max-w-4xl { .max-w-4xl {
max-width: var(--container-4xl); max-width: var(--container-4xl);
} }
@@ -489,12 +470,6 @@
.grid-cols-1 { .grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr)); 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-col {
flex-direction: column; flex-direction: column;
} }
@@ -525,9 +500,6 @@
.gap-2 { .gap-2 {
gap: calc(var(--spacing) * 2); gap: calc(var(--spacing) * 2);
} }
.gap-3 {
gap: calc(var(--spacing) * 3);
}
.gap-4 { .gap-4 {
gap: calc(var(--spacing) * 4); gap: calc(var(--spacing) * 4);
} }
@@ -599,9 +571,6 @@
.overflow-hidden { .overflow-hidden {
overflow: hidden; overflow: hidden;
} }
.rounded {
border-radius: 0.25rem;
}
.rounded-full { .rounded-full {
border-radius: calc(infinity * 1px); border-radius: calc(infinity * 1px);
} }
@@ -611,9 +580,6 @@
.rounded-md { .rounded-md {
border-radius: var(--radius-md); border-radius: var(--radius-md);
} }
.rounded-sm {
border-radius: var(--radius-sm);
}
.rounded-xl { .rounded-xl {
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
} }
@@ -688,12 +654,6 @@
.bg-green-100 { .bg-green-100 {
background-color: var(--color-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 { .bg-green-500 {
background-color: var(--color-green-500); background-color: var(--color-green-500);
} }
@@ -716,14 +676,6 @@
--tw-gradient-position: to bottom right in oklab; --tw-gradient-position: to bottom right in oklab;
background-image: linear-gradient(var(--tw-gradient-stops)); 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 { .from-blue-900 {
--tw-gradient-from: var(--color-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)); --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));
@@ -737,10 +689,6 @@
--tw-gradient-to: var(--color-indigo-900); --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)); --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 { .p-1 {
padding: calc(var(--spacing) * 1); padding: calc(var(--spacing) * 1);
} }
@@ -759,9 +707,6 @@
.p-6 { .p-6 {
padding: calc(var(--spacing) * 6); padding: calc(var(--spacing) * 6);
} }
.p-8 {
padding: calc(var(--spacing) * 8);
}
.px-2 { .px-2 {
padding-inline: calc(var(--spacing) * 2); padding-inline: calc(var(--spacing) * 2);
} }
@@ -910,9 +855,6 @@
color: color-mix(in oklab, var(--color-white) 80%, transparent); color: color-mix(in oklab, var(--color-white) 80%, transparent);
} }
} }
.text-yellow-500 {
color: var(--color-yellow-500);
}
.opacity-0 { .opacity-0 {
opacity: 0%; opacity: 0%;
} }
@@ -966,10 +908,6 @@
--tw-duration: 1000ms; --tw-duration: 1000ms;
transition-duration: 1000ms; transition-duration: 1000ms;
} }
.ease-out {
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
}
.hover\:bg-blue-700 { .hover\:bg-blue-700 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@@ -1105,11 +1043,6 @@
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
} }
.md\:grid-cols-4 {
@media (width >= 48rem) {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.md\:flex-row { .md\:flex-row {
@media (width >= 48rem) { @media (width >= 48rem) {
flex-direction: row; flex-direction: row;
@@ -1168,22 +1101,22 @@
} }
} }
.dark\:border-gray-600 { .dark\:border-gray-600 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
border-color: var(--color-gray-600); border-color: var(--color-gray-600);
} }
} }
.dark\:border-gray-700 { .dark\:border-gray-700 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
border-color: var(--color-gray-700); border-color: var(--color-gray-700);
} }
} }
.dark\:bg-blue-900 { .dark\:bg-blue-900 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
background-color: var(--color-blue-900); background-color: var(--color-blue-900);
} }
} }
.dark\:bg-blue-900\/20 { .dark\:bg-blue-900\/20 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
background-color: color-mix(in srgb, oklch(37.9% 0.146 265.522) 20%, transparent); background-color: color-mix(in srgb, oklch(37.9% 0.146 265.522) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-blue-900) 20%, transparent); background-color: color-mix(in oklab, var(--color-blue-900) 20%, transparent);
@@ -1191,27 +1124,27 @@
} }
} }
.dark\:bg-gray-600 { .dark\:bg-gray-600 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
background-color: var(--color-gray-600); background-color: var(--color-gray-600);
} }
} }
.dark\:bg-gray-700 { .dark\:bg-gray-700 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
background-color: var(--color-gray-700); background-color: var(--color-gray-700);
} }
} }
.dark\:bg-gray-800 { .dark\:bg-gray-800 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
background-color: var(--color-gray-800); background-color: var(--color-gray-800);
} }
} }
.dark\:bg-gray-900 { .dark\:bg-gray-900 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
background-color: var(--color-gray-900); background-color: var(--color-gray-900);
} }
} }
.dark\:bg-gray-900\/20 { .dark\:bg-gray-900\/20 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 20%, transparent); background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-gray-900) 20%, transparent); background-color: color-mix(in oklab, var(--color-gray-900) 20%, transparent);
@@ -1219,12 +1152,12 @@
} }
} }
.dark\:bg-green-900 { .dark\:bg-green-900 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
background-color: var(--color-green-900); background-color: var(--color-green-900);
} }
} }
.dark\:bg-green-900\/20 { .dark\:bg-green-900\/20 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
background-color: color-mix(in srgb, oklch(39.3% 0.095 152.535) 20%, transparent); background-color: color-mix(in srgb, oklch(39.3% 0.095 152.535) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-green-900) 20%, transparent); background-color: color-mix(in oklab, var(--color-green-900) 20%, transparent);
@@ -1232,7 +1165,7 @@
} }
} }
.dark\:bg-purple-900\/20 { .dark\:bg-purple-900\/20 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
background-color: color-mix(in srgb, oklch(38.1% 0.176 304.987) 20%, transparent); background-color: color-mix(in srgb, oklch(38.1% 0.176 304.987) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-purple-900) 20%, transparent); background-color: color-mix(in oklab, var(--color-purple-900) 20%, transparent);
@@ -1240,47 +1173,47 @@
} }
} }
.dark\:bg-red-900 { .dark\:bg-red-900 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
background-color: var(--color-red-900); background-color: var(--color-red-900);
} }
} }
.dark\:text-blue-200 { .dark\:text-blue-200 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
color: var(--color-blue-200); color: var(--color-blue-200);
} }
} }
.dark\:text-gray-200 { .dark\:text-gray-200 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
color: var(--color-gray-200); color: var(--color-gray-200);
} }
} }
.dark\:text-gray-300 { .dark\:text-gray-300 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
color: var(--color-gray-300); color: var(--color-gray-300);
} }
} }
.dark\:text-gray-400 { .dark\:text-gray-400 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
color: var(--color-gray-400); color: var(--color-gray-400);
} }
} }
.dark\:text-green-200 { .dark\:text-green-200 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
color: var(--color-green-200); color: var(--color-green-200);
} }
} }
.dark\:text-red-200 { .dark\:text-red-200 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
color: var(--color-red-200); color: var(--color-red-200);
} }
} }
.dark\:text-white { .dark\:text-white {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
color: var(--color-white); color: var(--color-white);
} }
} }
.dark\:shadow-gray-200 { .dark\:shadow-gray-200 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
--tw-shadow-color: oklch(92.8% 0.006 264.531); --tw-shadow-color: oklch(92.8% 0.006 264.531);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
--tw-shadow-color: color-mix(in oklab, var(--color-gray-200) var(--tw-shadow-alpha), transparent); --tw-shadow-color: color-mix(in oklab, var(--color-gray-200) var(--tw-shadow-alpha), transparent);
@@ -1288,7 +1221,7 @@
} }
} }
.dark\:hover\:bg-gray-200 { .dark\:hover\:bg-gray-200 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
background-color: var(--color-gray-200); background-color: var(--color-gray-200);
@@ -1297,7 +1230,7 @@
} }
} }
.dark\:hover\:text-blue-400 { .dark\:hover\:text-blue-400 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
color: var(--color-blue-400); color: var(--color-blue-400);
@@ -1306,7 +1239,7 @@
} }
} }
.dark\:hover\:text-gray-200 { .dark\:hover\:text-gray-200 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
color: var(--color-gray-200); color: var(--color-gray-200);
@@ -1315,7 +1248,7 @@
} }
} }
.dark\:hover\:text-gray-800 { .dark\:hover\:text-gray-800 {
@media (prefers-color-scheme: dark) { &:where(.dark, .dark *) {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
color: var(--color-gray-800); color: var(--color-gray-800);
@@ -1509,10 +1442,6 @@
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
} }
@property --tw-ease {
syntax: "*";
inherits: false;
}
@property --tw-scale-x { @property --tw-scale-x {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@@ -1612,7 +1541,6 @@
--tw-backdrop-saturate: initial; --tw-backdrop-saturate: initial;
--tw-backdrop-sepia: initial; --tw-backdrop-sepia: initial;
--tw-duration: initial; --tw-duration: initial;
--tw-ease: initial;
--tw-scale-x: 1; --tw-scale-x: 1;
--tw-scale-y: 1; --tw-scale-y: 1;
--tw-scale-z: 1; --tw-scale-z: 1;

View File

@@ -1,6 +1,5 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { export default {
darkMode: 'class',
content: ["./src/**/*.{js,jsx,ts,tsx}"], content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: { theme: {
extend: {}, extend: {},

View File

@@ -19,5 +19,5 @@
"types": ["@simonsmith/cypress-image-snapshot/types"], "types": ["@simonsmith/cypress-image-snapshot/types"],
"outDir": "./dist", "outDir": "./dist",
}, },
"include": ["src"] "include": ["src", "env.d.ts"]
} }