feat(projets): cartes entièrement cliquables + logos adaptatifs clair/sombre
All checks were successful
Deploy production / build-deploy (push) Successful in 30s
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:
@@ -12,11 +12,20 @@ interface Props { project: Project; }
|
||||
const { project: p } = Astro.props;
|
||||
const st = STATUS[p.status];
|
||||
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">
|
||||
<div class="card-media">
|
||||
<Tag class:list={["card", p.link && "card--link"]} {...linkAttrs}>
|
||||
<div class:list={["card-media", p.imgDark && "card-media--themed"]}>
|
||||
{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>
|
||||
)}
|
||||
@@ -25,9 +34,9 @@ const mono = p.title.replace(/[^A-Za-z0-9]/g, "").slice(0, 2);
|
||||
<div class="card-head">
|
||||
<span class="status"><span class="status-dot" style={`background:${st.color}`}></span>{st.label}</span>
|
||||
{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} />
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<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>)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</Tag>
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface Project {
|
||||
status: ProjectStatus;
|
||||
link?: string;
|
||||
img?: string;
|
||||
/** Variante du logo pour le thème sombre (logo « blanc »). Si absent, `img` est utilisé partout. */
|
||||
imgDark?: string;
|
||||
}
|
||||
|
||||
export interface Experience {
|
||||
@@ -86,6 +88,7 @@ export const projectsFeatured: Project[] = [
|
||||
status: "live",
|
||||
link: "https://techos-asso.fr",
|
||||
img: "/images/projets/techos.png",
|
||||
imgDark: "/images/projets/techos-dark.png",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
stack: ["Vue 3", "Vite", "NestJS 11", "TypeORM", "MariaDB", "Tailwind 4"],
|
||||
status: "live",
|
||||
img: "/images/projets/amarea.png",
|
||||
},
|
||||
{
|
||||
id: "lidge",
|
||||
title: "Lidge",
|
||||
subtitle: "Portail d'abonnements aux services auto-hébergés",
|
||||
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"],
|
||||
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"],
|
||||
status: "oss",
|
||||
link: "https://github.com/JohanLeroy",
|
||||
img: "/images/projets/arboretum.png",
|
||||
imgDark: "/images/projets/arboretum-dark.png",
|
||||
},
|
||||
{
|
||||
id: "ksauce",
|
||||
@@ -126,14 +134,18 @@ export const projectsOther: Project[] = [
|
||||
"Bot Discord pour la communauté KSauce : commandes, modération et fonctionnalités sur-mesure.",
|
||||
stack: ["Node.js", "TypeScript", "Discord.js"],
|
||||
status: "live",
|
||||
link: "https://ksauce.lidge.fr/",
|
||||
img: "/images/projets/ksauce.png",
|
||||
imgDark: "/images/projets/ksauce-dark.png",
|
||||
},
|
||||
{
|
||||
id: "lplv",
|
||||
title: "Liberty Pour La Vie",
|
||||
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"],
|
||||
status: "archived",
|
||||
status: "live",
|
||||
link: "https://libertypourlavie.fr/",
|
||||
img: "/images/projets/lplv.png",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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));}
|
||||
.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-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;}
|
||||
.card-media img{width:100%;height:100%;object-fit:cover;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);}
|
||||
.card-media img{width:100%;height:100%;object-fit:contain;display:block;}
|
||||
/* 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;}
|
||||
.is-other .card-mono span{font-size:38px;}
|
||||
.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;}
|
||||
.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%;}
|
||||
.ext-link{color:var(--text-muted);display:inline-flex;}
|
||||
.ext-link:hover{color:var(--accent);}
|
||||
.ext-link{color:var(--text-muted);display:inline-flex;transition:color .25s ease;}
|
||||
.card--link:hover .ext-link{color:var(--accent);}
|
||||
.card-title{font-size:19px;font-weight:700;letter-spacing:-.02em;color:var(--text);}
|
||||
.is-other .card-title{font-size:17px;}
|
||||
.card-sub{font-family:var(--font-mono);font-size:12px;color:var(--accent);}
|
||||
|
||||
Reference in New Issue
Block a user