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:
@@ -60,10 +60,12 @@ function App() {
|
||||
setThemeSelected(newTheme);
|
||||
};
|
||||
|
||||
const scrollToSection = (id: string) => {
|
||||
const scrollToSection = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
|
||||
e.preventDefault();
|
||||
const section = document.getElementById(id);
|
||||
if (section) {
|
||||
section.scrollIntoView({ behavior: 'smooth' });
|
||||
window.history.pushState(null, '', `#${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,11 +85,11 @@ function App() {
|
||||
Louis EMARD
|
||||
</h1>
|
||||
<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 onClick={() => scrollToSection('experience')} className='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 onClick={() => scrollToSection('education')} className='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='#about' onClick={(e) => scrollToSection(e, 'about')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.about')}</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 href='#skills' onClick={(e) => scrollToSection(e, 'skills')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.skills')}</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 href='#contact' onClick={(e) => scrollToSection(e, 'contact')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.contact')}</a>
|
||||
|
||||
{/* Language Toggle */}
|
||||
<button
|
||||
@@ -107,7 +109,7 @@ function App() {
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className='bg-'>
|
||||
<main>
|
||||
<Hero />
|
||||
<About />
|
||||
<Experience />
|
||||
@@ -118,7 +120,7 @@ function App() {
|
||||
|
||||
{/* Footer */}
|
||||
<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'>
|
||||
Gitea
|
||||
</a>
|
||||
|
||||
@@ -5,13 +5,13 @@ const Education = () => {
|
||||
|
||||
const educations = [
|
||||
{
|
||||
period: '2020 - 2023',
|
||||
period: '2018 - 2021',
|
||||
title: 'education.degree1.title',
|
||||
school: 'education.degree1.school',
|
||||
description: 'education.degree1.description'
|
||||
},
|
||||
{
|
||||
period: '2018 - 2020',
|
||||
period: '2015 - 2018',
|
||||
title: 'education.degree2.title',
|
||||
school: 'education.degree2.school',
|
||||
description: 'education.degree2.description'
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
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 { Skill } from '../Skills';
|
||||
import { CustomInfra, CustomVeggie, CustomStripe, CustomDocker } from './custom';
|
||||
|
||||
type ProjectType = 'professional' | 'personal';
|
||||
|
||||
@@ -12,66 +14,105 @@ const Experience = () => {
|
||||
const professionalExperiences = [
|
||||
{
|
||||
period: '2024',
|
||||
title: 'experience.job1.title',
|
||||
company: 'experience.job1.company',
|
||||
description: 'experience.job1.description',
|
||||
tasks: ['experience.job1.task1', 'experience.job1.task2', 'experience.job1.task3']
|
||||
title: 'experience.project1.title',
|
||||
company: 'experience.project1.company',
|
||||
description: 'experience.project1.description',
|
||||
tasks: ['experience.project1.task1', 'experience.project1.task2', 'experience.project1.task3'],
|
||||
skills: ['Docker', 'Docker Compose', 'PHP', 'Apache', 'MySQL', 'VSCode'],
|
||||
customContent: <CustomDocker t={t} />
|
||||
},
|
||||
{
|
||||
period: '2024',
|
||||
title: 'experience.job2.title',
|
||||
company: 'experience.job2.company',
|
||||
description: 'experience.job2.description',
|
||||
tasks: ['experience.job2.task1', 'experience.job2.task2']
|
||||
title: 'experience.project2.title',
|
||||
company: 'experience.project2.company',
|
||||
description: 'experience.project2.description',
|
||||
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 = [
|
||||
{
|
||||
period: '2018 - 2025',
|
||||
title: 'experience.job3.title',
|
||||
company: 'experience.job3.company',
|
||||
description: 'experience.job3.description',
|
||||
tasks: ['experience.job3.task1', 'experience.job3.task2']
|
||||
period: '2025',
|
||||
title: 'experience.project4.title',
|
||||
company: 'experience.project4.company',
|
||||
description: 'experience.project4.description',
|
||||
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 routes = useRoutes(['/', '/experiences/:xpId'].map((path, id) => ({
|
||||
const routes = useRoutes(['/', '/experiences/:xpId'].map((path) => ({
|
||||
path,
|
||||
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>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-4 mb-8 border-b border-secondary/20">
|
||||
<button
|
||||
onClick={() => setActiveTab('professional')}
|
||||
className={`px-6 py-3 font-medium transition-all ${
|
||||
className={`relative px-6 py-3 font-medium transition-colors ${
|
||||
activeTab === 'professional'
|
||||
? 'text-accent border-b-2 border-accent'
|
||||
? 'text-accent'
|
||||
: 'text-secondary/60 hover:text-secondary'
|
||||
}`}
|
||||
>
|
||||
{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
|
||||
onClick={() => setActiveTab('personal')}
|
||||
className={`px-6 py-3 font-medium transition-all ${
|
||||
className={`relative px-6 py-3 font-medium transition-colors ${
|
||||
activeTab === 'personal'
|
||||
? 'text-accent border-b-2 border-accent'
|
||||
? 'text-accent'
|
||||
: 'text-secondary/60 hover:text-secondary'
|
||||
}`}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
<ExperiencesList experiences={currentExperiences} t={t} />
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.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>
|
||||
</>
|
||||
)
|
||||
@@ -90,6 +131,9 @@ interface Experience {
|
||||
company: string;
|
||||
description: string;
|
||||
tasks: string[];
|
||||
detailedDescription?: string;
|
||||
skills?: string[];
|
||||
customContent?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ExperiencesListProps {
|
||||
@@ -98,24 +142,18 @@ interface ExperiencesListProps {
|
||||
}
|
||||
|
||||
const ExperiencesList = ({ experiences, t }: ExperiencesListProps) => {
|
||||
const { xpId } = useParams();
|
||||
const { xpId } = useParams() as { xpId?: number };
|
||||
return <div className=' flex flex-col space-y-8'>
|
||||
{experiences.map((exp, index) => <ExperienceSummary key={index} index={index} exp={exp} t={t} />)}
|
||||
<AnimatePresence mode="wait">
|
||||
{xpId !== undefined && <DetailedExperience t={t} />}
|
||||
{xpId !== undefined && <DetailedExperience t={t} exp={experiences[xpId]} />}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
}
|
||||
|
||||
interface ExperienceSummaryProps {
|
||||
index: number;
|
||||
exp: {
|
||||
period: string;
|
||||
title: string;
|
||||
company: string;
|
||||
description: string;
|
||||
tasks: string[];
|
||||
};
|
||||
exp: Experience;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
@@ -152,6 +190,9 @@ const ExperienceSummary = ({ index, exp, t }: ExperienceSummaryProps) => {
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
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" />
|
||||
</motion.svg>
|
||||
@@ -177,9 +218,10 @@ const ExperienceSummary = ({ index, exp, t }: ExperienceSummaryProps) => {
|
||||
|
||||
interface DetailedExperienceProps {
|
||||
t: (key: string) => string;
|
||||
exp: Experience;
|
||||
}
|
||||
|
||||
const DetailedExperience = ({ t }: DetailedExperienceProps) => {
|
||||
const DetailedExperience = ({ t, exp }: DetailedExperienceProps) => {
|
||||
const { xpId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
@@ -216,47 +258,38 @@ const DetailedExperience = ({ t }: DetailedExperienceProps) => {
|
||||
>
|
||||
<motion.div
|
||||
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 }}
|
||||
>
|
||||
<motion.button
|
||||
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">
|
||||
<div className="flex flex-row md:justify-between md:items-start mb-6">
|
||||
<motion.div
|
||||
layoutId={`experience-header-${xpId}`}
|
||||
transition={{ type: "spring", stiffness: 350, damping: 35 }}
|
||||
className="flex-1"
|
||||
>
|
||||
<h3 className="text-2xl font-semibold text-secondary mb-1">Dev full stack</h3>
|
||||
<p className="text-accent font-medium">Meta Video</p>
|
||||
<h3 className="text-xl md:text-2xl font-semibold text-secondary mb-1">{t(exp.title)}</h3>
|
||||
<p className="text-accent font-medium">{t(exp.company)}</p>
|
||||
</motion.div>
|
||||
<div className="flex items-center gap-3 mt-2 md:mt-0">
|
||||
<motion.span
|
||||
layoutId={`experience-period-${xpId}`}
|
||||
className="text-secondary/60 text-sm md:text-base"
|
||||
|
||||
<div className="flex items-start gap-3 mt-2 md:mt-0">
|
||||
<motion.button
|
||||
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 }}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
2023 - 2024
|
||||
</motion.span>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -266,8 +299,19 @@ const DetailedExperience = ({ t }: DetailedExperienceProps) => {
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<h4 className="text-xl font-semibold text-secondary mb-4">Détails supplémentaires</h4>
|
||||
<p className="text-secondary/70 mb-4">Contenu détaillé de l'expérience...</p>
|
||||
{/* <h4 className="text-xl font-semibold text-secondary mb-4">{t('experience.additional_details')}</h4> */}
|
||||
{/* <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>;
|
||||
|
||||
181
src/components/Experience/custom/CustomDocker.tsx
Normal file
181
src/components/Experience/custom/CustomDocker.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
233
src/components/Experience/custom/CustomInfra.tsx
Normal file
233
src/components/Experience/custom/CustomInfra.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
144
src/components/Experience/custom/CustomStripe.tsx
Normal file
144
src/components/Experience/custom/CustomStripe.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
167
src/components/Experience/custom/CustomVeggie.tsx
Normal file
167
src/components/Experience/custom/CustomVeggie.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
4
src/components/Experience/custom/index.ts
Normal file
4
src/components/Experience/custom/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { CustomDocker } from './CustomDocker';
|
||||
export { CustomStripe } from './CustomStripe';
|
||||
export { CustomVeggie } from './CustomVeggie';
|
||||
export { CustomInfra } from './CustomInfra';
|
||||
@@ -23,8 +23,12 @@ const Hero = () => {
|
||||
{t('hero.cta')}
|
||||
</a>
|
||||
<a
|
||||
href="#projects"
|
||||
className="px-8 py-3 border-2 border-accent text-accent rounded-lg hover:bg-accent hover:text-white transition-all"
|
||||
href="#projects"
|
||||
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')}
|
||||
</a>
|
||||
|
||||
@@ -6,11 +6,11 @@ const Skills = () => {
|
||||
const skillCategories = [
|
||||
{
|
||||
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',
|
||||
skills: ['React', 'Node.js', 'Django', 'FastAPI', 'TailwindCSS', 'Bootstrap']
|
||||
skills: ['React', 'Node.js', 'Effect', 'Next.js', 'TailwindCSS', 'Nest.js']
|
||||
},
|
||||
{
|
||||
title: 'skills.tools.title',
|
||||
@@ -18,7 +18,7 @@ const Skills = () => {
|
||||
},
|
||||
{
|
||||
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>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{category.skills.map((skill, skillIndex) => (
|
||||
<span
|
||||
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"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
// <span
|
||||
// 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"
|
||||
// >
|
||||
// {skill}
|
||||
// </span>
|
||||
<Skill key={skillIndex} name={skill} />
|
||||
))}
|
||||
</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;
|
||||
|
||||
76
src/components/utility/OverlayPicture.tsx
Normal file
76
src/components/utility/OverlayPicture.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user