feat(experience): Add proper experience declaration

TODO: Translation of experience, Github statistiques and contact form
This commit is contained in:
Félix MARQUET
2025-08-26 11:13:46 +00:00
parent 191158f197
commit 46a1b5acdc
13 changed files with 7503 additions and 2685 deletions

2444
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@tailwindcss/cli": "^4.1.12",
"@tailwindcss/vite": "^4.1.12",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
@@ -20,6 +22,7 @@
"react-icons": "^5.5.0",
"react-particles": "^2.12.2",
"sass": "^1.86.0",
"tailwindcss": "^4.1.12",
"tsparticles-slim": "^2.12.0",
"typescript": "^5.8.3",
"web-vitals": "^5.1.0",
@@ -29,7 +32,7 @@
"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"
@@ -56,6 +59,7 @@
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@simonsmith/cypress-image-snapshot": "^10.0.2",
"@vitejs/plugin-react": "^4.6.0",
"prettier": "^3.6.2",
"vite": "^7.0.6",
"vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^5.1.4"

4495
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,6 @@ import CV from "./components/CV.tsx";
import Menu from "./components/Menu.tsx";
import LoadingScreen from "./components/LoadingScreen.tsx";
import ParticlesBackground from "./components/ParticlesBackground.tsx";
import HomelabSection from "./components/HomelabSection.tsx";
import GitHubStatsSection from "./components/GitHubStatsSection.tsx";
import ContactSection from "./components/ContactSection.tsx";
import TimelineSection from "./components/TimelineSection.tsx";
@@ -71,9 +70,7 @@ function App() {
<About />
<Skills skills={data.skills} />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="experience" />
<TimelineSection />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="homelab" />
<HomelabSection />
<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" />

View File

@@ -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",

View File

@@ -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>

View File

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

View File

@@ -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>

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FaBriefcase, FaGraduationCap, FaCalendarAlt, FaMapMarkerAlt } from 'react-icons/fa';
interface TimelineItem {
@@ -14,90 +15,8 @@ interface TimelineItem {
current?: boolean;
}
const TimelineSection: React.FC = () => {
const timelineData: TimelineItem[] = [
{
id: 1,
type: 'work',
title: 'Développeur - Alternant',
organization: 'Horoquartz',
location: 'Cesson-Sévigné, France',
startDate: '2024',
endDate: 'Présent',
current: true,
description: [
'Développement d\'un système de mise à jour pour les produits Horoquartz',
'Conception et implémentation d\'APIs REST avec Node.js et Go',
'Gestion des bases de données PostgreSQL',
'Déploiement avec Docker et Kubernetes sur Azure'
],
technologies: ['Node.js', 'Go', 'PostgreSQL', 'Docker', 'Kubernetes', 'Azure']
},
{
id: 2,
type: 'education',
title: 'ISEN Nantes - Cycle Ingénieur',
organization: 'Institut Supérieur de l\'Électronique et du Numérique',
location: 'Nantes, France',
startDate: '2022',
endDate: '2025',
current: true,
description: [
'Formation d\'ingénieur en informatique et nouvelles technologies',
'Spécialisation en développement logiciel et administration système',
'Projets en équipe et gestion de projet'
],
technologies: ['C', 'C++', 'Python', 'Linux', 'Réseaux']
},
{
id: 3,
type: 'work',
title: 'Support Technique & Admin VPS',
organization: 'MercuryCloud',
location: 'Remote',
startDate: '2021',
endDate: '2024',
description: [
'Support technique pour serveurs de jeu et VPS',
'Administration des services CPanel et Plesk',
'Gestion du système WHMCS pour la facturation',
'Virtualisation et maintenance des serveurs Linux'
],
technologies: ['Linux', 'Virtualisation', 'CPanel', 'Plesk', 'WHMCS']
},
{
id: 4,
type: 'achievement',
title: 'Projet Robot - Coupe de France de Robotique',
organization: 'Club Modelec ISEN',
location: 'Nantes, France',
startDate: '2023',
endDate: '2023',
description: [
'Développement du système de contrôle du robot',
'Interface utilisateur avec QT',
'Déploiement sur Raspberry Pi',
'Travail en équipe multidisciplinaire'
],
technologies: ['C++', 'QT', 'Raspberry Pi', 'Linux']
},
{
id: 5,
type: 'education',
title: 'Classes Préparatoires Intégrées',
organization: 'ISEN Nantes',
location: 'Nantes, France',
startDate: '2020',
endDate: '2022',
description: [
'Mathématiques, Physique, Informatique',
'Bases de la programmation en C',
'Introduction aux systèmes embarqués'
],
technologies: ['C', 'Mathématiques', 'Physique', 'Électronique']
}
];
const TimelineSection: React.FC<{ experience: TimelineItem[] }> = ({ experience }) => {
const { t } = useTranslation();
const getIcon = (type: string) => {
switch (type) {
case 'work':
@@ -140,7 +59,7 @@ const TimelineSection: React.FC = () => {
<div className="absolute left-4 md:left-8 top-0 bottom-0 w-0.5 bg-gray-300 dark:bg-gray-600"></div>
<div className="space-y-8">
{timelineData.map((item, index) => (
{experience.map((item, index) => (
<div
key={item.id}
className={`relative pl-12 md:pl-20 animate-fadeInUp`}

View File

@@ -1,3 +1 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import "tailwindcss";

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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',