mirror of
https://github.com/BreizhHardware/portfolio.git
synced 2026-01-18 16:37:22 +01:00
Merge pull request #179 from BreizhHardware/feat/global-improvment
Feat/global improvment
This commit is contained in:
19
.github/workflows/deploy-test.yml
vendored
19
.github/workflows/deploy-test.yml
vendored
@@ -17,26 +17,35 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies and build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run build
|
||||
|
||||
- name: Install Cypress binary
|
||||
run: pnpx cypress install
|
||||
|
||||
- name: Install http-server
|
||||
run: npm install --save-dev http-server
|
||||
run: pnpm add -D http-server
|
||||
|
||||
- name: Start HTTP server
|
||||
run: npx http-server ./dist -p 8080 &
|
||||
run: pnpx http-server ./dist -p 8080 &
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Run Cypress tests and Browserslist
|
||||
run: npm run test
|
||||
run: pnpm run test
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:8080
|
||||
|
||||
|
||||
19
.github/workflows/deploy.yml
vendored
19
.github/workflows/deploy.yml
vendored
@@ -17,26 +17,35 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies and build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run build
|
||||
|
||||
- name: Install Cypress binary
|
||||
run: pnpx cypress install
|
||||
|
||||
- name: Install http-server
|
||||
run: npm install --save-dev http-server
|
||||
run: pnpm add -D http-server
|
||||
|
||||
- name: Start HTTP server
|
||||
run: npx http-server ./dist -p 8080 &
|
||||
run: pnpx http-server ./dist -p 8080 &
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Run Cypress tests and Browserslist
|
||||
run: npm run test
|
||||
run: pnpm run test
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:8080
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -49,3 +49,4 @@ dist
|
||||
/cypress/fixtures/__tests__/require-vanilla-test.js
|
||||
/src/components/SkillCard.js
|
||||
/src/components/Skills.js
|
||||
.env
|
||||
|
||||
12
env.d.ts
vendored
Normal file
12
env.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
<meta name="generator" content="React" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="darkreader" content="dark" />
|
||||
<script defer data-domain="mrqt.fr" src="https://plausible.mrqt.fr/js/script.outbound-links.js"></script>
|
||||
<meta
|
||||
name="description"
|
||||
content="Portfolio de Félix MARQUET"
|
||||
|
||||
5873
package-lock.json
generated
5873
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@@ -3,23 +3,30 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@emailjs/browser": "^4.4.1",
|
||||
"@tailwindcss/cli": "^4.1.12",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.11",
|
||||
"@types/react-dom": "19.1.8",
|
||||
"aos": "^2.3.4",
|
||||
"browserslist": "^4.25.1",
|
||||
"browserslist": "^4.25.3",
|
||||
"browserslist-useragent": "^4.0.0",
|
||||
"cypress": "^14.5.3",
|
||||
"i18next": "^25.3.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"cypress": "^15.0.0",
|
||||
"i18next": "^25.4.2",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-i18next": "^15.7.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"sass": "^1.86.0",
|
||||
"typescript": "^5.8.3",
|
||||
"react-particles": "^2.12.2",
|
||||
"sass": "^1.91.0",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"tsparticles-slim": "^2.12.0",
|
||||
"typescript": "^5.9.2",
|
||||
"web-vitals": "^5.1.0",
|
||||
"yarn": "^1.22.22"
|
||||
},
|
||||
@@ -27,10 +34,10 @@
|
||||
"start": "vite",
|
||||
"build": "npx tsc && vite build",
|
||||
"serve": "vite preview",
|
||||
"build:css": "tailwindcss build -i src/index.css -o src/output.css",
|
||||
"build:css": "npx @tailwindcss/cli -i src/index.css -o src/output.css",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run",
|
||||
"test": "browserslist && cypress run"
|
||||
"test": "browserslist"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -53,9 +60,10 @@
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@simonsmith/cypress-image-snapshot": "^10.0.2",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"vite": "^7.0.6",
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"@vitejs/plugin-react": "^5.0.1",
|
||||
"prettier": "^3.6.2",
|
||||
"vite": "^7.1.3",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
4556
pnpm-lock.yaml
generated
Normal file
4556
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
148
public/locales/en/translation.json
Normal file
148
public/locales/en/translation.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
147
public/locales/fr/translation.json
Normal file
147
public/locales/fr/translation.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
51
src/App.jsx
51
src/App.jsx
@@ -10,27 +10,49 @@ 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 { useTranslation } from "react-i18next";
|
||||
import i18n from './i18n.js';
|
||||
import {createRoot} from "react-dom/client";
|
||||
|
||||
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 [showContent, setShowContent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Detect if the user has their system in dark mode
|
||||
const userPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (userPrefersDark) {
|
||||
setTheme("dark");
|
||||
document.documentElement.classList.add("dark");
|
||||
// Synchronise la classe 'dark' avec l'état 'theme' et sauvegarde dans localStorage
|
||||
const html = document.documentElement;
|
||||
if (theme === "dark") {
|
||||
html.classList.add("dark");
|
||||
} else {
|
||||
html.classList.remove("dark");
|
||||
}
|
||||
}, []);
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === "light" ? "dark" : "light");
|
||||
document.documentElement.classList.toggle("dark");
|
||||
}
|
||||
|
||||
const handleLoadingComplete = () => {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setTimeout(() => setShowContent(true), 100);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
useTranslation();
|
||||
@@ -39,9 +61,14 @@ function App() {
|
||||
i18n.changeLanguage(i18n.language === "fr" ? "en" : "fr");
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingScreen onLoadingComplete={handleLoadingComplete} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen py-10 bg-gray-100 dark:bg-gray-900 m-0" data-testid="root">
|
||||
<div className="mobile-margin">
|
||||
<ParticlesBackground isDark={theme === "dark"} />
|
||||
<div className={`mobile-margin transition-all duration-1000 ${showContent ? 'animate-fadeInUp' : 'opacity-0'}`}>
|
||||
<Menu />
|
||||
<div data-aos="face-down" data-aos-duration="800" id="top">
|
||||
<Card name={data.name} title={data.title} social={data.social} />
|
||||
@@ -50,8 +77,12 @@ function App() {
|
||||
<div data-aos="fade-up" data-aos-duration="800" data-aos-delay="400">
|
||||
<About />
|
||||
<Skills skills={data.skills} />
|
||||
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="experience" />
|
||||
<TimelineSection experience={data.experience} />
|
||||
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="projects" />
|
||||
<Project projects={data.projects} />
|
||||
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="contact" />
|
||||
<ContactSection />
|
||||
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="cv" />
|
||||
<CV />
|
||||
<Footer />
|
||||
|
||||
@@ -61,6 +61,74 @@ const Felix = {
|
||||
skillLevel: 80,
|
||||
}
|
||||
],
|
||||
experience: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'work',
|
||||
title: 'Développeur - Alternant',
|
||||
organization: 'Horoquartz',
|
||||
location: 'Saint-Herblain, France',
|
||||
startDate: '2024',
|
||||
endDate: 'Present',
|
||||
current: true,
|
||||
description: [
|
||||
'Développement d\'un système de mise à jour pour les produits Horoquartz',
|
||||
'Conception et implémentation d\'APIs REST avec Node.js et Go',
|
||||
'Gestion des bases de données PostgreSQL',
|
||||
'Déploiement avec Docker et Kubernetes sur Azure'
|
||||
],
|
||||
technologies: ['Node.js', 'Go', 'PostgreSQL', 'Docker', 'Kubernetes', 'Azure']
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'education',
|
||||
title: 'ISEN Nantes - Cycle Ingénieur',
|
||||
organization: 'Institut Supérieur de l\'Électronique et du Numérique',
|
||||
location: 'Nantes, France',
|
||||
startDate: '2022',
|
||||
endDate: '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']
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'work',
|
||||
title: 'Support Technique & Admin VPS',
|
||||
organization: 'MercuryCloud',
|
||||
location: 'Remote',
|
||||
startDate: '2023',
|
||||
endDate: 'Present',
|
||||
current: true,
|
||||
description: [
|
||||
'Support technique pour serveurs de jeu et VPS',
|
||||
'Administration des services CPanel et Plesk',
|
||||
'Gestion du système WHMCS pour la facturation',
|
||||
'Virtualisation et maintenance des serveurs Linux'
|
||||
],
|
||||
technologies: ['Linux', 'Virtualisation', 'CPanel', 'Plesk', 'WHMCS']
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'achievement',
|
||||
title: 'Projet Robot - Coupe de France de Robotique',
|
||||
organization: 'Club Modelec ISEN',
|
||||
location: 'Nantes, France',
|
||||
startDate: '2023',
|
||||
endDate: '2023',
|
||||
description: [
|
||||
'Développement du système de contrôle du robot',
|
||||
'Interface utilisateur avec QT',
|
||||
'Déploiement sur Raspberry Pi',
|
||||
'Travail en équipe multidisciplinaire'
|
||||
],
|
||||
technologies: ['C++', 'QT', 'Raspberry Pi', 'Linux']
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
title: "Front end starter",
|
||||
@@ -95,7 +163,7 @@ const Felix = {
|
||||
{
|
||||
title: "Github NTFY",
|
||||
description: "Projet de notification pour les releases github et dockerhub qui envoie des notifications sur ntfy, gotify et discord",
|
||||
tags: ["Python", "Docker", "Github Actions"],
|
||||
tags: ["Python", "Rust", "Nuxt", "Docker", "Github Actions"],
|
||||
link: "https://github.com/BreizhHardware/ntfy_alerts"
|
||||
},
|
||||
{
|
||||
@@ -103,6 +171,12 @@ const Felix = {
|
||||
description: "Projet de jeu de simulation de comportement de banc de poisson en C++ avec la librairie SLD2, avec support du multijoueur.",
|
||||
tags: ["C++", "SDL2", "Multiplayer"],
|
||||
link: "https://github.com/BreizhHardware/bloubloulespoissons"
|
||||
},
|
||||
{
|
||||
title: "Alternance Horoquartz",
|
||||
description: "Développement d'un système de mise à jour pour les produits Horoquartz",
|
||||
tags: ["Node.js", "Go", "PostgreSQL", "Docker", "Kubernetes", "Azure"],
|
||||
link: "https://www.horoquartz.com/"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
262
src/components/ContactSection.tsx
Normal file
262
src/components/ContactSection.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
import React, { useState } from 'react';
|
||||
import emailjs from '@emailjs/browser';
|
||||
import { FaPaperPlane, FaUser, FaEnvelope, FaCommentDots, FaCheckCircle, FaSpinner } from 'react-icons/fa';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface FormData {
|
||||
name: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface FormStatus {
|
||||
type: 'success' | 'error' | 'loading' | null;
|
||||
}
|
||||
|
||||
const ContactSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
});
|
||||
const [status, setStatus] = useState<FormStatus>({ type: null });
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus({ type: 'loading' });
|
||||
|
||||
const serviceId = import.meta.env.VITE_EMAILJS_SERVICE_ID;
|
||||
const templateId = import.meta.env.VITE_EMAILJS_TEMPLATE_ID;
|
||||
const userId = import.meta.env.VITE_EMAILJS_USER_ID;
|
||||
|
||||
if (!serviceId || !templateId || !userId) {
|
||||
console.error('Missing required EmailJS environment variables:', {
|
||||
VITE_EMAILJS_SERVICE_ID: serviceId,
|
||||
VITE_EMAILJS_TEMPLATE_ID: templateId,
|
||||
VITE_EMAILJS_USER_ID: userId
|
||||
});
|
||||
setStatus({ type: 'error' });
|
||||
return;
|
||||
}
|
||||
const templateParams = {
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
subject: formData.subject,
|
||||
message: formData.message
|
||||
};
|
||||
|
||||
try {
|
||||
const publicKey = import.meta.env.VITE_EMAILJS_USER_ID;
|
||||
if (!publicKey) {
|
||||
console.error('EmailJS Error: VITE_EMAILJS_USER_ID is not defined.');
|
||||
setStatus({ type: 'error' });
|
||||
return;
|
||||
}
|
||||
await emailjs.init({ publicKey });
|
||||
await emailjs.send(serviceId, templateId, templateParams);
|
||||
setStatus({
|
||||
type: 'success'
|
||||
});
|
||||
setFormData({ name: '', email: '', subject: '', message: '' });
|
||||
} catch (error) {
|
||||
console.error('EmailJS Error:', error);
|
||||
setStatus({
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isFormValid = formData.name && formData.email && formData.subject && formData.message;
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto mt-16" id="contact">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4">
|
||||
{t('contact.title')}
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400">
|
||||
{t('contact.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
{/* Informations de contact */}
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
|
||||
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-6">
|
||||
{t('contact.info.title')}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<FaEnvelope className="text-blue-500 text-lg" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('contact.info.email')}</p>
|
||||
<p className="text-gray-800 dark:text-gray-200">felix.marquet@isen-ouest.yncrea.fr</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<FaUser className="text-green-500 text-lg" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('contact.info.status')}</p>
|
||||
<p className="text-gray-800 dark:text-gray-200">{t('contact.info.statusValue')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<FaCommentDots className="text-purple-500 text-lg" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('contact.info.response')}</p>
|
||||
<p className="text-gray-800 dark:text-gray-200">{t('contact.info.responseValue')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
|
||||
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-4">
|
||||
{t('contact.subjects.title')}
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(t('contact.subjects.list', { returnObjects: true }) as string[]).map((subject, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-3 py-1 text-sm bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full animate-fadeInUp"
|
||||
style={{ animationDelay: `${index * 100}ms` }}
|
||||
>
|
||||
{subject}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formulaire de contact */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
|
||||
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-6">
|
||||
{t('contact.form.title')}
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('contact.form.name')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-200 transition-all"
|
||||
placeholder={t('contact.form.namePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('contact.form.email')}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-200 transition-all"
|
||||
placeholder={t('contact.form.emailPlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('contact.form.subject')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
name="subject"
|
||||
value={formData.subject}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-200 transition-all"
|
||||
placeholder={t('contact.form.subjectPlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('contact.form.message')}
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
value={formData.message}
|
||||
onChange={handleInputChange}
|
||||
rows={5}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-200 transition-all resize-none"
|
||||
placeholder={t('contact.form.messagePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isFormValid || status.type === 'loading'}
|
||||
className={`w-full flex items-center justify-center space-x-2 py-3 px-4 rounded-md text-white font-medium transition-all ${
|
||||
isFormValid && status.type !== 'loading'
|
||||
? 'bg-blue-600 hover:bg-blue-700 active:transform active:scale-95'
|
||||
: 'bg-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{status.type === 'loading' ? (
|
||||
<>
|
||||
<FaSpinner className="animate-spin" />
|
||||
<span>{t('contact.form.sending')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaPaperPlane />
|
||||
<span>{t('contact.form.send')}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Status message */}
|
||||
{status.type && (
|
||||
<div className={`mt-4 p-3 rounded-md flex items-center space-x-2 animate-fadeInUp ${
|
||||
status.type === 'success' ? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200' :
|
||||
status.type === 'error' ? 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200' :
|
||||
'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
|
||||
}`}>
|
||||
{status.type === 'success' && <FaCheckCircle />}
|
||||
{status.type === 'loading' && <FaSpinner className="animate-spin" />}
|
||||
<span className="text-sm">
|
||||
{status.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>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactSection;
|
||||
216
src/components/HomelabSection.tsx
Normal file
216
src/components/HomelabSection.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FaServer, FaDocker, FaLinux, FaNetworkWired, FaHdd, FaMicrochip } from 'react-icons/fa';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ServerSpec {
|
||||
name: string;
|
||||
model: string;
|
||||
cpu: string;
|
||||
ram: string;
|
||||
storage: string;
|
||||
os: string;
|
||||
services: string[];
|
||||
status: 'online' | 'offline' | 'maintenance';
|
||||
}
|
||||
|
||||
const HomelabSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'servers' | 'services'>('overview');
|
||||
|
||||
const servers: ServerSpec[] = [
|
||||
{
|
||||
name: "DELL T320",
|
||||
model: "PowerEdge T320",
|
||||
cpu: "Intel Xeon E5-2400 series",
|
||||
ram: "32GB DDR3",
|
||||
storage: "4x 1TB HDD RAID 10",
|
||||
os: "Proxmox VE",
|
||||
services: ["Proxmox", "Docker", "VM Management", "Backup Server"],
|
||||
status: 'online'
|
||||
},
|
||||
{
|
||||
name: "DELL T330",
|
||||
model: "PowerEdge T330",
|
||||
cpu: "Intel Xeon E3-1200 series",
|
||||
ram: "64GB DDR4",
|
||||
storage: "6x 2TB HDD + 2x 500GB SSD",
|
||||
os: "Proxmox VE",
|
||||
services: ["Media Server", "Development Environment", "CI/CD", "Monitoring"],
|
||||
status: 'online'
|
||||
}
|
||||
];
|
||||
|
||||
const services = [
|
||||
{ name: "Proxmox Cluster", icon: <FaServer />, description: "Virtualisation et orchestration" },
|
||||
{ name: "Docker Swarm", icon: <FaDocker />, description: "Conteneurisation des services" },
|
||||
{ name: "Monitoring Stack", icon: <FaNetworkWired />, description: "Grafana + Prometheus + Alertmanager" },
|
||||
{ name: "Backup Solution", icon: <FaHdd />, description: "Sauvegarde automatisée 3-2-1" },
|
||||
{ name: "Development Lab", icon: <FaMicrochip />, description: "Environnements de développement isolés" },
|
||||
{ name: "Media Center", icon: <FaLinux />, description: "Streaming et gestion multimédia" }
|
||||
];
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'online': return 'bg-green-500';
|
||||
case 'offline': return 'bg-red-500';
|
||||
case 'maintenance': return 'bg-yellow-500';
|
||||
default: return 'bg-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'online': return 'En ligne';
|
||||
case 'offline': return 'Hors ligne';
|
||||
case 'maintenance': return 'Maintenance';
|
||||
default: return 'Inconnu';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto mt-16">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4">
|
||||
🏠 Mon Homelab
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400">
|
||||
Infrastructure personnelle et laboratoire de développement
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Navigation tabs */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="bg-gray-200 dark:bg-gray-700 rounded-lg p-1">
|
||||
{['overview', 'servers', 'services'].map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab as any)}
|
||||
className={`px-6 py-2 rounded-md text-sm font-medium transition-all ${
|
||||
activeTab === tab
|
||||
? 'bg-white dark:bg-gray-600 text-gray-800 dark:text-gray-200 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
{tab === 'overview' ? 'Vue d\'ensemble' : tab === 'servers' ? 'Serveurs' : 'Services'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content based on active tab */}
|
||||
<div className="animate-fadeInUp">
|
||||
{activeTab === 'overview' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">2</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">Serveurs</p>
|
||||
</div>
|
||||
<FaServer className="text-blue-500 text-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">96GB</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">RAM Total</p>
|
||||
</div>
|
||||
<FaMicrochip className="text-green-500 text-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">16TB</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">Stockage</p>
|
||||
</div>
|
||||
<FaHdd className="text-purple-500 text-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-gray-800 dark:text-gray-200">24/7</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">Uptime</p>
|
||||
</div>
|
||||
<FaNetworkWired className="text-orange-500 text-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'servers' && (
|
||||
<div className="space-y-6">
|
||||
{servers.map((server, index) => (
|
||||
<div key={index} className="bg-white dark:bg-gray-800 rounded-lg shadow-lg card-hover overflow-hidden">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<FaServer className="text-2xl text-blue-500" />
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200">{server.name}</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{server.model}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-3 h-3 rounded-full ${getStatusColor(server.status)}`}></div>
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
{getStatusText(server.status)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">CPU</p>
|
||||
<p className="text-sm text-gray-800 dark:text-gray-200">{server.cpu}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">RAM</p>
|
||||
<p className="text-sm text-gray-800 dark:text-gray-200">{server.ram}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">Stockage</p>
|
||||
<p className="text-sm text-gray-800 dark:text-gray-200">{server.storage}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Services</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{server.services.map((service, idx) => (
|
||||
<span key={idx} className="px-3 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full">
|
||||
{service}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'services' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{services.map((service, index) => (
|
||||
<div key={index} className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover">
|
||||
<div className="flex items-center space-x-3 mb-3">
|
||||
<div className="text-2xl text-blue-500">{service.icon}</div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200">{service.name}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{service.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomelabSection;
|
||||
57
src/components/LoadingScreen.tsx
Normal file
57
src/components/LoadingScreen.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface LoadingScreenProps {
|
||||
onLoadingComplete: () => void;
|
||||
}
|
||||
|
||||
const LoadingScreen: React.FC<LoadingScreenProps> = ({ onLoadingComplete }) => {
|
||||
const [displayText, setDisplayText] = useState('');
|
||||
const [showCursor, setShowCursor] = useState(true);
|
||||
const fullText = "Félix MARQUET";
|
||||
const [isComplete, setIsComplete] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let currentIndex = 0;
|
||||
const typingInterval = setInterval(() => {
|
||||
if (currentIndex <= fullText.length) {
|
||||
setDisplayText(fullText.substring(0, currentIndex));
|
||||
currentIndex++;
|
||||
} else {
|
||||
clearInterval(typingInterval);
|
||||
setIsComplete(true);
|
||||
setTimeout(() => {
|
||||
onLoadingComplete();
|
||||
}, 1000);
|
||||
}
|
||||
}, 150);
|
||||
|
||||
return () => clearInterval(typingInterval);
|
||||
}, [fullText, onLoadingComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
const cursorInterval = setInterval(() => {
|
||||
setShowCursor(prev => !prev);
|
||||
}, 500);
|
||||
|
||||
return () => clearInterval(cursorInterval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 flex items-center justify-center z-50 transition-all duration-1000">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl md:text-6xl font-bold text-white mb-8">
|
||||
{displayText}
|
||||
{showCursor && !isComplete && <span className="animate-pulse">|</span>}
|
||||
</div>
|
||||
<div className="flex space-x-2 justify-center">
|
||||
<div className="w-3 h-3 bg-white rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
|
||||
<div className="w-3 h-3 bg-white rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
|
||||
<div className="w-3 h-3 bg-white rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
|
||||
</div>
|
||||
<p className="text-white/80 mt-4 text-lg">Chargement du portfolio...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingScreen;
|
||||
@@ -5,12 +5,14 @@ import {useTranslation} from "react-i18next";
|
||||
function Menu() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<nav className="fixed top-0 w-full bg-gray-100 dark:bg-gray-900 z-50 shadow-md shadow-gray-800 dark:shadow-gray-200 menu-hidden">
|
||||
<nav className="fixed top-0 w-full bg-gray-100 dark:bg-gray-900 z-50 shadow-md shadow-gray-800 dark:shadow-gray-200 menu-hidden backdrop-blur-sm">
|
||||
<ul className="flex justify-around">
|
||||
<li><a href="#top" className="text-gray-800 dark:text-gray-200">Félix MARQUET</a></li>
|
||||
<li><a href="#about" className="text-gray-800 dark:text-gray-200">{t('nav.about')}</a></li>
|
||||
<li><a href="#projects" className="text-gray-800 dark:text-gray-200">{t('nav.projects')}</a></li>
|
||||
<li><a href="#cv" className="text-gray-800 dark:text-gray-200">{t('nav.cv')}</a></li>
|
||||
<li><a href="#top" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">Félix MARQUET</a></li>
|
||||
<li><a href="#about" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.about')}</a></li>
|
||||
<li><a href="#experience" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">Expérience</a></li>
|
||||
<li><a href="#projects" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.projects')}</a></li>
|
||||
<li><a href="#contact" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">Contact</a></li>
|
||||
<li><a href="#cv" className="text-gray-800 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors">{t('nav.cv')}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
|
||||
29
src/components/ParticlesBackground.tsx
Normal file
29
src/components/ParticlesBackground.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ParticlesBackgroundProps {
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
const ParticlesBackground: React.FC<ParticlesBackgroundProps> = ({ isDark }) => {
|
||||
return (
|
||||
<div className="absolute inset-0 -z-10">
|
||||
{/* Placeholder pour les particules - nous utiliserons une version CSS simple */}
|
||||
<div className="particles-container">
|
||||
{Array.from({ length: 20 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`absolute w-1 h-1 rounded-full ${isDark ? 'bg-white' : 'bg-gray-600'} animate-float`}
|
||||
style={{
|
||||
left: `${Math.random() * 100}%`,
|
||||
top: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 5}s`,
|
||||
animationDuration: `${3 + Math.random() * 4}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParticlesBackground;
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
interface ProgressBarProps {
|
||||
bgcolor: string;
|
||||
completed: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ProgressBar = (props: ProgressBarProps) => {
|
||||
const { bgcolor, completed } = props;
|
||||
|
||||
const completedPercentage = completed + "%";
|
||||
|
||||
const containerStyles = {
|
||||
height: 30,
|
||||
width: '100%',
|
||||
backgroundColor: "#cacdd2",
|
||||
borderRadius: 50,
|
||||
borderWidth: 2,
|
||||
marginTop: 5
|
||||
}
|
||||
|
||||
const fillerStyles = {
|
||||
height: '100%',
|
||||
width: completedPercentage,
|
||||
backgroundColor: bgcolor,
|
||||
borderRadius: 'inherit',
|
||||
textAlign: 'right' as 'right',
|
||||
transition: 'width 1s ease-in-out',
|
||||
}
|
||||
|
||||
const labelStyles = {
|
||||
padding: 5,
|
||||
color: 'white',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
<div style={fillerStyles}>
|
||||
<span style={labelStyles}>{`${completed}%`}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressBar;
|
||||
@@ -1,11 +1,18 @@
|
||||
// @ts-ignore
|
||||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import ProjectCard from "./ProjectCard.tsx";
|
||||
import ProjectCard from "./ProjectCard";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
// @ts-ignore
|
||||
function Projects({projects}) {
|
||||
type Project = {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
link: string;
|
||||
};
|
||||
|
||||
interface ProjectsProps {
|
||||
projects: Project[];
|
||||
}
|
||||
|
||||
function Projects({projects}: ProjectsProps) {
|
||||
const { t } = useTranslation();
|
||||
return(
|
||||
<div>
|
||||
|
||||
@@ -33,7 +33,7 @@ const ProjectCard = ({ project }: ProjectCardProps) => {
|
||||
<div className="px-4 py-1 border-2 rounded-full dark:text-gray-300">{tag}</div>
|
||||
))}
|
||||
</div>
|
||||
{title !== "MercuryCloud" && (
|
||||
{(title !== "MercuryCloud" && title !== "Alternance Horoquartz") && (
|
||||
<div className="w-full text-center">
|
||||
<GitHubButton href={link} data-color-scheme="no-preference: light; light: light; dark: dark;" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star ntkme/github-buttons on GitHub">Star</GitHubButton>
|
||||
{" "}
|
||||
|
||||
@@ -62,7 +62,7 @@ const SkillLevel: React.FC<SkillLevelProps> = ({ level, skillName }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<div className="mt-2 relative">
|
||||
<div
|
||||
ref={badgeRef}
|
||||
className="inline-block px-3 py-1 rounded-full text-sm font-medium bg-gray-200 text-gray-800 border cursor-pointer dark:bg-gray-800 dark:text-gray-200 dark:border-gray-700"
|
||||
@@ -76,11 +76,7 @@ const SkillLevel: React.FC<SkillLevelProps> = ({ level, skillName }) => {
|
||||
{showTooltip && (
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className="fixed z-50 w-64 p-3 text-sm bg-gray-200 rounded-xl border-2 border-gray-300 dark:border-gray-700 hover:shadow-lg dark:bg-gray-800 dark:text-gray-200 transition-shadow"
|
||||
style={{
|
||||
top: `${tooltipPosition.top}px`,
|
||||
left: `${tooltipPosition.left}px`,
|
||||
}}
|
||||
className="absolute left-1/2 -translate-x-1/2 top-full mt-2 z-50 w-64 p-3 text-sm bg-gray-200 rounded-xl border-2 border-gray-300 dark:border-gray-700 hover:shadow-lg dark:bg-gray-800 dark:text-gray-200 transition-shadow"
|
||||
>
|
||||
<p className="font-bold mb-1">{t(`skills.examples.${skillName}.title`)}</p>
|
||||
<p>{t(`skills.examples.${skillName}.description`)}</p>
|
||||
|
||||
139
src/components/TimelineSection.tsx
Normal file
139
src/components/TimelineSection.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaBriefcase, FaGraduationCap, FaCalendarAlt, FaMapMarkerAlt } from 'react-icons/fa';
|
||||
|
||||
interface TimelineItem {
|
||||
id: number;
|
||||
type: 'work' | 'education' | 'achievement';
|
||||
title: string;
|
||||
organization: string;
|
||||
location: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
description: string[];
|
||||
technologies?: string[];
|
||||
current?: boolean;
|
||||
}
|
||||
|
||||
const TimelineSection: React.FC<{ experience: TimelineItem[] }> = ({ experience }) => {
|
||||
const { t } = useTranslation();
|
||||
const getIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'work':
|
||||
return <FaBriefcase className="text-blue-500" />;
|
||||
case 'education':
|
||||
return <FaGraduationCap className="text-green-500" />;
|
||||
case 'achievement':
|
||||
return <FaCalendarAlt className="text-purple-500" />;
|
||||
default:
|
||||
return <FaBriefcase className="text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getBackgroundColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'work':
|
||||
return 'bg-blue-50 dark:bg-blue-900/20 border-l-blue-500';
|
||||
case 'education':
|
||||
return 'bg-green-50 dark:bg-green-900/20 border-l-green-500';
|
||||
case 'achievement':
|
||||
return 'bg-purple-50 dark:bg-purple-900/20 border-l-purple-500';
|
||||
default:
|
||||
return 'bg-gray-50 dark:bg-gray-900/20 border-l-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto mt-16" id="experience">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4">
|
||||
{t('experience.title')}
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400">
|
||||
{t('experience.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
{/* Ligne verticale */}
|
||||
<div className="absolute left-4 md:left-8 top-0 bottom-0 w-0.5 bg-gray-300 dark:bg-gray-600"></div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{experience.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`relative pl-12 md:pl-20 animate-fadeInUp`}
|
||||
style={{ animationDelay: `${index * 200}ms` }}
|
||||
>
|
||||
{/* Icône */}
|
||||
<div className="absolute left-1 md:left-5 w-6 h-6 bg-white dark:bg-gray-800 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center">
|
||||
<div className="text-sm">
|
||||
{getIcon(item.type)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenu */}
|
||||
<div className={`bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg card-hover border-l-4 ${getBackgroundColor(item.type)}`}>
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-3">
|
||||
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-200">
|
||||
{t(`experience.items.${index}.title`)}
|
||||
</h3>
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-600 dark:text-gray-400 mt-2 md:mt-0">
|
||||
{item.current && (
|
||||
<span className="px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded-full text-xs">
|
||||
{t('experience.current')}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center space-x-1">
|
||||
<FaCalendarAlt className="text-xs" />
|
||||
<span>{t(`experience.items.${index}.startDate`)} - {t(`experience.items.${index}.endDate`)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<h4 className="text-lg font-semibold text-gray-700 dark:text-gray-300">
|
||||
{t(`experience.items.${index}.organization`)}
|
||||
</h4>
|
||||
<span className="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<FaMapMarkerAlt className="mr-1" />
|
||||
{t(`experience.items.${index}.location`)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-1 mb-4">
|
||||
{item.description.map((_, idx) => (
|
||||
<li key={idx} className="text-gray-600 dark:text-gray-400 flex items-start">
|
||||
<span className="text-blue-500 mr-2 mt-1.5">•</span>
|
||||
<span>{t(`experience.items.${index}.description.${idx}`)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{item.technologies && (
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('experience.technologies')}
|
||||
</h5>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{item.technologies.map((_, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-3 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full"
|
||||
>
|
||||
{t(`experience.items.${index}.technologies.${idx}`)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineSection;
|
||||
119
src/i18n.js
119
src/i18n.js
@@ -1,110 +1,19 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import HttpBackend from "i18next-http-backend";
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
fr: {
|
||||
translation: {
|
||||
"about.title": "A propos de moi",
|
||||
"about.description": "Je suis étudiant en 3e année à l'ISEN Nantes en alternance chez Horoquartz. Je suis passionné par l'informatique. J'ai appris à coder en autodidacte et je suis actuellement en train d'apprendre le Rust et le Go. Je suis également passionné par l'électronique et le hardware. Je possède un homelab composé de 3serveur, un DELL T320, un DELL T330 et un Dell Precision T3610 les 3 sous proxmox.",
|
||||
"card.title": "Etudiant en 3e année a l'ISEN Nantes en alternance chez Horoquartz",
|
||||
"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.",
|
||||
"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",
|
||||
"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 ISEN Orbit 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 ISEN Orbit, projets professionnels",
|
||||
},
|
||||
},
|
||||
en: {
|
||||
translation: {
|
||||
"cv.title": "My CV",
|
||||
"about.title": "About me",
|
||||
"about.description": "I am a third-year student at ISEN Nantes, currently in a work-study program at Horoquartz. I am passionate about computer science. I learned to code on my own and am currently learning Rust and Go. I am also passionate about electronics and hardware. I have a homelab consisting of 3 servers: a DELL T320, a DELL T330, and a Dell Precision T3610, all running Proxmox.",
|
||||
"card.title": "Third year student at ISEN Nantes in work-study program at Horoquartz",
|
||||
"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.",
|
||||
"nav.about": "About me",
|
||||
"nav.projects": "My projects",
|
||||
"nav.cv": "My CV",
|
||||
"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.",
|
||||
"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 ISEN Orbit 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, ISEN Orbit mobile application, professional projects."
|
||||
},
|
||||
},
|
||||
},
|
||||
lng: navigator.language.startsWith('fr') ? 'fr' : 'en',
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
},
|
||||
});
|
||||
.use(HttpBackend)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: navigator.language.startsWith('fr') ? 'fr' : 'en',
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
},
|
||||
backend: {
|
||||
loadPath: "/locales/{{lng}}/translation.json"
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
export default i18n;
|
||||
@@ -1,3 +1,2 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
@import "tailwindcss";
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
2638
src/output.css
2638
src/output.css
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: 'class',
|
||||
export default {
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
"types": ["@simonsmith/cypress-image-snapshot/types"],
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src", "env.d.ts"]
|
||||
}
|
||||
@@ -2,11 +2,13 @@ import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import viteTsconfigPaths from 'vite-tsconfig-paths';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
viteTsconfigPaths(),
|
||||
svgr({
|
||||
include: '**/*.svg?react',
|
||||
|
||||
Reference in New Issue
Block a user