feat(stack): grille complète (71 technos) en masonry centré + peaufinage UI
All checks were successful
Deploy production / build-deploy (push) Successful in 24s
All checks were successful
Deploy production / build-deploy (push) Successful in 24s
Stack & icônes - grille unique enrichie (+24 technos) : React, TypeORM, Apache, Gitea, GitLab, GitHub Actions, Vite, pnpm, Socket.io, Chart.js, Discord.js, Prisma, Sequelize, IntelliJ IDEA, VS Code, Windows, HubSpot, HTML5/CSS3… - nouvelles catégories « Bases de données & ORM » et « Tests & Qualité » - 21 icônes simple-icons + tracés monochromes custom VS Code, Windows, Playwright ; support du fill-rule dans TechIcon - mise en page masonry (CSS columns) + titres et puces centrés Contenu & UI - Hero : retrait du badge « ouvert aux opportunités » - About / Contact : reformulation orientée projets - ProjectCard : statut « en développement » - typographie : tirets cadratins → virgules (titres, OG, aria-labels) - ajout du visuel public/images/logo/couverture.png
This commit is contained in:
@@ -14,7 +14,7 @@ import { profile, stats } from "../data/content";
|
||||
</div>
|
||||
<div>
|
||||
<div class="eyebrow">01 / à propos</div>
|
||||
<h2 class="h2 about-h2">Un développeur qui livre, du code à la prod.</h2>
|
||||
<h2 class="h2 about-h2">Penser, construire, livrer.</h2>
|
||||
<p class="about-p">{profile.bio}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,9 @@ import { social, coffeeUrl } from "../data/content";
|
||||
<section class="section container" id="contact">
|
||||
<div class="reveal contact-card">
|
||||
<div class="eyebrow">06 / contact</div>
|
||||
<h2 class="contact-h2">Une idée, un poste, un projet ? Parlons-en.</h2>
|
||||
<h2 class="contact-h2">Une idée à construire ensemble ? Parlons-en.</h2>
|
||||
<p class="contact-p">
|
||||
Disponible pour de nouvelles opportunités. Le plus simple, c'est un email — je réponds vite.
|
||||
Toujours partant pour échanger autour d'un projet. Le plus simple, c'est un email. Je réponds vite.
|
||||
</p>
|
||||
|
||||
<div class="contact-cta">
|
||||
|
||||
@@ -12,7 +12,7 @@ const nav = [
|
||||
---
|
||||
<header class="site-header">
|
||||
<nav class="nav" aria-label="Navigation principale">
|
||||
<a class="nav-logo" href="#top" aria-label="Johan Leroy — accueil">
|
||||
<a class="nav-logo" href="#top" aria-label="Johan Leroy, accueil">
|
||||
<img src="/images/logo/logo.png" alt="Logo Johan Leroy" width="36" height="29" />
|
||||
</a>
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { profile, social, terminalText } from "../data/content";
|
||||
<section class="section container hero">
|
||||
<div class="hero-grid">
|
||||
<div>
|
||||
<span class="badge"><span class="badge-dot"></span>Ouvert aux opportunités</span>
|
||||
<h1>{profile.name}</h1>
|
||||
<p class="hero-role">{profile.role}</p>
|
||||
<p class="hero-lede">{profile.lede}</p>
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Project } from "../data/content";
|
||||
|
||||
const STATUS: Record<Project["status"], { label: string; color: string }> = {
|
||||
live: { label: "en ligne", color: "var(--accent)" },
|
||||
dev: { label: "en développement", color: "var(--warn)" },
|
||||
archived: { label: "archivé", color: "var(--text-dim)" },
|
||||
oss: { label: "open-source", color: "var(--info)" },
|
||||
};
|
||||
@@ -16,7 +17,7 @@ const mono = p.title.replace(/[^A-Za-z0-9]/g, "").slice(0, 2);
|
||||
// Toute la carte devient cliquable (nouvel onglet) dès qu'un lien est défini.
|
||||
const Tag = p.link ? "a" : "article";
|
||||
const linkAttrs = p.link
|
||||
? { href: p.link, target: "_blank", rel: "noopener", "aria-label": `${p.title} — voir le projet (nouvel onglet)` }
|
||||
? { href: p.link, target: "_blank", rel: "noopener", "aria-label": `${p.title}, voir le projet (nouvel onglet)` }
|
||||
: {};
|
||||
---
|
||||
<Tag class:list={["card", p.link && "card--link"]} {...linkAttrs}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import TechIcon from "./TechIcon.astro";
|
||||
import { stackMain, stackSecondary, softTech, softHuman } from "../data/content";
|
||||
import { stack, softTech, softHuman } from "../data/content";
|
||||
import { brandColor } from "../lib/techIcons";
|
||||
---
|
||||
<section class="section container" id="stack">
|
||||
<div class="reveal" style="margin-bottom:36px;">
|
||||
@@ -9,27 +10,18 @@ import { stackMain, stackSecondary, softTech, softHuman } from "../data/content"
|
||||
</div>
|
||||
|
||||
<div class="reveal stack-grid">
|
||||
{stackMain.map((g) => (
|
||||
{stack.map((g) => (
|
||||
<div class="stack-group">
|
||||
<div class="stack-group-label">{g.label}</div>
|
||||
<div class="stack-group-label">{g.label}<span class="stack-group-count">{g.items.length}</span></div>
|
||||
<div class="tiles">
|
||||
{g.items.map((it) => (
|
||||
<div class="tile"><TechIcon name={it} /><span class="tile-name">{it}</span></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="reveal label-mono" style="margin:36px 0 16px;">// également à l'aise avec</div>
|
||||
<div class="reveal stack-grid is-secondary">
|
||||
{stackSecondary.map((g) => (
|
||||
<div class="stack-group">
|
||||
<div class="stack-group-label">{g.label}</div>
|
||||
<div class="tiles">
|
||||
{g.items.map((it) => (
|
||||
<div class="tile"><TechIcon name={it} /><span class="tile-name">{it}</span></div>
|
||||
))}
|
||||
{g.items.map((it) => {
|
||||
const c = brandColor(it);
|
||||
return (
|
||||
<div class="tile" style={c ? `--brand:${c}` : undefined}>
|
||||
<TechIcon name={it} /><span class="tile-name">{it}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -6,11 +6,13 @@ interface Props { name: string; }
|
||||
const { name } = Astro.props;
|
||||
const def: TechDef | undefined = techIcons[name];
|
||||
const path = def && "path" in def ? def.path : null;
|
||||
const vb = def && "path" in def && def.vb ? def.vb : "0 0 24 24";
|
||||
const fr = def && "path" in def && def.fr ? def.fr : undefined;
|
||||
const mono = def && "mono" in def ? def.mono : monogram(name);
|
||||
---
|
||||
{path ? (
|
||||
<svg class="ic" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d={path} />
|
||||
<svg class="ic" viewBox={vb} fill="currentColor" aria-hidden="true">
|
||||
<path d={path} fill-rule={fr} />
|
||||
</svg>
|
||||
) : (
|
||||
<span class="tile-mono">{mono}</span>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
c'est ici que ça se passe. Tout est typé.
|
||||
============================================================ */
|
||||
|
||||
export type ProjectStatus = "live" | "archived" | "oss";
|
||||
export type ProjectStatus = "live" | "dev" | "archived" | "oss";
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
@@ -53,9 +53,9 @@ export const profile = {
|
||||
name: "Johan Leroy",
|
||||
role: "Développeur fullstack",
|
||||
lede:
|
||||
"Je conçois et développe des applications web performantes, propres et scalables — du front jusqu'à la mise en production. Basé à Nantes, profil « builder » : open-source, auto-hébergement, projets perso.",
|
||||
"Je conçois et développe des applications web performantes, propres et scalables, du front jusqu'à la mise en production. Basé à Nantes, profil « builder » : open-source, auto-hébergement, projets perso.",
|
||||
bio:
|
||||
"Développeur fullstack passionné par les technologies modernes. J'aime construire des produits complets — penser l'archi, soigner l'expérience, déployer et maintenir. J'expérimente en continu via mes projets perso, mes services auto-hébergés et l'open-source.",
|
||||
"Développeur fullstack passionné par les technologies modernes, j'aime construire des produits complets : penser l'architecture, soigner l'expérience, déployer et maintenir. J'expérimente en continu via mes projets perso, mes services auto-hébergés et l'open-source.",
|
||||
};
|
||||
|
||||
export const social = {
|
||||
@@ -86,7 +86,7 @@ export const projects: Project[] = [
|
||||
desc:
|
||||
"Plateforme client + admin de gestion d'hébergement web et de domaines pour une association, bâtie sur WHMCS. Design system « Liquid Glass » clair/sombre.",
|
||||
stack: ["Next.js 16", "React 19", "NestJS 11", "Tailwind 4", "shadcn/ui", "Redis", "WHMCS API"],
|
||||
status: "live",
|
||||
status: "dev",
|
||||
link: "https://techos-asso.fr",
|
||||
img: "/images/projets/techos.png",
|
||||
imgDark: "/images/projets/techos-dark.png",
|
||||
@@ -94,9 +94,9 @@ export const projects: Project[] = [
|
||||
{
|
||||
id: "amarea",
|
||||
title: "Amarea Tattoo",
|
||||
subtitle: "Site du studio de tatouage Amarea",
|
||||
subtitle: "Site d'une tatoueuse indépendante",
|
||||
desc:
|
||||
"Site vitrine + gestion pour un studio de tatouage. Architecture découplée SPA + API REST avec gestion de médias.",
|
||||
"Site vitrine + gestion pour une tatoueuse indépendante. Architecture découplée SPA + API REST avec gestion de médias.",
|
||||
stack: ["Vue 3", "Vite", "NestJS 11", "TypeORM", "MariaDB", "Tailwind 4"],
|
||||
status: "live",
|
||||
link: "https://amarea-tattoo.fr",
|
||||
@@ -180,7 +180,7 @@ export const experiences: Experience[] = [
|
||||
company: "Almeria",
|
||||
logo: "/images/entreprises/almeria.png",
|
||||
role: "Développeur",
|
||||
period: "2023 — 2025",
|
||||
period: "2023–2025",
|
||||
duration: "2 ans",
|
||||
desc: "Conception et développement d'applications web et de logiciels d'intégration ERP.",
|
||||
missions: [
|
||||
@@ -193,8 +193,8 @@ export const experiences: Experience[] = [
|
||||
{
|
||||
company: "CEREMA",
|
||||
logo: "/images/entreprises/cerema.png",
|
||||
role: "Technicien évaluation — mesure trafic & mobilité",
|
||||
period: "2021 — 2023",
|
||||
role: "Technicien évaluation, mesure trafic & mobilité",
|
||||
period: "2021–2023",
|
||||
duration: "2 ans",
|
||||
desc: "Installation de capteurs et analyse de données de mobilité.",
|
||||
missions: [
|
||||
@@ -208,7 +208,7 @@ export const experiences: Experience[] = [
|
||||
company: "Réseau SNCF",
|
||||
logo: "/images/entreprises/sncf.png",
|
||||
role: "Agent télécom",
|
||||
period: "2019 — 2021",
|
||||
period: "2019–2021",
|
||||
duration: "2 ans",
|
||||
desc:
|
||||
"Maintenance des équipements de sonorisation, d'affichage et de téléphonie sur le réseau ferroviaire.",
|
||||
@@ -255,20 +255,20 @@ export const experiences: Experience[] = [
|
||||
export const formation: Formation[] = [
|
||||
{
|
||||
title: "Concepteur Développeur d'Applications",
|
||||
school: "Campus ENI — Nantes",
|
||||
school: "Campus ENI, Nantes",
|
||||
period: "Depuis 2023",
|
||||
desc: "Conception & dév d'applications web/mobiles, architecture logicielle, gestion de projet.",
|
||||
},
|
||||
{
|
||||
title: "BTS Systèmes Numériques",
|
||||
school: "Saint Félix La Salle — Nantes",
|
||||
period: "2021 — 2023",
|
||||
school: "Saint Félix La Salle, Nantes",
|
||||
period: "2021–2023",
|
||||
desc: "Systèmes électroniques & informatiques, informatique industrielle & réseaux.",
|
||||
},
|
||||
{
|
||||
title: "BAC PRO Systèmes Numériques",
|
||||
school: "Saint Félix La Salle — Nantes",
|
||||
period: "2018 — 2021",
|
||||
school: "Saint Félix La Salle, Nantes",
|
||||
period: "2018–2021",
|
||||
desc: "Systèmes électroniques & réseaux, installation & maintenance.",
|
||||
},
|
||||
{
|
||||
@@ -278,20 +278,16 @@ export const formation: Formation[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const stackMain: StackGroup[] = [
|
||||
{ label: "Langages", items: ["TypeScript", "JavaScript", "Java", "Python", "PHP"] },
|
||||
{ label: "Frameworks", items: ["NestJS", "Nuxt", "Next.js", "Vue.js", "Angular", "Tailwind CSS", "Astro"] },
|
||||
{ label: "Bases de données", items: ["MariaDB", "MySQL", "PostgreSQL", "Redis"] },
|
||||
{ label: "Outils & DevOps", items: ["Docker", "Git", "AWS", "Terraform", "NGINX", "Plesk"] },
|
||||
{ label: "IDE & Éditeurs", items: ["WebStorm", "PhpStorm", "PyCharm", "Claude Code"] },
|
||||
{ label: "Systèmes", items: ["Debian", "Ubuntu", "macOS"] },
|
||||
];
|
||||
|
||||
export const stackSecondary: StackGroup[] = [
|
||||
{ label: "Langages", items: ["C#", "C++", "Dart", "Kotlin"] },
|
||||
{ label: "Frameworks & libs", items: ["Spring", "Symfony", "Express.js", "FastAPI", "Flutter", "GraphQL", "Vuetify", "Bootstrap"] },
|
||||
{ label: "Bases de données", items: ["MongoDB", "SQLite"] },
|
||||
{ label: "Services & outils", items: ["Firebase", "Swagger", "Postman", "Stripe"] },
|
||||
/* Grille unique, groupée par catégorie (technos « cœur » d'abord, puis le reste). */
|
||||
export const stack: StackGroup[] = [
|
||||
{ label: "Langages", items: ["TypeScript", "JavaScript", "Java", "Python", "PHP", "C#", "C++", "Dart", "Kotlin", "HTML5", "CSS3"] },
|
||||
{ label: "Frameworks & libs", items: ["NestJS", "Nuxt", "Next.js", "React", "Vue.js", "Angular", "Astro", "Tailwind CSS", "Spring", "Symfony", "Express.js", "FastAPI", "Flutter", "GraphQL", "Socket.io", "Chart.js", "Discord.js", "Vuetify", "Bootstrap"] },
|
||||
{ label: "Bases de données & ORM", items: ["MariaDB", "MySQL", "PostgreSQL", "Redis", "MongoDB", "SQLite", "TypeORM", "Prisma", "Sequelize"] },
|
||||
{ label: "Outils & DevOps", items: ["Docker", "Git", "Gitea", "GitLab", "GitHub Actions", "AWS", "Terraform", "NGINX", "Apache", "Vite", "pnpm", "Plesk"] },
|
||||
{ label: "Tests & Qualité", items: ["Jest", "Vitest", "Playwright", "ESLint", "Prettier"] },
|
||||
{ label: "Services & outils", items: ["Firebase", "Swagger", "Postman", "Stripe", "HubSpot"] },
|
||||
{ label: "IDE & Éditeurs", items: ["WebStorm", "PhpStorm", "PyCharm", "IntelliJ IDEA", "VS Code", "Claude Code"] },
|
||||
{ label: "Systèmes", items: ["Debian", "Ubuntu", "Windows", "macOS"] },
|
||||
];
|
||||
|
||||
export const softTech = [
|
||||
@@ -330,6 +326,5 @@ $ cat about.json
|
||||
"name": "Johan Leroy",
|
||||
"role": "Développeur fullstack",
|
||||
"stack": ["TypeScript", "NestJS", "Nuxt", "Next.js"],
|
||||
"location": "Nantes, France",
|
||||
"status": "ouvert aux opportunités"
|
||||
"location": "Nantes, France"
|
||||
}`;
|
||||
|
||||
@@ -7,7 +7,7 @@ interface Props {
|
||||
description?: string;
|
||||
}
|
||||
const {
|
||||
title = "Johan Leroy — Développeur fullstack",
|
||||
title = "Johan Leroy, Développeur fullstack",
|
||||
description = "Johan Leroy, développeur fullstack basé à Nantes. TypeScript, NestJS, Nuxt/Vue, Next.js. Conception d'applications web performantes, du front à la mise en production.",
|
||||
} = Astro.props;
|
||||
|
||||
@@ -15,7 +15,7 @@ const siteUrl = Astro.site?.href ?? "https://johanleroy.fr/";
|
||||
const canonical = new URL(Astro.url.pathname, siteUrl).href;
|
||||
const ogImage = new URL("/images/og.png", siteUrl).href;
|
||||
const ogDescription =
|
||||
"Développeur fullstack à Nantes — applications web performantes, propres et scalables.";
|
||||
"Développeur fullstack à Nantes. Applications web performantes, propres et scalables.";
|
||||
|
||||
// Données structurées (schema.org Person) — aide les moteurs à comprendre la page.
|
||||
const jsonLd = {
|
||||
@@ -51,7 +51,7 @@ const jsonLd = {
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content="Johan Leroy — Développeur fullstack" />
|
||||
<meta property="og:image:alt" content="Johan Leroy, Développeur fullstack" />
|
||||
|
||||
{/* Twitter */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
|
||||
@@ -12,69 +12,129 @@ import {
|
||||
siCplusplus, siDart, siKotlin, siSpring, siSymfony, siExpress, siFastapi,
|
||||
siFlutter, siGraphql, siVuetify, siBootstrap, siMongodb, siSqlite, siFirebase,
|
||||
siSwagger, siPostman, siStripe,
|
||||
siHtml5, siCss3, siReact, siSocketdotio, siChartdotjs, siDiscord,
|
||||
siTypeorm, siPrisma, siSequelize, siApache, siVite, siGitea, siGitlab,
|
||||
siGithubactions, siPnpm, siHubspot, siIntellijidea, siJest, siVitest,
|
||||
siEslint, siPrettier,
|
||||
} from "simple-icons";
|
||||
|
||||
export type TechDef = { path: string } | { mono: string };
|
||||
export type TechDef = { path: string; hex?: string; vb?: string; fr?: "evenodd" | "nonzero" } | { mono: string };
|
||||
|
||||
/* Logos absents de simple-icons : SVG monochromes Devicon (MIT), viewBox 128. */
|
||||
const DEVICON_VB = "0 0 128 128";
|
||||
const javaPath = "M47.617 98.12c-19.192 5.362 11.677 16.439 36.115 5.969-4.003-1.556-6.874-3.351-6.874-3.351-10.897 2.06-15.952 2.222-25.844 1.092-8.164-.935-3.397-3.71-3.397-3.71zm33.189-10.46c-14.444 2.779-22.787 2.69-33.354 1.6-8.171-.845-2.822-4.805-2.822-4.805-21.137 7.016 11.767 14.977 41.309 6.336-3.14-1.106-5.133-3.131-5.133-3.131zm11.319-60.575c.001 0-42.731 10.669-22.323 34.187 6.024 6.935-1.58 13.17-1.58 13.17s15.289-7.891 8.269-17.777c-6.559-9.215-11.587-13.793 15.634-29.58zm9.998 81.144s3.529 2.91-3.888 5.159c-14.102 4.272-58.706 5.56-71.095.171-4.45-1.938 3.899-4.625 6.526-5.192 2.739-.593 4.303-.485 4.303-.485-4.952-3.487-32.013 6.85-13.742 9.815 49.821 8.076 90.817-3.637 77.896-9.468zM85 77.896c2.395-1.634 5.703-3.053 5.703-3.053s-9.424 1.685-18.813 2.474c-11.494.964-23.823 1.154-30.012.326-14.652-1.959 8.033-7.348 8.033-7.348s-8.812-.596-19.644 4.644C17.455 81.134 61.958 83.958 85 77.896zm5.609 15.145c-.108.29-.468.616-.468.616 31.273-8.221 19.775-28.979 4.822-23.725-1.312.464-2 1.543-2 1.543s.829-.334 2.678-.72c7.559-1.575 18.389 10.119-5.032 22.286zM64.181 70.069c-4.614-10.429-20.26-19.553.007-35.559C89.459 14.563 76.492 1.587 76.492 1.587c5.23 20.608-18.451 26.833-26.999 39.667-5.821 8.745 2.857 18.142 14.688 28.815zm27.274 51.748c-19.187 3.612-42.854 3.191-56.887.874 0 0 2.874 2.38 17.646 3.331 22.476 1.437 57-.8 57.816-11.436.001 0-1.57 4.032-18.575 7.231z";
|
||||
const csharpPath = "M109 50h-4.8l-1.2 6h-3.8l1.2-6h-4.9l-1.2 6H89v5h4.4l-.9 4H89v5h2.5l-1.2 6h4.8l1.2-6h3.8l-1.2 6h4.9l1.2-6h5v-5h-4.1l.9-4h3.2v-5h-2.2l1.2-6zm-7.9 15h-3.8l.9-4h3.8l-.9 4zm15.4-32.7c-.6-1.1-1.4-2.1-2.3-2.6L66.1 1.9c-.8-.5-1.9-.7-3.1-.7-1.2 0-2.3.3-3.1.7L11.4 29.7c-1.7 1-3.4 3.5-3.4 5.4v55.7c0 1.1.7 2.3 1.4 3.4l.1.1c.5.8 1.3 1.5 2 1.9l48.3 27.9c.8.5 2 .7 3.2.7 1.2 0 2.3-.3 3.1-.7l47.5-27.9c1.7-1 2.4-3.5 2.4-5.4V35.1c0-.8.4-1.8 0-2.6l.5-.2zm-4.2 2.1c0 .3-.3.5-.3.7v55.7c0 .8-.2 1.7-.4 2L64 120.6c-.1.1-.5.2-1.1.2-.6 0-1-.1-1.1-.2L13.6 92.8s-.1-.1-.2-.1l-.6-.6c-.4-.7.2-1.1-.8-1.2V35.2c1-.5.9-1.7 1.4-1.9L61.7 5.4c.1 0 .6-.2 1.2-.2s1 .1 1.1.2l48 27.7.4.9c.1.1-.1.3-.1.4zM63 87.5c-13.5 0-24.5-11-24.5-24.5s11-24.5 24.5-24.5c9.1 0 17.1 5 21.3 12.5l13-7.5c-6.8-11.9-19.6-20-34.3-20-21.8 0-39.5 17.7-39.5 39.5s17.7 39.5 39.5 39.5c14.6 0 27.4-8 34.2-19.8l-12.9-7.6C80.1 82.5 72.1 87.5 63 87.5z";
|
||||
/* VS Code & Windows : absents de simple-icons (marques protégées). VS Code = tracé
|
||||
Devicon « plain » (nécessite fill-rule evenodd pour l'encoche) ; Windows = logo
|
||||
4-carreaux dessiné à la main (net en monochrome). viewBox 128. */
|
||||
const vscodePath = "M90.767 127.126a7.968 7.968 0 0 0 6.35-.244l26.353-12.681a8 8 0 0 0 4.53-7.209V21.009a8 8 0 0 0-4.53-7.21L97.117 1.12a7.97 7.97 0 0 0-9.093 1.548l-50.45 46.026L15.6 32.013a5.328 5.328 0 0 0-6.807.302l-7.048 6.411a5.335 5.335 0 0 0-.006 7.888L20.796 64 1.74 81.387a5.336 5.336 0 0 0 .006 7.887l7.048 6.411a5.327 5.327 0 0 0 6.807.303l21.974-16.68 50.45 46.025a7.96 7.96 0 0 0 2.743 1.793Zm5.252-92.183L57.74 64l38.28 29.058V34.943Z";
|
||||
const windowsPath = "M12 12h50v50H12z M66 12h50v50H66z M12 66h50v50H12z M66 66h50v50H66z";
|
||||
/* Playwright : tracé Devicon « plain » (masque de théâtre). Rendu en nonzero
|
||||
(fill-rule par défaut) — l'evenodd casserait les creux du masque. */
|
||||
const playwrightPath = "m72.086 86.132-.594-.144c-13.125-3.844-15.15-14.311-15.15-14.311l18.182 5.082L84.15 39.77l-.116-.031c-11.807-3.162-19.64-8.692-22.744-11.292-4.4-3.685-6.335-6.246-8.24-2.372-1.682 3.417-3.836 8.977-5.92 16.762-4.516 16.857-7.892 52.429 20.027 59.914l.572.129zm-18.807-30.85s4.4-6.843 11.862-4.722c7.467 2.121 8.045 10.376 8.045 10.376zm-8.517 23.451L31.787 82.41s1.41-8.029 10.968-11.212l-7.347-27.573-.635.193c-9.111 2.457-16.476 1.805-19.55 1.273-4.357-.751-6.636-1.708-6.422 1.606.186 2.923.882 7.454 2.477 13.44 3.45 12.961 14.854 37.937 36.405 32.132l.635-.199-3.555-13.337ZM19.548 60.315l15.316-4.035s-.446 5.892-6.188 7.405c-5.743 1.512-9.128-3.371-9.128-3.371zm89.824-18.979c-3.981.698-13.532 1.567-25.336-1.596-11.807-3.162-19.64-8.692-22.744-11.292-4.4-3.685-6.335-6.246-8.24-2.372-1.684 3.417-3.837 8.977-5.921 16.762-4.516 16.857-7.892 52.429 20.027 59.914 27.912 7.479 42.772-25.017 47.289-41.875 2.084-7.783 2.998-13.676 3.25-17.476.287-4.305-2.67-3.055-8.324-2.064zM53.28 55.282s4.4-6.843 11.862-4.722c7.467 2.121 8.045 10.376 8.045 10.376zm18.215 30.706c-13.125-3.845-15.15-14.311-15.15-14.311l35.259 9.858c0-.002-7.117 8.25-20.109 4.453zm12.466-21.51s4.394-6.838 11.854-4.711c7.46 2.124 8.048 10.379 8.048 10.379zM51.732 83.935v-7.179l-19.945 5.656s1.474-8.563 11.876-11.514c3.155-.894 5.846-.888 8.069-.459V40.995h9.987c-1.087-3.36-2.139-5.947-3.023-7.744-1.461-2.975-2.96-1.003-6.361 1.842-2.396 2.001-8.45 6.271-17.561 8.726-9.111 2.457-16.476 1.805-19.55 1.273-4.357-.752-6.636-1.708-6.422 1.605.186 2.923.882 7.455 2.477 13.44 3.45 12.962 14.854 37.937 36.405 32.132 5.629-1.517 9.603-4.515 12.357-8.336h-8.309Zm-32.185-23.62 15.316-4.035s-.446 5.892-6.188 7.405c-5.743 1.512-9.128-3.371-9.128-3.371z";
|
||||
|
||||
/* Couleur de marque (simple-icons `.hex`, sans `#`) conservée uniquement si sa
|
||||
luminance relative est « moyenne » (lisible en thème clair ET sombre). Les
|
||||
marques quasi-noires/quasi-blanches (Next.js, Express, macOS, AWS, SQLite…)
|
||||
retombent sur `undefined` → l'emerald (--accent) est utilisé au survol. */
|
||||
function brand(hex: string): string | undefined {
|
||||
const h = hex.replace("#", "");
|
||||
const ch = (i: number) => parseInt(h.slice(i, i + 2), 16) / 255;
|
||||
const lin = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
||||
const L = 0.2126 * lin(ch(0)) + 0.7152 * lin(ch(2)) + 0.0722 * lin(ch(4));
|
||||
return L >= 0.12 && L <= 0.85 ? `#${h}` : undefined;
|
||||
}
|
||||
|
||||
export const techIcons: Record<string, TechDef> = {
|
||||
// Langages
|
||||
"TypeScript": { path: siTypescript.path },
|
||||
"JavaScript": { path: siJavascript.path },
|
||||
"Java": { mono: "Ja" }, // absent de simple-icons
|
||||
"Python": { path: siPython.path },
|
||||
"PHP": { path: siPhp.path },
|
||||
"TypeScript": { path: siTypescript.path, hex: brand(siTypescript.hex) },
|
||||
"JavaScript": { path: siJavascript.path, hex: brand(siJavascript.hex) },
|
||||
"Java": { path: javaPath, hex: "#EA2D2E", vb: DEVICON_VB }, // Devicon (absent de simple-icons)
|
||||
"Python": { path: siPython.path, hex: brand(siPython.hex) },
|
||||
"PHP": { path: siPhp.path, hex: brand(siPhp.hex) },
|
||||
"HTML5": { path: siHtml5.path, hex: brand(siHtml5.hex) },
|
||||
"CSS3": { path: siCss3.path, hex: brand(siCss3.hex) },
|
||||
// Frameworks
|
||||
"NestJS": { path: siNestjs.path },
|
||||
"Nuxt": { path: siNuxtdotjs.path },
|
||||
"Next.js": { path: siNextdotjs.path },
|
||||
"Vue.js": { path: siVuedotjs.path },
|
||||
"Angular": { path: siAngular.path },
|
||||
"Tailwind CSS": { path: siTailwindcss.path },
|
||||
"Astro": { path: siAstro.path },
|
||||
// Bases de données
|
||||
"MariaDB": { path: siMariadb.path },
|
||||
"MySQL": { path: siMysql.path },
|
||||
"PostgreSQL": { path: siPostgresql.path },
|
||||
"Redis": { path: siRedis.path },
|
||||
"MongoDB": { path: siMongodb.path },
|
||||
"SQLite": { path: siSqlite.path },
|
||||
"NestJS": { path: siNestjs.path, hex: brand(siNestjs.hex) },
|
||||
"Nuxt": { path: siNuxtdotjs.path, hex: brand(siNuxtdotjs.hex) },
|
||||
"Next.js": { path: siNextdotjs.path, hex: brand(siNextdotjs.hex) },
|
||||
"React": { path: siReact.path, hex: brand(siReact.hex) },
|
||||
"Vue.js": { path: siVuedotjs.path, hex: brand(siVuedotjs.hex) },
|
||||
"Angular": { path: siAngular.path, hex: brand(siAngular.hex) },
|
||||
"Tailwind CSS": { path: siTailwindcss.path, hex: brand(siTailwindcss.hex) },
|
||||
"Astro": { path: siAstro.path, hex: brand(siAstro.hex) },
|
||||
"Socket.io": { path: siSocketdotio.path, hex: brand(siSocketdotio.hex) },
|
||||
"Chart.js": { path: siChartdotjs.path, hex: brand(siChartdotjs.hex) },
|
||||
"Discord.js": { path: siDiscord.path, hex: brand(siDiscord.hex) }, // icône Discord
|
||||
// Bases de données & ORM
|
||||
"MariaDB": { path: siMariadb.path, hex: brand(siMariadb.hex) },
|
||||
"MySQL": { path: siMysql.path, hex: brand(siMysql.hex) },
|
||||
"PostgreSQL": { path: siPostgresql.path, hex: brand(siPostgresql.hex) },
|
||||
"Redis": { path: siRedis.path, hex: brand(siRedis.hex) },
|
||||
"MongoDB": { path: siMongodb.path, hex: brand(siMongodb.hex) },
|
||||
"SQLite": { path: siSqlite.path, hex: brand(siSqlite.hex) },
|
||||
"TypeORM": { path: siTypeorm.path, hex: brand(siTypeorm.hex) },
|
||||
"Prisma": { path: siPrisma.path, hex: brand(siPrisma.hex) },
|
||||
"Sequelize": { path: siSequelize.path, hex: brand(siSequelize.hex) },
|
||||
// Outils & DevOps
|
||||
"Docker": { path: siDocker.path },
|
||||
"Git": { path: siGit.path },
|
||||
"AWS": { path: siAmazonwebservices.path },
|
||||
"Terraform": { path: siTerraform.path },
|
||||
"NGINX": { path: siNginx.path },
|
||||
"Plesk": { path: siPlesk.path },
|
||||
"Docker": { path: siDocker.path, hex: brand(siDocker.hex) },
|
||||
"Git": { path: siGit.path, hex: brand(siGit.hex) },
|
||||
"Gitea": { path: siGitea.path, hex: brand(siGitea.hex) },
|
||||
"GitLab": { path: siGitlab.path, hex: brand(siGitlab.hex) },
|
||||
"GitHub Actions": { path: siGithubactions.path, hex: brand(siGithubactions.hex) },
|
||||
"AWS": { path: siAmazonwebservices.path, hex: brand(siAmazonwebservices.hex) },
|
||||
"Terraform": { path: siTerraform.path, hex: brand(siTerraform.hex) },
|
||||
"NGINX": { path: siNginx.path, hex: brand(siNginx.hex) },
|
||||
"Apache": { path: siApache.path, hex: brand(siApache.hex) },
|
||||
"Vite": { path: siVite.path, hex: brand(siVite.hex) },
|
||||
"pnpm": { path: siPnpm.path, hex: brand(siPnpm.hex) },
|
||||
"Plesk": { path: siPlesk.path, hex: brand(siPlesk.hex) },
|
||||
// Tests & Qualité
|
||||
"Jest": { path: siJest.path, hex: brand(siJest.hex) },
|
||||
"Vitest": { path: siVitest.path, hex: brand(siVitest.hex) },
|
||||
"Playwright": { path: playwrightPath, hex: "#2EAD33", vb: DEVICON_VB }, // Devicon plain (absent de simple-icons)
|
||||
"ESLint": { path: siEslint.path, hex: brand(siEslint.hex) },
|
||||
"Prettier": { path: siPrettier.path, hex: brand(siPrettier.hex) },
|
||||
// IDE & Éditeurs
|
||||
"WebStorm": { path: siWebstorm.path },
|
||||
"PhpStorm": { path: siPhpstorm.path },
|
||||
"PyCharm": { path: siPycharm.path },
|
||||
"Claude Code": { path: siClaude.path },
|
||||
"WebStorm": { path: siWebstorm.path, hex: brand(siWebstorm.hex) },
|
||||
"PhpStorm": { path: siPhpstorm.path, hex: brand(siPhpstorm.hex) },
|
||||
"PyCharm": { path: siPycharm.path, hex: brand(siPycharm.hex) },
|
||||
"IntelliJ IDEA": { path: siIntellijidea.path, hex: brand(siIntellijidea.hex) },
|
||||
"VS Code": { path: vscodePath, hex: "#007ACC", vb: DEVICON_VB, fr: "evenodd" }, // Devicon plain (absent de simple-icons)
|
||||
"Claude Code": { path: siClaude.path, hex: brand(siClaude.hex) },
|
||||
// Systèmes
|
||||
"Debian": { path: siDebian.path },
|
||||
"Ubuntu": { path: siUbuntu.path },
|
||||
"macOS": { path: siApple.path },
|
||||
"Debian": { path: siDebian.path, hex: brand(siDebian.hex) },
|
||||
"Ubuntu": { path: siUbuntu.path, hex: brand(siUbuntu.hex) },
|
||||
"Windows": { path: windowsPath, hex: "#0078D4", vb: DEVICON_VB }, // logo 4-carreaux (absent de simple-icons)
|
||||
"macOS": { path: siApple.path, hex: brand(siApple.hex) },
|
||||
// Secondaire — langages
|
||||
"C#": { mono: "C#" }, // absent de simple-icons
|
||||
"C++": { path: siCplusplus.path },
|
||||
"Dart": { path: siDart.path },
|
||||
"Kotlin": { path: siKotlin.path },
|
||||
"C#": { path: csharpPath, hex: "#9B4F96", vb: DEVICON_VB }, // Devicon (absent de simple-icons)
|
||||
"C++": { path: siCplusplus.path, hex: brand(siCplusplus.hex) },
|
||||
"Dart": { path: siDart.path, hex: brand(siDart.hex) },
|
||||
"Kotlin": { path: siKotlin.path, hex: brand(siKotlin.hex) },
|
||||
// Secondaire — frameworks & libs
|
||||
"Spring": { path: siSpring.path },
|
||||
"Symfony": { path: siSymfony.path },
|
||||
"Express.js": { path: siExpress.path },
|
||||
"FastAPI": { path: siFastapi.path },
|
||||
"Flutter": { path: siFlutter.path },
|
||||
"GraphQL": { path: siGraphql.path },
|
||||
"Vuetify": { path: siVuetify.path },
|
||||
"Bootstrap": { path: siBootstrap.path },
|
||||
"Spring": { path: siSpring.path, hex: brand(siSpring.hex) },
|
||||
"Symfony": { path: siSymfony.path, hex: brand(siSymfony.hex) },
|
||||
"Express.js": { path: siExpress.path, hex: brand(siExpress.hex) },
|
||||
"FastAPI": { path: siFastapi.path, hex: brand(siFastapi.hex) },
|
||||
"Flutter": { path: siFlutter.path, hex: brand(siFlutter.hex) },
|
||||
"GraphQL": { path: siGraphql.path, hex: brand(siGraphql.hex) },
|
||||
"Vuetify": { path: siVuetify.path, hex: brand(siVuetify.hex) },
|
||||
"Bootstrap": { path: siBootstrap.path, hex: brand(siBootstrap.hex) },
|
||||
// Secondaire — services & outils
|
||||
"Firebase": { path: siFirebase.path },
|
||||
"Swagger": { path: siSwagger.path },
|
||||
"Postman": { path: siPostman.path },
|
||||
"Stripe": { path: siStripe.path },
|
||||
"Firebase": { path: siFirebase.path, hex: brand(siFirebase.hex) },
|
||||
"Swagger": { path: siSwagger.path, hex: brand(siSwagger.hex) },
|
||||
"Postman": { path: siPostman.path, hex: brand(siPostman.hex) },
|
||||
"Stripe": { path: siStripe.path, hex: brand(siStripe.hex) },
|
||||
"HubSpot": { path: siHubspot.path, hex: brand(siHubspot.hex) },
|
||||
};
|
||||
|
||||
/** Couleur de marque lisible d'une techno (ou `undefined` → fallback emerald). */
|
||||
export function brandColor(name: string): string | undefined {
|
||||
const d = techIcons[name];
|
||||
return d && "hex" in d ? d.hex : undefined;
|
||||
}
|
||||
|
||||
/** Monogramme de repli pour une techno sans icône (2 premières lettres). */
|
||||
export function monogram(name: string): string {
|
||||
const clean = name.replace(/[^A-Za-z0-9]/g, "");
|
||||
|
||||
@@ -194,26 +194,29 @@ html[data-theme="light"] .icon-moon{display:block;}
|
||||
.formation-desc{font-size:12.5px;color:var(--text-dim);line-height:1.5;margin-top:10px;}
|
||||
|
||||
/* ---------- Stack & compétences ---------- */
|
||||
.stack-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(284px,1fr));gap:16px;}
|
||||
.stack-grid.is-secondary{grid-template-columns:repeat(auto-fit,minmax(260px,1fr));}
|
||||
.stack-group{border:1px solid var(--border);border-radius:14px;background:var(--surface-2);padding:18px;}
|
||||
.is-secondary .stack-group{border:1px solid var(--border-soft);background:transparent;}
|
||||
.stack-group-label{font-family:var(--font-mono);font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.06em;}
|
||||
.tiles{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px;}
|
||||
.is-secondary .tiles{gap:7px;margin-top:13px;}
|
||||
.tile{display:flex;align-items:center;gap:9px;padding:7px 11px 7px 9px;border:1px solid var(--border);border-radius:10px;background:var(--bg);color:var(--text-muted);}
|
||||
.is-secondary .tile{gap:8px;padding:6px 10px 6px 8px;border-radius:9px;}
|
||||
.tile .ic{width:18px;height:18px;}
|
||||
.is-secondary .tile .ic{width:15px;height:15px;}
|
||||
/* Masonry en CSS pur : packing vertical (pas de cartes à moitié vides) ;
|
||||
colonnes auto selon la largeur, comme l'ancien auto-fit. */
|
||||
.stack-grid{columns:284px;column-gap:16px;}
|
||||
.stack-group{border:1px solid var(--border);border-radius:14px;background:var(--surface-2);padding:18px;break-inside:avoid;margin-bottom:16px;}
|
||||
.stack-group-label{display:flex;align-items:center;justify-content:center;gap:8px;font-family:var(--font-mono);font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.06em;}
|
||||
.stack-group-count{font-size:10px;color:var(--text-dim);border:1px solid var(--border);border-radius:999px;padding:1px 7px;}
|
||||
.tiles{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:14px;}
|
||||
.tile{display:flex;align-items:center;gap:9px;padding:7px 11px 7px 9px;border:1px solid var(--border);border-radius:10px;background:var(--bg);color:var(--text-muted);cursor:default;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease,background-color .2s ease;}
|
||||
.tile:hover{transform:translateY(-2px);border-color:var(--accent);background:color-mix(in srgb,var(--brand,var(--accent)) 7%,var(--bg));box-shadow:0 8px 20px -10px color-mix(in srgb,var(--brand,var(--accent)) 55%,transparent);}
|
||||
.tile .ic{width:18px;height:18px;transition:color .2s ease;}
|
||||
.tile:hover .ic{color:var(--brand,var(--accent));}
|
||||
.tile-mono{width:24px;height:24px;border-radius:7px;background:var(--accent-solid);color:#fff;display:flex;align-items:center;justify-content:center;flex:none;font-family:var(--font-mono);font-size:11px;font-weight:600;}
|
||||
.is-secondary .tile-mono{width:21px;height:21px;border-radius:6px;font-size:10px;}
|
||||
.tile:hover .tile-mono{background:var(--accent-hover);}
|
||||
.tile-name{font-family:var(--font-mono);font-size:13px;color:var(--text);white-space:nowrap;}
|
||||
.is-secondary .tile-name{font-size:12.5px;color:var(--text-muted);}
|
||||
|
||||
.soft{margin-top:34px;display:flex;flex-direction:column;gap:16px;}
|
||||
.soft{margin-top:40px;display:grid;gap:24px;}
|
||||
@media(min-width:760px){.soft{grid-template-columns:1fr 1fr;gap:32px;}}
|
||||
.soft-label{font-family:var(--font-mono);font-size:11px;color:var(--text-dim);margin-bottom:11px;}
|
||||
.pills{display:flex;flex-wrap:wrap;gap:8px;}
|
||||
.pill{font-size:13px;padding:6px 12px;border:1px solid var(--border);border-radius:999px;color:var(--text-muted);background:var(--surface-2);}
|
||||
.pill{font-size:13px;padding:6px 12px;border:1px solid var(--border);border-radius:999px;color:var(--text-muted);background:var(--surface-2);transition:transform .2s ease,border-color .2s ease,color .2s ease;}
|
||||
.pill::before{content:"";display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--accent);margin-right:7px;vertical-align:middle;opacity:.45;transition:opacity .2s ease;}
|
||||
.pill:hover{border-color:var(--accent);color:var(--text);transform:translateY(-1px);}
|
||||
.pill:hover::before{opacity:1;}
|
||||
|
||||
/* ---------- Centres d'intérêt ---------- */
|
||||
.interests{display:grid;grid-template-columns:repeat(auto-fit,minmax(216px,1fr));gap:14px;}
|
||||
|
||||
Reference in New Issue
Block a user