feat(projets): cartes entièrement cliquables + logos adaptatifs clair/sombre
All checks were successful
Deploy production / build-deploy (push) Successful in 30s

- ProjectCard : carte = <a> (nouvel onglet) dès qu'un lien est défini,
  focus clavier accessible, hover de l'icône piloté par la carte
- Logos en object-fit:contain sur fond --surface adaptatif (cadrage « logo entier »)
- Nouveau champ Project.imgDark + swap CSS via [data-theme] (logo blanc en sombre)
- Liens : LPLV (live, libertypourlavie.fr), KSauce (ksauce.lidge.fr), Lidge (lidge.fr)
- Lidge : description sans Jellyfin/Mail → Cloud, Vault, Git
- Logos ajoutés : Amarea, Lidge, Arboretum (clair/sombre), KSauce (+ silhouette blanche générée), Techos (paire light/dark)
This commit is contained in:
Johan LEROY
2026-06-22 10:01:36 +02:00
parent 699353910f
commit a30d26aad4
11 changed files with 41 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -12,11 +12,20 @@ interface Props { project: Project; }
const { project: p } = Astro.props; const { project: p } = Astro.props;
const st = STATUS[p.status]; const st = STATUS[p.status];
const mono = p.title.replace(/[^A-Za-z0-9]/g, "").slice(0, 2); 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)` }
: {};
--- ---
<article class="card"> <Tag class:list={["card", p.link && "card--link"]} {...linkAttrs}>
<div class="card-media"> <div class:list={["card-media", p.imgDark && "card-media--themed"]}>
{p.img ? ( {p.img ? (
<img src={p.img} alt={p.title} loading="lazy" /> <>
<img class="logo-light" src={p.img} alt={p.title} loading="lazy" />
{p.imgDark && <img class="logo-dark" src={p.imgDark} alt={p.title} loading="lazy" />}
</>
) : ( ) : (
<div class="card-mono"><span>{mono}</span></div> <div class="card-mono"><span>{mono}</span></div>
)} )}
@@ -25,9 +34,9 @@ const mono = p.title.replace(/[^A-Za-z0-9]/g, "").slice(0, 2);
<div class="card-head"> <div class="card-head">
<span class="status"><span class="status-dot" style={`background:${st.color}`}></span>{st.label}</span> <span class="status"><span class="status-dot" style={`background:${st.color}`}></span>{st.label}</span>
{p.link && ( {p.link && (
<a class="ext-link" href={p.link} target="_blank" rel="noopener" aria-label="Voir le projet (nouvel onglet)"> <span class="ext-link" aria-hidden="true">
<Icon name="external" size={16} /> <Icon name="external" size={16} />
</a> </span>
)} )}
</div> </div>
<h3 class="card-title">{p.title}</h3> <h3 class="card-title">{p.title}</h3>
@@ -37,4 +46,4 @@ const mono = p.title.replace(/[^A-Za-z0-9]/g, "").slice(0, 2);
{p.stack.map((s) => <span class="chip">{s}</span>)} {p.stack.map((s) => <span class="chip">{s}</span>)}
</div> </div>
</div> </div>
</article> </Tag>

View File

@@ -15,6 +15,8 @@ export interface Project {
status: ProjectStatus; status: ProjectStatus;
link?: string; link?: string;
img?: string; img?: string;
/** Variante du logo pour le thème sombre (logo « blanc »). Si absent, `img` est utilisé partout. */
imgDark?: string;
} }
export interface Experience { export interface Experience {
@@ -86,6 +88,7 @@ export const projectsFeatured: Project[] = [
status: "live", status: "live",
link: "https://techos-asso.fr", link: "https://techos-asso.fr",
img: "/images/projets/techos.png", img: "/images/projets/techos.png",
imgDark: "/images/projets/techos-dark.png",
}, },
{ {
id: "amarea", id: "amarea",
@@ -95,15 +98,18 @@ export const projectsFeatured: Project[] = [
"Site vitrine + gestion pour un studio de tatouage. Architecture découplée SPA + API REST avec gestion de médias.", "Site vitrine + gestion pour un studio de tatouage. Architecture découplée SPA + API REST avec gestion de médias.",
stack: ["Vue 3", "Vite", "NestJS 11", "TypeORM", "MariaDB", "Tailwind 4"], stack: ["Vue 3", "Vite", "NestJS 11", "TypeORM", "MariaDB", "Tailwind 4"],
status: "live", status: "live",
img: "/images/projets/amarea.png",
}, },
{ {
id: "lidge", id: "lidge",
title: "Lidge", title: "Lidge",
subtitle: "Portail d'abonnements aux services auto-hébergés", subtitle: "Portail d'abonnements aux services auto-hébergés",
desc: desc:
"Plateforme perso pour gérer les abonnements à mes services auto-hébergés (Jellyfin, Cloud, Vault, Mail, Git) avec SSO, tickets et workflow d'accès.", "Plateforme perso pour gérer les abonnements à mes services auto-hébergés (Cloud, Vault, Git) avec SSO, tickets et workflow d'accès.",
stack: ["Nuxt 3", "NestJS 11", "TypeORM", "MariaDB", "JWT", "Nodemailer"], stack: ["Nuxt 3", "NestJS 11", "TypeORM", "MariaDB", "JWT", "Nodemailer"],
status: "live", status: "live",
link: "https://lidge.fr",
img: "/images/projets/lidge.png",
}, },
]; ];
@@ -117,6 +123,8 @@ export const projectsOther: Project[] = [
stack: ["Node.js", "Vue 3", "TypeScript", "npx"], stack: ["Node.js", "Vue 3", "TypeScript", "npx"],
status: "oss", status: "oss",
link: "https://github.com/JohanLeroy", link: "https://github.com/JohanLeroy",
img: "/images/projets/arboretum.png",
imgDark: "/images/projets/arboretum-dark.png",
}, },
{ {
id: "ksauce", id: "ksauce",
@@ -126,14 +134,18 @@ export const projectsOther: Project[] = [
"Bot Discord pour la communauté KSauce : commandes, modération et fonctionnalités sur-mesure.", "Bot Discord pour la communauté KSauce : commandes, modération et fonctionnalités sur-mesure.",
stack: ["Node.js", "TypeScript", "Discord.js"], stack: ["Node.js", "TypeScript", "Discord.js"],
status: "live", status: "live",
link: "https://ksauce.lidge.fr/",
img: "/images/projets/ksauce.png",
imgDark: "/images/projets/ksauce-dark.png",
}, },
{ {
id: "lplv", id: "lplv",
title: "Liberty Pour La Vie", title: "Liberty Pour La Vie",
subtitle: "Site associatif", subtitle: "Site associatif",
desc: "Site web complet pour l'association Liberty Pour La Vie. Projet désormais fermé.", desc: "Site web complet pour l'association Liberty Pour La Vie, toujours en ligne.",
stack: ["Java", "Spring Boot", "Thymeleaf", "Bootstrap", "MariaDB"], stack: ["Java", "Spring Boot", "Thymeleaf", "Bootstrap", "MariaDB"],
status: "archived", status: "live",
link: "https://libertypourlavie.fr/",
img: "/images/projets/lplv.png", img: "/images/projets/lplv.png",
}, },
{ {

View File

@@ -150,10 +150,16 @@ html[data-theme="light"] .icon-moon{display:block;}
.grid-projects.is-other{grid-template-columns:repeat(auto-fit,minmax(280px,1fr));} .grid-projects.is-other{grid-template-columns:repeat(auto-fit,minmax(280px,1fr));}
.card{display:flex;flex-direction:column;border:1px solid var(--border);border-radius:14px;background:var(--surface-2);overflow:hidden;transition:transform .25s ease,border-color .25s ease,box-shadow .25s ease;} .card{display:flex;flex-direction:column;border:1px solid var(--border);border-radius:14px;background:var(--surface-2);overflow:hidden;transition:transform .25s ease,border-color .25s ease,box-shadow .25s ease;}
.card:hover{transform:translateY(-4px);border-color:var(--accent);box-shadow:var(--shadow);} .card:hover{transform:translateY(-4px);border-color:var(--accent);box-shadow:var(--shadow);}
.card-media{position:relative;height:168px;overflow:hidden;} .card--link{cursor:pointer;text-decoration:none;color:inherit;}
.card--link:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
.card-media{position:relative;height:168px;overflow:hidden;background:var(--surface);padding:16px 20px;}
.is-other .card-media{height:140px;} .is-other .card-media{height:140px;}
.card-media img{width:100%;height:100%;object-fit:cover;display:block;} .card-media img{width:100%;height:100%;object-fit:contain;display:block;}
.card-mono{height:100%;display:flex;align-items:center;justify-content:center;background:radial-gradient(120% 130% at 0% 0%,color-mix(in srgb,var(--accent) 18%,transparent),transparent 58%),var(--surface);} /* Swap du logo selon le thème (:root = sombre → on montre la variante claire/blanche). */
.card-media--themed .logo-light{display:none;}
[data-theme="light"] .card-media--themed .logo-light{display:block;}
[data-theme="light"] .card-media--themed .logo-dark{display:none;}
.card-mono{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:radial-gradient(120% 130% at 0% 0%,color-mix(in srgb,var(--accent) 18%,transparent),transparent 58%),var(--surface);}
.card-mono span{font-family:var(--font-mono);font-size:44px;font-weight:700;color:var(--accent);opacity:.92;} .card-mono span{font-family:var(--font-mono);font-size:44px;font-weight:700;color:var(--accent);opacity:.92;}
.is-other .card-mono span{font-size:38px;} .is-other .card-mono span{font-size:38px;}
.card-body{padding:18px 18px 20px;display:flex;flex-direction:column;gap:11px;flex:1;} .card-body{padding:18px 18px 20px;display:flex;flex-direction:column;gap:11px;flex:1;}
@@ -161,8 +167,8 @@ html[data-theme="light"] .icon-moon{display:block;}
.card-head{display:flex;align-items:center;justify-content:space-between;gap:10px;} .card-head{display:flex;align-items:center;justify-content:space-between;gap:10px;}
.status{display:inline-flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;} .status{display:inline-flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;}
.status-dot{width:7px;height:7px;border-radius:50%;} .status-dot{width:7px;height:7px;border-radius:50%;}
.ext-link{color:var(--text-muted);display:inline-flex;} .ext-link{color:var(--text-muted);display:inline-flex;transition:color .25s ease;}
.ext-link:hover{color:var(--accent);} .card--link:hover .ext-link{color:var(--accent);}
.card-title{font-size:19px;font-weight:700;letter-spacing:-.02em;color:var(--text);} .card-title{font-size:19px;font-weight:700;letter-spacing:-.02em;color:var(--text);}
.is-other .card-title{font-size:17px;} .is-other .card-title{font-size:17px;}
.card-sub{font-family:var(--font-mono);font-size:12px;color:var(--accent);} .card-sub{font-family:var(--font-mono);font-size:12px;color:var(--accent);}