Updates dependencies and implements portfolio

Updates eslint and typescript-eslint dependencies.

Migrates to a new portfolio structure with components, styling, route management, and internationalization.

Adds project display with multiple project types using custom components.

Adds image assets to be displayed in experience sections
This commit is contained in:
Louis
2025-10-29 00:13:50 +01:00
parent 3e3a6dd125
commit e91f55b80d
19 changed files with 3929 additions and 199 deletions

View File

@@ -1,30 +1,75 @@
# React + TypeScript + Vite # Portfolio
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Personal portfolio website showcasing my projects and skills as a Full Stack Developer.
Currently, two official plugins are available: ## Tech Stack
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh - React 18 with TypeScript
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - Vite for fast development and build
- Tailwind CSS for styling
- Motion for animations
- React Router for navigation
- i18next for internationalization (French/English)
## Expanding the ESLint configuration ## Features
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - Responsive design with mobile-first approach
- Dark/Light theme with system preference support
- Animated page transitions and interactions
- Multi-language support
- Project showcase with detailed views
- Skills and experience sections
- Contact information
- Configure the top-level `parserOptions` property like this: ## Development
```js Install dependencies:
export default {
// other rules... ```bash
parserOptions: { npm install
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
``` ```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` Start development server:
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list ```bash
npm run dev
```
Build for production:
```bash
npm run build
```
Preview production build:
```bash
npm run preview
```
## Project Structure
```
src/
├── components/ # React components
├── i18n/ # Translation files (en/fr)
├── icons/ # SVG icon components
└── styles/ # Global styles and CSS
```
## Internationalization
Translations are modularized by section in JSON files under `src/i18n/en/` and `src/i18n/fr/`. Each section (nav, hero, about, experience, etc.) has its own file for easy maintenance.
## Theme System
The site supports three theme modes:
- Light mode
- Dark mode
- System preference (auto-detect)
Theme preference is persisted in localStorage.
## License
All rights reserved.

2344
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,12 +25,15 @@
"@typescript-eslint/parser": "^6.10.0", "@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.2.0", "@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"eslint": "^8.53.0", "eslint": "^8.57.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4", "eslint-plugin-react-refresh": "^0.4.4",
"globals": "^16.4.0",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"tailwindcss": "^3.3.5", "tailwindcss": "^3.3.5",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"typescript-eslint": "^8.46.2",
"vite": "^5.0.0" "vite": "^5.0.0"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -60,10 +60,12 @@ function App() {
setThemeSelected(newTheme); setThemeSelected(newTheme);
}; };
const scrollToSection = (id: string) => { const scrollToSection = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
e.preventDefault();
const section = document.getElementById(id); const section = document.getElementById(id);
if (section) { if (section) {
section.scrollIntoView({ behavior: 'smooth' }); section.scrollIntoView({ behavior: 'smooth' });
window.history.pushState(null, '', `#${id}`);
} }
} }
@@ -83,11 +85,11 @@ function App() {
Louis EMARD Louis EMARD
</h1> </h1>
<nav className='flex gap-6 h-full justify-center items-center'> <nav className='flex gap-6 h-full justify-center items-center'>
<a onClick={() => scrollToSection('about')} className='hover:text-accent transition-colors cursor-pointer'>{t('nav.about')}</a> <a href='#about' onClick={(e) => scrollToSection(e, 'about')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.about')}</a>
<a onClick={() => scrollToSection('experience')} className='hover:text-accent transition-colors cursor-pointer'>{t('nav.projects')}</a> <a href='#experience' onClick={(e) => scrollToSection(e, 'experience')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.projects')}</a>
<a onClick={() => scrollToSection('skills')} className='hover:text-accent transition-colors cursor-pointer'>{t('nav.skills')}</a> <a href='#skills' onClick={(e) => scrollToSection(e, 'skills')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.skills')}</a>
<a onClick={() => scrollToSection('education')} className='hover:text-accent transition-colors cursor-pointer'>{t('nav.education')}</a> <a href='#education' onClick={(e) => scrollToSection(e, 'education')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.education')}</a>
<a onClick={() => scrollToSection('contact')} className='hover:text-accent transition-colors cursor-pointer'>{t('nav.contact')}</a> <a href='#contact' onClick={(e) => scrollToSection(e, 'contact')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.contact')}</a>
{/* Language Toggle */} {/* Language Toggle */}
<button <button
@@ -107,7 +109,7 @@ function App() {
</header> </header>
{/* Main Content */} {/* Main Content */}
<main className='bg-'> <main>
<Hero /> <Hero />
<About /> <About />
<Experience /> <Experience />
@@ -118,7 +120,7 @@ function App() {
{/* Footer */} {/* Footer */}
<footer className='py-8 border-t border-secondary/10 bg-secondary/5'> <footer className='py-8 border-t border-secondary/10 bg-secondary/5'>
<div className='flex flex-col md:flex-row gap-6 justify-center items-center text-sm'> <div className='flex gap-6 justify-center items-center text-sm'>
<a href='https://git.louisemard.dev' target='_blank' rel='noopener noreferrer' className='hover:text-accent transition-colors'> <a href='https://git.louisemard.dev' target='_blank' rel='noopener noreferrer' className='hover:text-accent transition-colors'>
Gitea Gitea
</a> </a>

View File

@@ -5,13 +5,13 @@ const Education = () => {
const educations = [ const educations = [
{ {
period: '2020 - 2023', period: '2018 - 2021',
title: 'education.degree1.title', title: 'education.degree1.title',
school: 'education.degree1.school', school: 'education.degree1.school',
description: 'education.degree1.description' description: 'education.degree1.description'
}, },
{ {
period: '2018 - 2020', period: '2015 - 2018',
title: 'education.degree2.title', title: 'education.degree2.title',
school: 'education.degree2.school', school: 'education.degree2.school',
description: 'education.degree2.description' description: 'education.degree2.description'

View File

@@ -1,7 +1,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Routes, Route, useRoutes, Link, useParams, useNavigate } from 'react-router'; import { useRoutes, Link, useParams, useNavigate } from 'react-router';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { Skill } from '../Skills';
import { CustomInfra, CustomVeggie, CustomStripe, CustomDocker } from './custom';
type ProjectType = 'professional' | 'personal'; type ProjectType = 'professional' | 'personal';
@@ -12,66 +14,105 @@ const Experience = () => {
const professionalExperiences = [ const professionalExperiences = [
{ {
period: '2024', period: '2024',
title: 'experience.job1.title', title: 'experience.project1.title',
company: 'experience.job1.company', company: 'experience.project1.company',
description: 'experience.job1.description', description: 'experience.project1.description',
tasks: ['experience.job1.task1', 'experience.job1.task2', 'experience.job1.task3'] tasks: ['experience.project1.task1', 'experience.project1.task2', 'experience.project1.task3'],
skills: ['Docker', 'Docker Compose', 'PHP', 'Apache', 'MySQL', 'VSCode'],
customContent: <CustomDocker t={t} />
}, },
{ {
period: '2024', period: '2024',
title: 'experience.job2.title', title: 'experience.project2.title',
company: 'experience.job2.company', company: 'experience.project2.company',
description: 'experience.job2.description', description: 'experience.project2.description',
tasks: ['experience.job2.task1', 'experience.job2.task2'] tasks: ['experience.project2.task1', 'experience.project2.task2'],
skills: ['Stripe', 'PHP', 'MySQL', 'JavaScript', 'jQuery', 'Webhooks'],
customContent: <CustomStripe t={t} />
},
{
period: '2025',
title: 'experience.project3.title',
company: 'experience.project3.company',
description: 'experience.project3.description',
detailedDescription: 'experience.project3.detailedDescription',
tasks: ['experience.project3.task1', 'experience.project3.task2'],
skills: ['Stripe', 'Node.js', 'JavaScript', 'Webhooks', 'Make', 'Cloudflare'],
customContent: <CustomVeggie t={t} />
} }
]; ];
const personalExperiences = [ const personalExperiences = [
{ {
period: '2018 - 2025', period: '2025',
title: 'experience.job3.title', title: 'experience.project4.title',
company: 'experience.job3.company', company: 'experience.project4.company',
description: 'experience.job3.description', description: 'experience.project4.description',
tasks: ['experience.job3.task1', 'experience.job3.task2'] tasks: ['experience.project4.task1', 'experience.project4.task2'],
skills: ['Docker', 'Git', 'CI/CD', 'Ansible', 'Terraform'],
customContent: <CustomInfra t={t} />
} }
]; ];
const currentExperiences = activeTab === 'professional' ? professionalExperiences : personalExperiences; const currentExperiences = activeTab === 'professional' ? professionalExperiences : personalExperiences;
const routes = useRoutes(['/', '/experiences/:xpId'].map((path, id) => ({ const routes = useRoutes(['/', '/experiences/:xpId'].map((path) => ({
path, path,
element: ( element: (
<> <>
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto px-8">
<h2 className="text-4xl font-bold mb-8 text-accent" id="experience">{t('experience.title')}</h2> <h2 className="text-4xl font-bold mb-8 text-accent" id="experience">{t('experience.title')}</h2>
{/* Tabs */} {/* Tabs */}
<div className="flex gap-4 mb-8 border-b border-secondary/20"> <div className="flex gap-4 mb-8 border-b border-secondary/20">
<button <button
onClick={() => setActiveTab('professional')} onClick={() => setActiveTab('professional')}
className={`px-6 py-3 font-medium transition-all ${ className={`relative px-6 py-3 font-medium transition-colors ${
activeTab === 'professional' activeTab === 'professional'
? 'text-accent border-b-2 border-accent' ? 'text-accent'
: 'text-secondary/60 hover:text-secondary' : 'text-secondary/60 hover:text-secondary'
}`} }`}
> >
{t('experience.tabs.professional')} {t('experience.tabs.professional')}
{activeTab === 'professional' && (
<motion.div
layoutId="activeTab"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent"
transition={{ type: "spring", stiffness: 380, damping: 30 }}
/>
)}
</button> </button>
<button <button
onClick={() => setActiveTab('personal')} onClick={() => setActiveTab('personal')}
className={`px-6 py-3 font-medium transition-all ${ className={`relative px-6 py-3 font-medium transition-colors ${
activeTab === 'personal' activeTab === 'personal'
? 'text-accent border-b-2 border-accent' ? 'text-accent'
: 'text-secondary/60 hover:text-secondary' : 'text-secondary/60 hover:text-secondary'
}`} }`}
> >
{t('experience.tabs.personal')} {t('experience.tabs.personal')}
{activeTab === 'personal' && (
<motion.div
layoutId="activeTab"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent"
transition={{ type: "spring", stiffness: 380, damping: 30 }}
/>
)}
</button> </button>
</div> </div>
<div className="space-y-8"> <AnimatePresence mode="wait">
<ExperiencesList experiences={currentExperiences} t={t} /> <motion.div
</div> key={activeTab}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="space-y-8"
>
<ExperiencesList experiences={currentExperiences} t={t} />
</motion.div>
</AnimatePresence>
</div> </div>
</> </>
) )
@@ -90,6 +131,9 @@ interface Experience {
company: string; company: string;
description: string; description: string;
tasks: string[]; tasks: string[];
detailedDescription?: string;
skills?: string[];
customContent?: React.ReactNode;
} }
interface ExperiencesListProps { interface ExperiencesListProps {
@@ -98,24 +142,18 @@ interface ExperiencesListProps {
} }
const ExperiencesList = ({ experiences, t }: ExperiencesListProps) => { const ExperiencesList = ({ experiences, t }: ExperiencesListProps) => {
const { xpId } = useParams(); const { xpId } = useParams() as { xpId?: number };
return <div className=' flex flex-col space-y-8'> return <div className=' flex flex-col space-y-8'>
{experiences.map((exp, index) => <ExperienceSummary key={index} index={index} exp={exp} t={t} />)} {experiences.map((exp, index) => <ExperienceSummary key={index} index={index} exp={exp} t={t} />)}
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{xpId !== undefined && <DetailedExperience t={t} />} {xpId !== undefined && <DetailedExperience t={t} exp={experiences[xpId]} />}
</AnimatePresence> </AnimatePresence>
</div> </div>
} }
interface ExperienceSummaryProps { interface ExperienceSummaryProps {
index: number; index: number;
exp: { exp: Experience;
period: string;
title: string;
company: string;
description: string;
tasks: string[];
};
t: (key: string) => string; t: (key: string) => string;
} }
@@ -152,6 +190,9 @@ const ExperienceSummary = ({ index, exp, t }: ExperienceSummaryProps) => {
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
whileHover={{ scale: 1.2 }} whileHover={{ scale: 1.2 }}
layoutId={`experience-icon-${index}`}
transition={{ type: "spring", stiffness: 350, damping: 35 }}
whileTap={{ scale: 0.9 }}
> >
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</motion.svg> </motion.svg>
@@ -177,9 +218,10 @@ const ExperienceSummary = ({ index, exp, t }: ExperienceSummaryProps) => {
interface DetailedExperienceProps { interface DetailedExperienceProps {
t: (key: string) => string; t: (key: string) => string;
exp: Experience;
} }
const DetailedExperience = ({ t }: DetailedExperienceProps) => { const DetailedExperience = ({ t, exp }: DetailedExperienceProps) => {
const { xpId } = useParams(); const { xpId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
@@ -216,47 +258,38 @@ const DetailedExperience = ({ t }: DetailedExperienceProps) => {
> >
<motion.div <motion.div
layoutId={`experience-${xpId}`} layoutId={`experience-${xpId}`}
className="bg-primary p-8 rounded-lg border border-accent/50 max-w-3xl w-full h-1/2 overflow-y-auto relative" className="bg-primary p-8 rounded-lg border border-accent/50 w-4/5 h-4/5 lg:h-3/4 lg:w-6/10 overflow-y-auto relative"
transition={{ type: "spring", stiffness: 350, damping: 35 }} transition={{ type: "spring", stiffness: 350, damping: 35 }}
> >
<motion.button <div className="flex flex-row md:justify-between md:items-start mb-6">
onClick={() => navigate('/')}
className="absolute top-4 right-4 p-2 rounded-full hover:bg-accent/10 transition-colors group"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-accent group-hover:text-accent/80"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</motion.button>
<div className="flex flex-col md:flex-row md:justify-between md:items-start mb-6">
<motion.div <motion.div
layoutId={`experience-header-${xpId}`} layoutId={`experience-header-${xpId}`}
transition={{ type: "spring", stiffness: 350, damping: 35 }} transition={{ type: "spring", stiffness: 350, damping: 35 }}
className="flex-1" className="flex-1"
> >
<h3 className="text-2xl font-semibold text-secondary mb-1">Dev full stack</h3> <h3 className="text-xl md:text-2xl font-semibold text-secondary mb-1">{t(exp.title)}</h3>
<p className="text-accent font-medium">Meta Video</p> <p className="text-accent font-medium">{t(exp.company)}</p>
</motion.div> </motion.div>
<div className="flex items-center gap-3 mt-2 md:mt-0">
<motion.span <div className="flex items-start gap-3 mt-2 md:mt-0">
layoutId={`experience-period-${xpId}`} <motion.button
className="text-secondary/60 text-sm md:text-base" onClick={() => navigate('/')}
className="p-2 rounded-full hover:bg-accent/10 transition-colors group"
layoutId={`experience-icon-${xpId}`}
transition={{ type: "spring", stiffness: 350, damping: 35 }} transition={{ type: "spring", stiffness: 350, damping: 35 }}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
> >
2023 - 2024 <svg
</motion.span> xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-accent group-hover:text-accent/80"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</motion.button>
</div> </div>
</div> </div>
@@ -266,8 +299,19 @@ const DetailedExperience = ({ t }: DetailedExperienceProps) => {
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
> >
<h4 className="text-xl font-semibold text-secondary mb-4">Détails supplémentaires</h4> {/* <h4 className="text-xl font-semibold text-secondary mb-4">{t('experience.additional_details')}</h4> */}
<p className="text-secondary/70 mb-4">Contenu détaillé de l'expérience...</p> {/* <p className="text-secondary/70">{t(exp.detailedDescription!)}</p> */}
{exp.customContent}
{exp.skills && exp.skills.length > 0 && (
<div className="mb-4 mt-4">
<h5 className="text-lg font-semibold text-secondary mb-2">{t('experience.skills_used')}</h5>
<div className='flex gap-2 flex-wrap'>
{exp.skills.map((skill, skillIndex) => (
<Skill key={skillIndex} name={skill} />
))}
</div>
</div>
)}
</motion.div> </motion.div>
</motion.div> </motion.div>
</motion.div>; </motion.div>;

View File

@@ -0,0 +1,181 @@
import { TFunction } from "i18next";
export const CustomDocker = ({ t }: { t: TFunction }) => {
return (
<div className="mt-4">
<div className="mt-8 space-y-8">
<header className="border-b border-secondary/10 pb-4">
<h2 className="text-2xl font-semibold text-secondary mb-2">{t('experience.customs.docker.title')}</h2>
<p className="text-secondary/60">{t('experience.customs.docker.subtitle')}</p>
</header>
<section className="space-y-4">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.docker.overview.title')}</h3>
<p className="text-secondary/70">{t('experience.customs.docker.overview.description')}</p>
<p className="text-secondary/70">{t('experience.customs.docker.overview.problem')}</p>
</section>
<section className="space-y-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.docker.architecture.title')}</h3>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.docker.architecture.services.title')}</h4>
<ul className="space-y-2 text-secondary/70">
<li className="flex gap-2">
<span className="font-semibold text-accent">Apache/PHP 8.2:</span>
<span>{t('experience.customs.docker.architecture.services.php')}</span>
</li>
<li className="flex gap-2">
<span className="font-semibold text-accent">MySQL:</span>
<span>{t('experience.customs.docker.architecture.services.mysql')}</span>
</li>
<li className="flex gap-2">
<span className="font-semibold text-accent">PhpMyAdmin:</span>
<span>{t('experience.customs.docker.architecture.services.phpmyadmin')}</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.docker.architecture.features.title')}</h4>
<ul className="list-disc list-inside space-y-2 text-secondary/70">
<li>{t('experience.customs.docker.architecture.features.dependencies')}</li>
<li>{t('experience.customs.docker.architecture.features.repos')}</li>
<li>{t('experience.customs.docker.architecture.features.devcontainer')}</li>
<li>{t('experience.customs.docker.architecture.features.apache')}</li>
<li>{t('experience.customs.docker.architecture.features.vhosts')}</li>
<li>{t('experience.customs.docker.architecture.features.php_config')}</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.docker.architecture.devcontainer.title')}</h4>
<p className="text-secondary/70">{t('experience.customs.docker.architecture.devcontainer.description')}</p>
<ul className="list-disc list-inside space-y-2 text-secondary/70 text-sm">
<li>{t('experience.customs.docker.architecture.devcontainer.debugging')}</li>
<li>{t('experience.customs.docker.architecture.devcontainer.composer')}</li>
<li>{t('experience.customs.docker.architecture.devcontainer.terminal')}</li>
</ul>
</div>
</section>
<section className="space-y-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.docker.challenges.title')}</h3>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-2">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.docker.challenges.modules.title')}</h4>
<p className="text-secondary/70">{t('experience.customs.docker.challenges.modules.description')}</p>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-2">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.docker.challenges.standardization.title')}</h4>
<p className="text-secondary/70">{t('experience.customs.docker.challenges.standardization.description')}</p>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-2">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.docker.challenges.automation.title')}</h4>
<p className="text-secondary/70">{t('experience.customs.docker.challenges.automation.description')}</p>
</div>
</section>
<section className="space-y-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.docker.stack.title')}</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.docker.stack.core.title')}</h4>
<ul className="space-y-2 text-secondary/70 text-sm">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Docker & Docker Compose</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Apache 2.4</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>PHP 8.2</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>MySQL / MariaDB</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.docker.stack.tools.title')}</h4>
<ul className="space-y-2 text-secondary/70 text-sm">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>PhpMyAdmin</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Composer</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>XDebug</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Bitbucket (SSH)</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.docker.stack.vscode.title')}</h4>
<ul className="space-y-2 text-secondary/70 text-sm">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Dev Containers</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Remote Explorer</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Docker Extension</span>
</li>
</ul>
</div>
</div>
</section>
<section className="space-y-4">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.docker.results.title')}</h3>
<ul className="space-y-2 text-secondary/70">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.docker.results.standardization')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.docker.results.onboarding')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.docker.results.debugging')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.docker.results.flexibility')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.docker.results.productivity')}</span>
</li>
</ul>
</section>
<footer className="border-t border-secondary/10 pt-6">
<p className="text-sm text-secondary/60 italic">{t('experience.customs.docker.tags')}</p>
</footer>
</div>
</div>
);
};

View File

@@ -0,0 +1,233 @@
import { TFunction } from "i18next";
import OverlayPicture from "../../utility/OverlayPicture";
export const CustomInfra = ({ t }: { t: TFunction }) => {
return (
<div className="mt-4">
<div className="mt-8 space-y-8">
<header className="border-b border-secondary/10 pb-4">
<h2 className="text-2xl font-semibold text-secondary mb-2">{t('experience.customs.infrastructure.title')}</h2>
<p className="text-secondary/60">{t('experience.customs.infrastructure.subtitle')}</p>
</header>
<section className="space-y-4">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.infrastructure.overview.title')}</h3>
<p className="text-secondary/70">{t('experience.customs.infrastructure.overview.description')}</p>
<OverlayPicture
src="/images/infra_diagram.png"
alt="Diagramme de l'infrastructure cloud automatisée"
className="mt-4 w-full"
/>
</section>
<section className="space-y-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.infrastructure.architecture.title')}</h3>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.infrastructure.architecture.provisioning.title')}</h4>
<ul className="space-y-2 text-secondary/70">
<li className="flex gap-2 flex-wrap">
<span className="font-semibold text-accent">Terraform/OpenTofu:</span>
<span>{t('experience.customs.infrastructure.architecture.provisioning.terraform')}</span>
</li>
<li className="flex gap-2 flex-wrap">
<span className="font-semibold text-accent">Ansible:</span>
<span>{t('experience.customs.infrastructure.architecture.provisioning.ansible')}</span>
</li>
<li className="flex gap-2 flex-wrap">
<span className="font-semibold text-accent">DNS OVH:</span>
<span>{t('experience.customs.infrastructure.architecture.provisioning.dns')}</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.infrastructure.architecture.services.title')}</h4>
<ul className="space-y-2 text-secondary/70">
<li className="flex gap-2 flex-wrap">
<span className="font-semibold text-accent">{t('experience.customs.infrastructure.architecture.services.website.name')}:</span>
<code className="bg-secondary/10 px-2 py-0.5 rounded text-accent text-sm">louisemard.dev</code>
<span>- {t('experience.customs.infrastructure.architecture.services.website.description')}</span>
</li>
<li className="flex gap-2 flex-wrap">
<span className="font-semibold text-accent">{t('experience.customs.infrastructure.architecture.services.gitea.name')}:</span>
<code className="bg-secondary/10 px-2 py-0.5 rounded text-accent text-sm">git.louisemard.dev</code>
<span>- {t('experience.customs.infrastructure.architecture.services.gitea.description')}</span>
</li>
<li className="flex gap-2 flex-wrap">
<span className="font-semibold text-accent">{t('experience.customs.infrastructure.architecture.services.infisical.name')}:</span>
<code className="bg-secondary/10 px-2 py-0.5 rounded text-accent text-sm">vault.louisemard.dev</code>
<span>- {t('experience.customs.infrastructure.architecture.services.infisical.description')}</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.infrastructure.architecture.security.title')}</h4>
<ul className="list-disc list-inside space-y-2 text-secondary/70">
<li>{t('experience.customs.infrastructure.architecture.security.ssl')}</li>
<li>{t('experience.customs.infrastructure.architecture.security.nginx')}</li>
<li>{t('experience.customs.infrastructure.architecture.security.firewall')}</li>
<li>{t('experience.customs.infrastructure.architecture.security.docker')}</li>
</ul>
</div>
</section>
<section className="space-y-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.infrastructure.challenges.title')}</h3>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-2">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.infrastructure.challenges.secrets.title')}</h4>
<p className="text-secondary/70">{t('experience.customs.infrastructure.challenges.secrets.description')}</p>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-2">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.infrastructure.challenges.multiservice.title')}</h4>
<p className="text-secondary/70">{t('experience.customs.infrastructure.challenges.multiservice.description')}</p>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-2">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.infrastructure.challenges.automation.title')}</h4>
<p className="text-secondary/70">{t('experience.customs.infrastructure.challenges.automation.description')}</p>
</div>
</section>
<section className="space-y-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.infrastructure.stack.title')}</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.infrastructure.stack.infrastructure.title')}</h4>
<ul className="space-y-2 text-secondary/70 text-sm">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.stack.infrastructure.cloud')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.stack.infrastructure.os')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.stack.infrastructure.iac')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.stack.infrastructure.containers')}</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.infrastructure.stack.applications.title')}</h4>
<ul className="space-y-2 text-secondary/70 text-sm">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.stack.applications.webserver')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.stack.applications.gitea')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.stack.applications.infisical')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.stack.applications.ssl')}</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.infrastructure.stack.security.title')}</h4>
<ul className="space-y-2 text-secondary/70 text-sm">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.stack.security.firewall')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.stack.security.ids')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.stack.security.smtp')}</span>
</li>
</ul>
</div>
</div>
</section>
<section className="space-y-4">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.infrastructure.results.title')}</h3>
<ul className="space-y-2 text-secondary/70">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.results.automation')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.results.availability')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.results.security')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.results.maintainability')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.infrastructure.results.cost')}</span>
</li>
</ul>
</section>
<section className="space-y-4 border-t border-secondary/10 pt-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.infrastructure.links.title')}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<a
onClick={e => {
e.preventDefault();
alert(t('experience.customs.infrastructure.links.alert'));
}}
href="https://louisemard.dev"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-3 bg-accent/10 hover:bg-accent/20 text-accent rounded-lg transition-colors border border-accent/30 text-center font-medium"
>
{t('experience.customs.infrastructure.links.website')}
</a>
<a
href="https://git.louisemard.dev"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-3 bg-accent/10 hover:bg-accent/20 text-accent rounded-lg transition-colors border border-accent/30 text-center font-medium"
>
{t('experience.customs.infrastructure.links.gitea')}
</a>
<a
href="https://vault.louisemard.dev"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-3 bg-accent/10 hover:bg-accent/20 text-accent rounded-lg transition-colors border border-accent/30 text-center font-medium"
>
{t('experience.customs.infrastructure.links.infisical')}
</a>
<a
href="https://github.com/LouisDrame/louisemard_infrastructure"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-3 bg-accent/10 hover:bg-accent/20 text-accent rounded-lg transition-colors border border-accent/30 text-center font-medium"
>
{t('experience.customs.infrastructure.links.source')}
</a>
</div>
</section>
</div>
</div>
);
};

View File

@@ -0,0 +1,144 @@
import { TFunction } from "i18next";
export const CustomStripe = ({ t }: { t: TFunction }) => {
return (
<div className="mt-4">
<div className="mt-8 space-y-8">
<header className="border-b border-secondary/10 pb-4">
<h2 className="text-2xl font-semibold text-secondary mb-2">{t('experience.customs.stripe.title')}</h2>
<p className="text-secondary/60">{t('experience.customs.stripe.subtitle')}</p>
</header>
<section className="space-y-4">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.stripe.overview.title')}</h3>
<p className="text-secondary/70">{t('experience.customs.stripe.overview.description')}</p>
</section>
<section className="space-y-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.stripe.architecture.title')}</h3>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.stripe.architecture.frontend.title')}</h4>
<ul className="space-y-2 text-secondary/70">
<li className="flex gap-2">
<span className="font-semibold text-accent">{t('experience.customs.stripe.architecture.frontend.tech')}:</span>
<span>JavaScript natif, jQuery</span>
</li>
<li className="flex gap-2">
<span className="font-semibold text-accent">{t('experience.customs.stripe.architecture.frontend.features')}:</span>
<span>{t('experience.customs.stripe.architecture.frontend.features_list')}</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.stripe.architecture.backend.title')}</h4>
<ul className="list-disc list-inside space-y-2 text-secondary/70">
<li>{t('experience.customs.stripe.architecture.backend.integration')}</li>
<li>{t('experience.customs.stripe.architecture.backend.abstraction')}</li>
<li>{t('experience.customs.stripe.architecture.backend.extensibility')}</li>
<li>{t('experience.customs.stripe.architecture.backend.logging')}</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.stripe.architecture.security.title')}</h4>
<ul className="list-disc list-inside space-y-2 text-secondary/70">
<li>{t('experience.customs.stripe.architecture.security.webhooks')}</li>
<li>{t('experience.customs.stripe.architecture.security.idempotence')}</li>
<li>{t('experience.customs.stripe.architecture.security.reconciliation')}</li>
</ul>
</div>
</section>
<section className="space-y-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.stripe.challenges.title')}</h3>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-2">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.stripe.challenges.decoupling.title')}</h4>
<p className="text-secondary/70">{t('experience.customs.stripe.challenges.decoupling.description')}</p>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-2">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.stripe.challenges.errors.title')}</h4>
<p className="text-secondary/70">{t('experience.customs.stripe.challenges.errors.description')}</p>
</div>
</section>
<section className="space-y-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.stripe.stack.title')}</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.stripe.stack.frontend.title')}</h4>
<ul className="space-y-2 text-secondary/70 text-sm">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>JavaScript natif</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>jQuery</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.stripe.stack.backend.title')}</h4>
<ul className="space-y-2 text-secondary/70 text-sm">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>PHP</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>API Stripe</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>MySQL</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.stripe.stack.tools.title')}</h4>
<ul className="space-y-2 text-secondary/70 text-sm">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Webhooks Stripe</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.stripe.stack.tools.abstraction')}</span>
</li>
</ul>
</div>
</div>
</section>
<section className="space-y-4">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.stripe.results.title')}</h3>
<ul className="space-y-2 text-secondary/70">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.stripe.results.operational')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.stripe.results.extensible')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.stripe.results.stable')}</span>
</li>
</ul>
</section>
<footer className="border-t border-secondary/10 pt-6">
<p className="text-sm text-secondary/60 italic">{t('experience.customs.stripe.tags')}</p>
</footer>
</div>
</div>
);
};

View File

@@ -0,0 +1,167 @@
import { TFunction } from "i18next";
import OverlayPicture from "../../utility/OverlayPicture";
export const CustomVeggie = ({ t }: { t: TFunction }) => {
return (
<div className="mt-4">
<div className="mt-8 space-y-8">
<header className="border-b border-secondary/10 pb-4">
<h2 className="text-2xl font-semibold text-secondary mb-2">{t('experience.customs.veggie.title')}</h2>
<p className="text-secondary/60">{t('experience.customs.veggie.subtitle')}</p>
</header>
<section className="space-y-4">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.veggie.overview.title')}</h3>
<p className="text-secondary/70">{t('experience.customs.veggie.overview.description')}</p>
<p className="text-secondary/70">{t('experience.customs.veggie.overview.evolution')}</p>
<OverlayPicture
src="/images/les-serres-du-pre-marais.pages.dev_.png"
alt={t('experience.customs.veggie.overview.image_alt')}
className="mt-4 w-full"
/>
</section>
<section className="space-y-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.veggie.architecture.title')}</h3>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.veggie.architecture.frontend.title')}</h4>
<ul className="space-y-2 text-secondary/70">
<li className="flex gap-2">
<span className="font-semibold text-accent">{t('experience.customs.veggie.architecture.frontend.hosting')}:</span>
<span>Cloudflare Pages</span>
</li>
<li className="flex gap-2">
<span className="font-semibold text-accent">{t('experience.customs.veggie.architecture.frontend.tech')}:</span>
<span>HTML/CSS/JS vanilla</span>
</li>
<li className="flex gap-2">
<span className="font-semibold text-accent">{t('experience.customs.veggie.architecture.frontend.features')}:</span>
<span>{t('experience.customs.veggie.architecture.frontend.features_list')}</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.veggie.architecture.backend.title')}</h4>
<ul className="list-disc list-inside space-y-2 text-secondary/70">
<li>{t('experience.customs.veggie.architecture.backend.webhooks')}</li>
<li>{t('experience.customs.veggie.architecture.backend.pipeline')}</li>
<li>{t('experience.customs.veggie.architecture.backend.emailing')}</li>
<li>{t('experience.customs.veggie.architecture.backend.logistics')}</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.veggie.architecture.security.title')}</h4>
<ul className="list-disc list-inside space-y-2 text-secondary/70">
<li>{t('experience.customs.veggie.architecture.security.payment')}</li>
<li>{t('experience.customs.veggie.architecture.security.access')}</li>
<li>{t('experience.customs.veggie.architecture.security.backup')}</li>
</ul>
</div>
</section>
<section className="space-y-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.veggie.challenges.title')}</h3>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-2">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.veggie.challenges.iteration.title')}</h4>
<p className="text-secondary/70">{t('experience.customs.veggie.challenges.iteration.description')}</p>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-2">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.veggie.challenges.cohorts.title')}</h4>
<p className="text-secondary/70">{t('experience.customs.veggie.challenges.cohorts.description')}</p>
</div>
</section>
<section className="space-y-6">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.veggie.stack.title')}</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.veggie.stack.frontend.title')}</h4>
<ul className="space-y-2 text-secondary/70 text-sm">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>HTML, CSS, JavaScript</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Cloudflare Pages</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.veggie.stack.backend.title')}</h4>
<ul className="space-y-2 text-secondary/70 text-sm">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Node.js</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Stripe Webhooks</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Brevo / MailerLite</span>
</li>
</ul>
</div>
<div className="bg-primary/50 p-5 rounded-lg border border-secondary/10 space-y-3">
<h4 className="text-lg font-medium text-secondary">{t('experience.customs.veggie.stack.tools.title')}</h4>
<ul className="space-y-2 text-secondary/70 text-sm">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Stripe Payment Links</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Make (PoC)</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>Google Sheets (PoC)</span>
</li>
</ul>
</div>
</div>
</section>
<section className="space-y-4">
<h3 className="text-xl font-semibold text-accent">{t('experience.customs.veggie.results.title')}</h3>
<ul className="space-y-2 text-secondary/70">
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.veggie.results.deployment')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.veggie.results.reliability')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.veggie.results.scalability')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.veggie.results.maintenance')}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-1"></span>
<span>{t('experience.customs.veggie.results.cost')}</span>
</li>
</ul>
</section>
<footer className="border-t border-secondary/10 pt-6">
<p className="text-sm text-secondary/60 italic">{t('experience.customs.veggie.tags')}</p>
</footer>
</div>
</div>
);
};

View File

@@ -0,0 +1,4 @@
export { CustomDocker } from './CustomDocker';
export { CustomStripe } from './CustomStripe';
export { CustomVeggie } from './CustomVeggie';
export { CustomInfra } from './CustomInfra';

View File

@@ -23,8 +23,12 @@ const Hero = () => {
{t('hero.cta')} {t('hero.cta')}
</a> </a>
<a <a
href="#projects" href="#projects"
className="px-8 py-3 border-2 border-accent text-accent rounded-lg hover:bg-accent hover:text-white transition-all" onClick={(e) => {
e.preventDefault();
document.querySelector('#experience')?.scrollIntoView({ behavior: 'smooth' });
}}
className="px-8 py-3 border-2 border-accent text-accent rounded-lg hover:bg-accent hover:text-white transition-all cursor-pointer"
> >
{t('hero.projects')} {t('hero.projects')}
</a> </a>

View File

@@ -6,11 +6,11 @@ const Skills = () => {
const skillCategories = [ const skillCategories = [
{ {
title: 'skills.languages.title', title: 'skills.languages.title',
skills: ['Python', 'JavaScript/TypeScript', 'Java', 'C', 'SQL', 'HTML/CSS', 'Bash'] skills: ['JavaScript/TypeScript', 'PHP', 'SQL', 'HTML/CSS', 'Bash', 'Go']
}, },
{ {
title: 'skills.frameworks.title', title: 'skills.frameworks.title',
skills: ['React', 'Node.js', 'Django', 'FastAPI', 'TailwindCSS', 'Bootstrap'] skills: ['React', 'Node.js', 'Effect', 'Next.js', 'TailwindCSS', 'Nest.js']
}, },
{ {
title: 'skills.tools.title', title: 'skills.tools.title',
@@ -18,7 +18,7 @@ const Skills = () => {
}, },
{ {
title: 'skills.other.title', title: 'skills.other.title',
skills: ['Agile/Scrum', 'CI/CD', 'Test unitaires', 'Architecture logicielle'] skills: ['Agile/Scrum', 'CI/CD', 'TDD', 'Architecture microservices', 'Cloud Computing', 'Ansible', 'Terraform/Opentofu']
} }
]; ];
@@ -32,12 +32,13 @@ const Skills = () => {
<h3 className="text-xl font-semibold mb-4 text-secondary">{t(category.title)}</h3> <h3 className="text-xl font-semibold mb-4 text-secondary">{t(category.title)}</h3>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{category.skills.map((skill, skillIndex) => ( {category.skills.map((skill, skillIndex) => (
<span // <span
key={skillIndex} // key={skillIndex}
className="px-4 py-2 bg-accent/10 text-accent border border-accent/20 rounded-full text-sm font-medium hover:bg-accent/20 transition-all" // className="px-4 py-2 bg-accent/10 text-accent border border-accent/20 rounded-full text-sm font-medium hover:bg-accent/20 transition-all"
> // >
{skill} // {skill}
</span> // </span>
<Skill key={skillIndex} name={skill} />
))} ))}
</div> </div>
</div> </div>
@@ -48,4 +49,12 @@ const Skills = () => {
); );
}; };
export const Skill = ({ name, key }: { name: string, key: number }) => {
return (
<span key={key} className="px-4 py-2 bg-accent/10 text-accent border border-accent/20 rounded-full text-sm font-medium hover:bg-accent/20 transition-all">
{name}
</span>
);
}
export default Skills; export default Skills;

View File

@@ -0,0 +1,76 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
interface OverlayPictureProps {
src: string;
alt: string;
className?: string;
}
const OverlayPicture: React.FC<OverlayPictureProps> = ({ src, alt, className = '' }) => {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen) {
// We don't want the escape key event to propagate to the overlay parent
// If the picture overlay is used inside another overlay, this prevents both overlays from closing
event.stopPropagation();
setIsOpen(false);
}
};
if (isOpen) {
// Use capture phase
document.addEventListener('keydown', handleEscape, true);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape, true);
document.body.style.overflow = 'unset';
};
}, [isOpen]);
return (
<>
<motion.div className={`${className} flex items-center justify-center`}>
<motion.img
src={src}
alt={alt}
className={`cursor-zoom-in flex self-center max-w-xl max-h-96 rounded-lg shadow-md object-contain`}
onClick={() => setIsOpen(true)}
transition={{ duration: 0.2 }}
layoutId='overlay-image'
/>
</motion.div>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75 backdrop-blur-sm"
onClick={(e) => {
if (e.target === e.currentTarget) {
setIsOpen(false);
}
}}
>
<motion.img
src={src}
alt={alt}
className="max-w-[90%] max-h-[80%] object-contain rounded-md"
// We don't want the click on the image to close the overlay
layoutId='overlay-image'
/>
</motion.div>
)}
</AnimatePresence>
</>
);
};
export default OverlayPicture;

View File

@@ -4,26 +4,331 @@
"professional": "Professional projects", "professional": "Professional projects",
"personal": "Personal projects" "personal": "Personal projects"
}, },
"job1": { "additional_details": "Project details",
"title": "Complete development environment setup", "skills_used": "Technologies used",
"company": "Meta Video", "project1": {
"title": "Development environment setup",
"company": "Media startup",
"description": "Porting a development environment to Docker to standardize configurations", "description": "Porting a development environment to Docker to standardize configurations",
"task1": "Design of Dockerfiles and Docker Compose configurations", "task1": "Design of Dockerfiles and Docker Compose configurations",
"task2": "Integration of various services and dependencies into isolated containers", "task2": "Integration of various services and dependencies into isolated containers",
"task3": "Integration of existing Git repositories into the new Docker environment" "task3": "Integration of existing Git repositories into the new Docker environment"
}, },
"job2": { "project2": {
"title": "Junior Developer", "title": "Payment API integration",
"company": "Startup Inc.", "company": "Media startup",
"description": "Contributed to various web development projects", "description": "Adding a payment API to a multimedia platform to enable content monetization",
"task1": "Built responsive user interfaces with React and TailwindCSS", "task1": "Design of a modular API to handle transactions and subscriptions",
"task2": "Optimized database queries and improved application performance" "task2": "Database architecture to add monetization to existing content"
}, },
"job3": { "project3": {
"title": "Intern Developer", "title": "Basket reservation system automation",
"company": "Software Solutions", "company": "Local client project",
"description": "Learned foundational software development practices", "description": "Setting up a static website with Stripe payments, database and automated emailing",
"task1": "Assisted in development of internal tools and applications", "task1": "Website creation in HTML/CSS/JS to present offers",
"task2": "Participated in code reviews and team meetings" "task2": "Order flow automation with emails and logistics management",
"task3": "Secure connection between form, payments and logistics management",
"detailedDescription": "Project designed for a vegetable basket company, with the goal of rapid deployment and minimal maintenance. I developed a lightweight but robust architecture around a static site, Stripe payment links, and Make scenarios to connect payments, logistics database and customer communication. Result: a smooth system, with no manual input, functional from day one.",
"technos": "Stripe, Make, Google Sheets, HTML, JavaScript"
},
"project4": {
"title": "Personal infrastructure",
"company": "",
"description": "Setting up a self-hosted infrastructure for various web services",
"task1": "Deployment of a Gitea instance for Git repository hosting",
"task2": "Deployment of an Infisical instance for secrets management"
},
"customs": {
"docker": {
"title": "LAMP Docker Development Environment",
"subtitle": "Standardization and containerization of the development environment",
"overview": {
"title": "Overview",
"description": "Setup of a complete Docker-based development environment to standardize development configurations across the team.",
"problem": "The company used a LAMP stack in production, which made the dev setup highly dependent on each developer's machine. The goal was to create a reproducible and easy-to-deploy environment."
},
"architecture": {
"title": "Technical Architecture",
"services": {
"title": "Docker Services",
"php": "Web server with native PHP module compilation",
"mysql": "Database with automatic initialization scripts",
"phpmyadmin": "Database management interface"
},
"features": {
"title": "Features",
"dependencies": "Automatic installation and compilation of all PHP dependencies at build time",
"repos": "Automatic source retrieval from company's Bitbucket",
"devcontainer": "VSCode Dev Containers integration for better developer experience",
"apache": "Editable Apache configuration (modules, virtualhost, etc.)",
"vhosts": "Automatic VirtualHosts configuration for direct service access",
"php_config": "Ability to switch PHP configuration based on environment (dev/prod)"
},
"devcontainer": {
"title": "VSCode Integration",
"description": "Use of VSCode Dev Containers to enable easier workspace management.",
"debugging": "Integrated XDebug debugger functional with breakpoints",
"composer": "Terminal access to container for Composer commands",
"terminal": "Attach terminal directly in container via Remote Explorer"
}
},
"challenges": {
"title": "Technical Challenges",
"modules": {
"title": "Native PHP module compilation",
"description": "Compilation from source of all necessary PHP modules for PHP 8.2, with complex system dependency management. Changing PHP version may require configuration file adjustments."
},
"standardization": {
"title": "Multi-machine standardization",
"description": "Creation of a completely reproducible environment independent of the developer's operating system (Windows, macOS, Linux), eliminating local configuration issues."
},
"automation": {
"title": "Complete setup automation",
"description": "Implementation of a one-command installation process (docker-compose up) including: image building, dependency installation, database initialization, VirtualHost configuration, and repository cloning."
}
},
"stack": {
"title": "Technology Stack",
"core": {
"title": "Core Stack"
},
"tools": {
"title": "Tools"
},
"vscode": {
"title": "VSCode Extensions"
}
},
"results": {
"title": "Results",
"standardization": "Standardized development environment for the entire team",
"onboarding": "New developer onboarding reduced to a few minutes",
"debugging": "Simplified debugging with integrated and functional XDebug",
"flexibility": "Flexible configuration (Apache, PHP, VirtualHosts) without touching code",
"productivity": "Major productivity gain: no more \"works on my machine\" issues"
},
"tags": "Docker • Docker Compose • PHP 8.2 • Apache • MySQL • VSCode Dev Containers • XDebug • LAMP Stack"
},
"stripe": {
"title": "Stripe Integration & Payment API Design",
"subtitle": "Content monetization system for video platform",
"overview": {
"title": "Overview",
"description": "Development of an integrated payment system for a premium video streaming platform. The goal was to enable the purchase or access to on-demand videos, with a flexible architecture to accommodate payment providers other than Stripe based on end-client needs."
},
"architecture": {
"title": "Technical Architecture",
"frontend": {
"title": "Frontend",
"tech": "Technologies",
"features": "Features",
"features_list": "Payment page redirect management, status retrieval, request optimization, and specific display design for paid content"
},
"backend": {
"title": "Backend",
"integration": "Stripe API integration: payment session creation, webhook listening (success, failure, refund)",
"abstraction": "Design of an abstraction layer on top of Stripe to standardize payment operations (initiation, status, refund)",
"extensibility": "Preparation for adding alternative payment methods without impacting client code",
"logging": "Complete payment flow logging for audit and reconciliation"
},
"security": {
"title": "Security & Robustness",
"webhooks": "Use of secure webhooks with Stripe signature verification",
"idempotence": "Idempotence of critical operations (avoid duplicates during retries)",
"reconciliation": "Payment status storage and periodic reconciliation logic"
}
},
"challenges": {
"title": "Technical Challenges",
"decoupling": {
"title": "Decoupled payment API design",
"description": "Architecture design enabling easy integration of payment providers other than Stripe without major code refactoring."
},
"errors": {
"title": "Critical error handling",
"description": "Implementation of robust mechanisms to handle abandoned payments, disconnected users, webhook retries, and other edge cases."
}
},
"stack": {
"title": "Technology Stack",
"frontend": {
"title": "Frontend"
},
"backend": {
"title": "Backend"
},
"tools": {
"title": "Tools",
"abstraction": "Custom payment operation abstraction"
}
},
"results": {
"title": "Results",
"operational": "Stripe payment operational quickly (with complete use case testing)",
"extensible": "API ready for adding new payment methods without refactoring",
"stable": "Stable payment logic tested in real conditions (with end users)"
},
"tags": "Stripe • PHP • MySQL • JavaScript • Webhooks • API Design • Payment Gateway"
},
"veggie": {
"title": "Vegetable Basket Reservation System",
"subtitle": "Automated subscription and reservation management platform with online payment",
"overview": {
"title": "Overview",
"description": "Development of a complete reservation and subscription management system for weekly vegetable baskets. The project needed to be quick to deploy, reliable, and inexpensive to maintain, while ensuring payment traceability and smooth logistics.",
"evolution": "The architecture initially relied on a low-code prototype (Make, Google Sheets) to validate use cases. It then evolved into a custom backend solution with webhook management, secure flow automation, and email integration."
},
"architecture": {
"title": "Technical Architecture",
"frontend": {
"title": "Frontend",
"hosting": "Hosting",
"tech": "Technologies",
"features": "Features",
"features_list": "Product presentation, FAQ, external form, and Stripe payment buttons"
},
"backend": {
"title": "Backend & Automation",
"webhooks": "Secure Stripe webhooks: real-time payment event processing",
"pipeline": "Backend pipeline: payment receipt → user data enrichment → database insertion → email triggering",
"emailing": "Email integration (Brevo/MailerLite) with automatic cohort segmentation",
"logistics": "Weekly logistics view generation for delivery team (PDF export or filtered table)"
},
"security": {
"title": "Security & Resilience",
"payment": "100% payment handled by Stripe (3D Secure, PCI compliance)",
"access": "Restricted access to sensitive data, clear role separation (admin / field team)",
"backup": "Automated backups and critical event logging"
}
},
"challenges": {
"title": "Technical Challenges",
"iteration": {
"title": "Rapid iteration towards reliability",
"description": "Initial validation via Make and Google Sheets, gradually replaced by a more robust architecture capable of handling scale and edge cases (payment failures, duplicate submissions, etc.)"
},
"cohorts": {
"title": "Seamless cohort management",
"description": "System designed to automatically track subscription cycles (12 weeks), notify users, and generate delivery files one day in advance."
}
},
"stack": {
"title": "Technology Stack",
"frontend": {
"title": "Frontend"
},
"backend": {
"title": "Backend"
},
"tools": {
"title": "Tools"
}
},
"results": {
"title": "Results",
"deployment": "Deployed in <10 days",
"reliability": "0 events lost since launch",
"scalability": "Extensible system (new plans, segmentation, custom tracking)",
"maintenance": "Minimal maintenance for field team",
"cost": "Controlled infrastructure cost (<€10/month excluding emails)"
},
"tags": "Stripe • Node.js • Webhooks • Automation • Make • Cloudflare Pages • Brevo"
},
"infrastructure": {
"title": "Infrastructure as Code (IaC) & DevOps",
"description": "Setting up an automated and scalable cloud infrastructure using modern tools.",
"task1": "Automated deployment with Terraform and Ansible.",
"task2": "Monitoring and alerts with Prometheus and Grafana.",
"task3": "CI/CD with GitHub/Gitea Actions for smooth and reliable deployments.",
"subtitle": "Design and deployment of a complete cloud infrastructure using Infrastructure as Code",
"overview": {
"title": "Overview",
"description": "Design and deployment of a complete cloud infrastructure using Infrastructure as Code, hosting multiple web services on Hetzner Cloud servers. Everything is automatically provisioned via Terraform and configured with Ansible to ensure reproducibility and maintainability."
},
"architecture": {
"title": "Technical Architecture",
"provisioning": {
"title": "Provisioning & Configuration",
"terraform": "Automated provisioning of 2 Ubuntu servers on Hetzner Cloud (Germany, Finland)",
"ansible": "System configuration, application deployment, and secrets management",
"dns": "Automated DNS record management via API"
},
"services": {
"title": "Deployed Services",
"website": {
"name": "Static website",
"description": "Portfolio and presentation"
},
"gitea": {
"name": "Gitea",
"description": "Self-hosted Git platform with MySQL"
},
"infisical": {
"name": "Infisical",
"description": "Secrets manager with PostgreSQL + Redis"
}
},
"security": {
"title": "Security & Performance",
"ssl": "Automated SSL/TLS via Let's Encrypt (Certbot)",
"nginx": "Nginx reverse proxy with HTTPS-only configuration",
"firewall": "UFW firewall + Fail2ban brute-force protection",
"docker": "Docker Compose containerization for service isolation"
}
},
"challenges": {
"title": "Technical Challenges",
"secrets": {
"title": "Secrets Management",
"description": "Implementation of an environment variable management system with validation scripts to prevent default values in production. Secrets are loaded from local files before deployment via Ansible."
},
"multiservice": {
"title": "Multi-Service Architecture",
"description": "Nginx configuration as a reverse proxy to intelligently route HTTPS traffic to multiple containerized applications on a single server, with separate SSL certificate management per subdomain."
},
"automation": {
"title": "Complete Automation",
"description": "Development of Bash scripts to automate the deployment workflow: variable validation, secrets loading, Ansible playbook execution with error handling."
}
},
"stack": {
"title": "Technology Stack",
"infrastructure": {
"title": "Infrastructure",
"cloud": "Cloud Provider: Hetzner Cloud",
"os": "OS: Ubuntu 24.04 LTS",
"iac": "IaC: Terraform 1.10, Ansible 2.19",
"containers": "Containerization: Docker, Docker Compose"
},
"applications": {
"title": "Applications",
"webserver": "Web Server: Nginx",
"gitea": "Git Hosting: Gitea (MySQL 8)",
"infisical": "Secret Management: Infisical (PostgreSQL 14, Redis 7)",
"ssl": "SSL/TLS: Let's Encrypt, Certbot"
},
"security": {
"title": "Security",
"firewall": "Firewall: UFW",
"ids": "IDS/IPS: Fail2ban",
"smtp": "SMTP: Brevo (secure notifications)"
}
},
"results": {
"title": "Results",
"automation": "Fully automated infrastructure: reproducible deployment with a single command",
"availability": "High availability: containerized services with automatic restart",
"security": "Enhanced security: mandatory HTTPS, firewall, brute-force protection",
"maintainability": "Maintainability: versioned code, configuration as code, externalized secrets",
"cost": "Optimized cost: ~€15/month for 3 production services"
},
"links": {
"title": "Links",
"website": "Website",
"gitea": "Gitea Instance",
"infisical": "Secrets Manager",
"source": "Source Code on GitHub"
}
}
} }
} }

View File

@@ -1,13 +1,13 @@
{ {
"title": "Formation", "title": "Formation",
"degree1": { "degree1": {
"title": "Licence en Informatique", "title": "Diplôme d'Ingénieur en Informatique",
"school": "Nom de l'Université", "school": "CNAM Angoulême",
"description": "Spécialisé en génie logiciel et développement web" "description": "Spécialité science et technologies des médias numériques"
}, },
"degree2": { "degree2": {
"title": "Diplôme Technique", "title": "Licence en Informatique",
"school": "Institut Technique", "school": "Université de La Rochelle",
"description": "Fondations en informatique et programmation" "description": "Fondations en informatique et programmation"
} }
} }

View File

@@ -4,7 +4,9 @@
"professional": "Projets professionnels", "professional": "Projets professionnels",
"personal": "Projets personnels" "personal": "Projets personnels"
}, },
"job1": { "additional_details": "Détails du projet",
"skills_used": "Technos utilisées",
"project1": {
"title": "Mise en place d'un environnement de développement", "title": "Mise en place d'un environnement de développement",
"company": "Startup média", "company": "Startup média",
"description": "Portage d'un environnement de dev vers Docker pour standardiser les configurations", "description": "Portage d'un environnement de dev vers Docker pour standardiser les configurations",
@@ -12,14 +14,14 @@
"task2": "Intégration des différents services et dépendances dans des conteneurs isolés", "task2": "Intégration des différents services et dépendances dans des conteneurs isolés",
"task3": "Intégration des repos Git existants dans le nouvel environnement Docker" "task3": "Intégration des repos Git existants dans le nouvel environnement Docker"
}, },
"job2": { "project2": {
"title": "Intégration d'une API de paiement", "title": "Intégration d'une API de paiement",
"company": "Startup média", "company": "Startup média",
"description": "Ajout d'une API de paiement à une plateforme multimédia pour permettre la monétisation des contenus", "description": "Ajout d'une API de paiement à une plateforme multimédia pour permettre la monétisation des contenus",
"task1": "Conception d'une API modulaire pour gérer les transactions et les abonnements", "task1": "Conception d'une API modulaire pour gérer les transactions et les abonnements",
"task2": "Architecture de la base de données pour ajouter la monétisation sur le contenu existant" "task2": "Architecture de la base de données pour ajouter la monétisation sur le contenu existant"
}, },
"job3": { "project3": {
"title": "Automatisation dun système de réservation de paniers", "title": "Automatisation dun système de réservation de paniers",
"company": "Projet client local", "company": "Projet client local",
"description": "Mise en place dun site statique avec paiements Stripe, base de données et emailing automatisé", "description": "Mise en place dun site statique avec paiements Stripe, base de données et emailing automatisé",
@@ -28,5 +30,306 @@
"task3": "Connexion sécurisée entre formulaire, paiements et gestion logistique", "task3": "Connexion sécurisée entre formulaire, paiements et gestion logistique",
"detailedDescription": "Projet conçu pour une entreprise de paniers de légumes, avec un objectif de déploiement rapide et de maintenance minimale. Jai développé une architecture légère mais robuste autour dun site statique, de liens Stripe pour les paiements, et de scénarios Make pour relier les paiements, la base de données logistique et la communication client. Résultat : un système fluide, sans saisie manuelle, fonctionnel dès les premiers jours.", "detailedDescription": "Projet conçu pour une entreprise de paniers de légumes, avec un objectif de déploiement rapide et de maintenance minimale. Jai développé une architecture légère mais robuste autour dun site statique, de liens Stripe pour les paiements, et de scénarios Make pour relier les paiements, la base de données logistique et la communication client. Résultat : un système fluide, sans saisie manuelle, fonctionnel dès les premiers jours.",
"technos": "Stripe, Make, Google Sheets, HTML, JavaScript" "technos": "Stripe, Make, Google Sheets, HTML, JavaScript"
},
"project4": {
"title": "Infrastructure personnelle",
"company": "",
"description": "Mise en place d'une infrastructure auto-hébergée pour divers services web",
"task1": "Déploiement d'un gitea pour l'hébergement de dépôts Git",
"task2": "Déploiement d'une instance infisical pour la gestion des secrets"
},
"customs": {
"docker": {
"title": "Environnement de développement Docker LAMP",
"subtitle": "Standardisation et conteneurisation de l'environnement de développement",
"overview": {
"title": "Vue d'ensemble",
"description": "Mise en place d'un environnement de développement complet basé sur Docker pour standardiser les configurations de développement au sein de l'équipe.",
"problem": "L'entreprise utilisait une stack LAMP en production, ce qui rendait le setup de développement hyper dépendant du poste de chaque développeur. L'objectif était de créer un environnement reproductible et facile à déployer."
},
"architecture": {
"title": "Architecture Technique",
"services": {
"title": "Services Docker",
"php": "Serveur web avec compilation native des modules PHP",
"mysql": "Base de données avec scripts d'initialisation automatique",
"phpmyadmin": "Interface de gestion de base de données"
},
"features": {
"title": "Fonctionnalités",
"dependencies": "Installation et compilation automatique de toutes les dépendances PHP au build",
"repos": "Récupération automatique des sources depuis Bitbucket de l'entreprise",
"devcontainer": "Intégration VSCode Dev Containers pour une meilleure expérience développeur",
"apache": "Configuration Apache éditable (modules, virtualhost, etc.)",
"vhosts": "Configuration automatique des VirtualHosts pour accès direct aux services",
"php_config": "Possibilité de changer de configuration PHP selon l'environnement (dev/prod)"
},
"devcontainer": {
"title": "Intégration VSCode",
"description": "Utilisation des Dev Containers VSCode pour permettre un travail plus simple avec les workspaces.",
"debugging": "Debugger XDebug intégré fonctionnel avec breakpoints",
"composer": "Accès terminal au conteneur pour commandes Composer",
"terminal": "Attach terminal directement dans le conteneur via Remote Explorer"
}
},
"challenges": {
"title": "Défis Techniques Relevés",
"modules": {
"title": "Compilation native des modules PHP",
"description": "Compilation à partir des sources de tous les modules PHP nécessaires pour PHP 8.2, avec gestion des dépendances système complexes. Le changement de version PHP peut nécessiter des ajustements dans les fichiers de configuration."
},
"standardization": {
"title": "Standardisation multi-poste",
"description": "Création d'un environnement complètement reproductible indépendant du système d'exploitation du développeur (Windows, macOS, Linux), éliminant les problèmes de configuration locaux."
},
"automation": {
"title": "Automatisation complète du setup",
"description": "Mise en place d'un processus d'installation en une commande (docker-compose up) incluant : build des images, installation des dépendances, initialisation des bases de données, configuration des VirtualHosts et clonage des repositories."
}
},
"stack": {
"title": "Stack Technologique",
"core": {
"title": "Stack principale"
},
"tools": {
"title": "Outils"
},
"vscode": {
"title": "Extensions VSCode"
}
},
"results": {
"title": "Résultats",
"standardization": "Environnement de développement standardisé pour toute l'équipe",
"onboarding": "Onboarding des nouveaux développeurs réduit à quelques minutes",
"debugging": "Débogage facilité avec XDebug intégré et fonctionnel",
"flexibility": "Configuration flexible (Apache, PHP, VirtualHosts) sans toucher au code",
"productivity": "Gain de productivité majeur : plus de problèmes de \"ça marche sur ma machine\""
},
"tags": "Docker • Docker Compose • PHP 8.2 • Apache • MySQL • VSCode Dev Containers • XDebug • LAMP Stack"
},
"stripe": {
"title": "Intégration Stripe & Design d'API de paiement",
"subtitle": "Système de monétisation de contenu pour plateforme vidéo",
"overview": {
"title": "Vue d'ensemble",
"description": "Développement d'un système de paiement intégré pour une plateforme de diffusion de contenus vidéo premium. L'objectif était de permettre l'achat ou l'accès à des vidéos à la demande, avec une architecture suffisamment souple pour accueillir d'autres fournisseurs que Stripe selon les besoins des clients finaux."
},
"architecture": {
"title": "Architecture Technique",
"frontend": {
"title": "Frontend",
"tech": "Technologies",
"features": "Fonctionnalités",
"features_list": "Gestion des redirections vers les pages de paiement, récupération de statuts, optimisation des requêtes et design d'un affichage spécifique pour le contenu payant"
},
"backend": {
"title": "Backend",
"integration": "Intégration de l'API Stripe : création de sessions de paiement, écoute des webhooks (success, failure, refund)",
"abstraction": "Conception d'une couche d'abstraction au-dessus de Stripe pour standardiser les opérations de paiement (initiation, statut, remboursement)",
"extensibility": "Préparation à l'ajout de moyens de paiement alternatifs sans impacter le code client",
"logging": "Journalisation complète des flux de paiement pour audit et réconciliation"
},
"security": {
"title": "Sécurité & Robustesse",
"webhooks": "Utilisation de webhooks sécurisés avec vérification de signature Stripe",
"idempotence": "Idempotence des opérations critiques (éviter les doublons lors des retries)",
"reconciliation": "Stockage des statuts de paiement et logique de réconciliation périodique"
}
},
"challenges": {
"title": "Défis Techniques Relevés",
"decoupling": {
"title": "Conception d'une API de paiement découplée",
"description": "Design d'une architecture permettant d'intégrer facilement d'autres fournisseurs de paiement que Stripe sans refonte majeure du code existant."
},
"errors": {
"title": "Gestion des erreurs critiques",
"description": "Implémentation de mécanismes robustes pour gérer les paiements abandonnés, utilisateurs déconnectés, retries de webhooks et autres cas limites."
}
},
"stack": {
"title": "Stack Technologique",
"frontend": {
"title": "Frontend"
},
"backend": {
"title": "Backend"
},
"tools": {
"title": "Outils",
"abstraction": "Abstraction maison des opérations de paiement"
}
},
"results": {
"title": "Résultats",
"operational": "Paiement Stripe opérationnel rapidement (avec test complet des cas d'usage)",
"extensible": "API prête pour l'ajout de nouveaux moyens de paiement sans refonte",
"stable": "Logique de paiement stable et testée en conditions réelles (avec utilisateurs finaux)"
},
"tags": "Stripe • PHP • MySQL • JavaScript • Webhooks • API Design • Payment Gateway"
},
"veggie": {
"title": "Système de réservation de paniers de légumes",
"subtitle": "Plateforme automatisée de gestion d'abonnements et de réservations avec paiement en ligne",
"overview": {
"title": "Vue d'ensemble",
"description": "Développement d'un système complet de réservation et de gestion d'abonnements à des paniers de légumes hebdomadaires. Le projet devait être rapide à mettre en production, fiable et peu coûteux à maintenir, tout en garantissant la traçabilité des paiements et la fluidité logistique.",
"evolution": "L'architecture a d'abord reposé sur un prototype low-code (Make, Google Sheets) pour valider les usages. Elle a ensuite évolué vers une solution backend personnalisée avec gestion par webhook, automatisation sécurisée des flux et intégration emailing."
},
"architecture": {
"title": "Architecture Technique",
"frontend": {
"title": "Frontend",
"hosting": "Hébergement",
"tech": "Technologies",
"features": "Fonctionnalités",
"features_list": "Présentation des offres, FAQ, formulaire externe, et boutons de paiement Stripe"
},
"backend": {
"title": "Backend & Automatisation",
"webhooks": "Webhooks Stripe sécurisés : traitement des événements de paiement en temps réel",
"pipeline": "Pipeline backend : réception du paiement → enrichissement des données utilisateur → ajout en base de données → déclenchement d'emails",
"emailing": "Intégration emailing (Brevo/MailerLite) avec segmentation automatique par cohorte",
"logistics": "Génération de vues logistiques hebdomadaires pour l'équipe livraison (export PDF ou table filtrée)"
},
"security": {
"title": "Sécurité & Résilience",
"payment": "Paiement 100% géré par Stripe (3D Secure, conformité PCI)",
"access": "Accès restreint aux données sensibles, séparation claire des rôles (admin / équipe terrain)",
"backup": "Backups automatisés et journalisation des événements critiques"
}
},
"challenges": {
"title": "Défis Techniques Relevés",
"iteration": {
"title": "Itération rapide vers fiabilité",
"description": "Validation initiale via Make et Google Sheets, remplacée progressivement par une architecture plus robuste, capable de supporter la montée en charge et les cas limites (échecs de paiement, double soumission, etc.)"
},
"cohorts": {
"title": "Gestion sans faille des cohortes",
"description": "Système conçu pour suivre automatiquement les cycles d'abonnement (12 semaines), notifier les utilisateurs, et générer les bons fichiers de livraison à J1."
}
},
"stack": {
"title": "Stack Technologique",
"frontend": {
"title": "Frontend"
},
"backend": {
"title": "Backend"
},
"tools": {
"title": "Outils"
}
},
"results": {
"title": "Résultats",
"deployment": "Mise en production en <10 jours",
"reliability": "0 événement perdu depuis lancement",
"scalability": "Système extensible (ajout de formules, segmentation, suivis personnalisés)",
"maintenance": "Maintenance minimale pour l'équipe terrain",
"cost": "Coût infra maîtrisé (<10€/mois hors emails)"
},
"tags": "Stripe • Node.js • Webhooks • Automation • Make • Cloudflare Pages • Brevo"
},
"infrastructure": {
"title": "Infrastructure as Code (IaC) & DevOps",
"description": "Mise en place d'une infrastructure cloud automatisée et scalable utilisant des outils modernes.",
"task1": "Déploiement automatisé avec Terraform et Ansible.",
"task2": "Surveillance et alertes avec Prometheus et Grafana.",
"task3": "CI/CD avec GitHub/Gitea Actions pour des déploiements fluides et fiables.",
"subtitle": "Conception et déploiement d'une infrastructure cloud complète en Infrastructure as Code",
"overview": {
"title": "Vue d'ensemble",
"description": "Conception et déploiement d'une infrastructure cloud complète en Infrastructure as Code, hébergeant plusieurs services web sur des serveurs Hetzner Cloud. L'ensemble est provisionné automatiquement via Terraform et configuré avec Ansible pour garantir reproductibilité et maintenabilité."
},
"architecture": {
"title": "Architecture Technique",
"provisioning": {
"title": "Provisioning & Configuration",
"terraform": "Provisioning automatisé de 2 serveurs Ubuntu sur Hetzner Cloud (Allemagne, Finlande)",
"ansible": "Configuration système, déploiement des applications et gestion des secrets",
"dns": "Gestion automatisée des enregistrements DNS via API"
},
"services": {
"title": "Services Déployés",
"website": {
"name": "Site web statique",
"description": "Portfolio et présentation"
},
"gitea": {
"name": "Gitea",
"description": "Plateforme Git auto-hébergée avec MySQL"
},
"infisical": {
"name": "Infisical",
"description": "Gestionnaire de secrets avec PostgreSQL + Redis"
}
},
"security": {
"title": "Sécurité & Performance",
"ssl": "SSL/TLS automatisé via Let's Encrypt (Certbot)",
"nginx": "Reverse proxy Nginx avec configuration HTTPS uniquement",
"firewall": "Firewall UFW + protection anti-bruteforce Fail2ban",
"docker": "Conteneurisation Docker Compose pour isolation des services"
}
},
"challenges": {
"title": "Défis Techniques Relevés",
"secrets": {
"title": "Gestion des Secrets",
"description": "Mise en place d'un système de gestion des variables d'environnement avec scripts de validation pour éviter les valeurs par défaut en production. Les secrets sont chargés depuis des fichiers locaux avant déploiement via Ansible."
},
"multiservice": {
"title": "Architecture Multi-Services",
"description": "Configuration de Nginx comme reverse proxy pour router intelligemment le trafic HTTPS vers plusieurs applications conteneurisées sur un même serveur, avec gestion des certificats SSL distincts par sous-domaine."
},
"automation": {
"title": "Automatisation Complète",
"description": "Développement de scripts Bash pour automatiser le workflow de déploiement : validation des variables, chargement des secrets, exécution des playbooks Ansible avec gestion d'erreurs."
}
},
"stack": {
"title": "Stack Technologique",
"infrastructure": {
"title": "Infrastructure",
"cloud": "Cloud Provider: Hetzner Cloud",
"os": "OS: Ubuntu 24.04 LTS",
"iac": "IaC: Terraform 1.10, Ansible 2.19",
"containers": "Conteneurisation: Docker, Docker Compose"
},
"applications": {
"title": "Applications",
"webserver": "Web Server: Nginx",
"gitea": "Git Hosting: Gitea (MySQL 8)",
"infisical": "Secret Management: Infisical (PostgreSQL 14, Redis 7)",
"ssl": "SSL/TLS: Let's Encrypt, Certbot"
},
"security": {
"title": "Sécurité",
"firewall": "Firewall: UFW",
"ids": "IDS/IPS: Fail2ban",
"smtp": "SMTP: Brevo (notifications sécurisées)"
}
},
"results": {
"title": "Résultats",
"automation": "Infrastructure entièrement automatisée : déploiement reproductible en une seule commande",
"availability": "Haute disponibilité : services conteneurisés avec restart automatique",
"security": "Sécurité renforcée : HTTPS obligatoire, firewall, anti-bruteforce",
"maintainability": "Maintenabilité : code versionné, configuration as code, secrets externalisés",
"cost": "Coût optimisé : ~15€/mois pour 3 services en production"
},
"links": {
"title": "Liens",
"website": "Site web",
"gitea": "Instance Gitea",
"infisical": "Gestionnaire de secrets",
"source": "Code source sur GitHub",
"alert": "Vous y êtes déjà !"
}
}
} }
} }