Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# AGENTS.md

Consignes pour les agents de code (et les humains) qui travaillent sur ce dépôt. Le
fonctionnement du site pour son propriétaire est dans [README.md](README.md) ; les thèmes
saisonniers dans [THEMES.md](THEMES.md).

## Le projet

Portfolio personnel statique (Astro 5, TypeScript strict), une seule page qui défile, en
français (`/`, par défaut) et en anglais (`/en/`). Aucun framework client, aucune
dépendance d'exécution : du HTML/CSS généré au build, plus un seul script navigateur
(`src/scripts/ui.ts`). Déployé sur GitHub Pages.

## Commandes

```bash
npm install
npm run dev # http://localhost:4321
npm run build # génère dist/ ET valide toutes les données : c'est le test du projet
npm run preview # sert dist/ (pour vérifier le rendu final)
```

Il n'y a pas de suite de tests : **`npm run build` doit passer** après chaque changement.
Il vérifie les JSON de `src/data/` (zod), les traductions et les types. Pour un changement
visuel, vérifier aussi le rendu dans un navigateur, en clair **et** en sombre, sur
ordinateur **et** à ~390 px de large (pas de défilement horizontal).

## Où sont les choses

| Chemin | Rôle |
|---|---|
| `src/data/*.json` | Tout le contenu et les réglages (profil, expériences, formations, certifications, bénévolat, textes, thèmes). Source de vérité, en français. |
| `src/data/en/*.json` | Traduction anglaise : **uniquement les champs à traduire** (voir « Langues »). |
| `src/content/projets/*.md` | Un projet par fichier (en français seulement). |
| `src/content.config.ts` | Schémas des listes de `src/data/` et des projets. |
| `src/lib/reglages.ts` | Schémas de `site.json`, `profil.json`, `themes.json` ; `reglages(langue)` → `{ site, profil, t }`. |
| `src/lib/i18n.ts` | Langues, `langueDe(Astro.url)`, pose des traductions sur le français. |
| `src/lib/contenu.ts` | `liste(nom, langue)` : une liste de `src/data/` traduite. |
| `src/lib/dates.ts` | Dates « AAAA-MM », périodes et durées, en fr et en en. |
| `src/lib/paths.ts` | `url(chemin, langue)` pour les liens internes, `asset(chemin)` pour `public/`. |
| `src/vues/` | Pages communes aux deux langues (accueil, projet). |
| `src/pages/` | Routes seulement : `/`, `/en/`, `/projets/[slug]/`, `/en/projets/[slug]/`, 404. Elles rendent une vue. |
| `src/components/sections/` | Une section de l'accueil par fichier. |
| `src/layouts/Base.astro` | `<head>`, scripts d'avant-rendu (thème, habillage), haut de page, pied de page. |
| `src/scripts/ui.ts` | Tout le JS navigateur : thème, menu des langues, replis « Voir plus », bulles, liens externes. |
| `src/styles/tokens.css` | Jetons de design (couleurs clair/sombre, verre, rayons). |
| `src/styles/global.css` | Mise en page et composants. |
| `src/styles/habillages/`, `src/components/habillages/` | Thèmes saisonniers (Halloween…). |
| `public/logos/` | Logos d'organisations, carrés (400 × 400 de préférence). |

## Conventions

- **Français partout dans le code** : noms de fichiers, de variables, de fonctions et
commentaires (`langue`, `periode`, `plusRecentsDabord`…). Les classes CSS sont en
anglais court (`tile`, `entry`, `fold`, `cert`). Suivre le style du fichier modifié.
- **Commentaires** : un en-tête court par fichier ou bloc qui dit *pourquoi*, pas de
commentaire ligne à ligne. Garder la densité actuelle.
- **Aucun texte affiché en dur** dans les composants : les libellés viennent de
`site.json → textes` (lus via `reglages(langueDe(Astro.url)).t`), le contenu des JSON de
`src/data/`. Un nouveau libellé = une clé dans `src/data/site.json`, dans
`src/data/en/site.json` **et** dans le schéma de `src/lib/reglages.ts`.
- **Liens internes relatifs** via `url(…, langue)` ; ne jamais écrire une adresse complète
vers le site lui-même. Toute adresse en `https://…` est traitée comme externe et s'ouvre
dans un nouvel onglet (`ui.ts`).
- **Couleurs** : uniquement via les jetons de `tokens.css` (`var(--ink)`, `var(--accent)`…),
avec leur version sombre. L'accent vient de `site.json → couleurs`.
- **Mouvement** : toute animation dans `@media (prefers-reduced-motion: no-preference)`, ou
désactivée quand `still` est vrai dans `ui.ts`.
- **Sans JavaScript, tout reste lisible** : les états repliés/masqués ne s'appliquent que sous
`.js` (classe posée sur `<html>` avant le rendu).
- **Accessibilité** : zones cliquables d'au moins 44 px (2.75rem), `aria-label` traduit sur
les boutons à icône, contrastes AA en clair et en sombre.
- **Pas de nouvelle dépendance** sans demande explicite. Scripts externes interdits (polices
auto-hébergées via `@fontsource`).

## Données et langues

- Ajouter une entrée : un objet avec un `id` unique dans le JSON concerné ; l'ordre
d'affichage se calcule (du plus récent au plus ancien).
- La traduction (`src/data/en/<fichier>.json`) ne contient que les textes : les entrées sont
retrouvées par `id`, les listes sans `id` (postes d'une entreprise, langues du profil) par
position, les listes de textes (compétences) sont remplacées en entier. Un champ absent
reste en français ; un champ ou un `id` inconnu **fait échouer le build** (c'est voulu).
- Les pages de projets ne sont pas traduites (seule l'interface autour l'est).

## Thèmes

Deux axes indépendants, détaillés dans [THEMES.md](THEMES.md) :

- **Apparence** clair/sombre : système par défaut, choix au bouton gardé pour la visite
(`sessionStorage`).
- **Habillage** saisonnier (`src/data/themes.json`) : choisi dans le navigateur selon la date
du visiteur ; chaque habillage a une version jour (clair) et nuit (sombre).
Prévisualisation : `?theme=halloween` ou `?theme=aucun`.

## Git

- Travailler sur `dev`. `main` n'accepte que des PR **depuis `dev`** (CI : Build + Source dev) ;
chaque merge dans `main` déploie sur `gh-pages`. Ne jamais pousser sur `main` ni `gh-pages`.
- Ne pas commiter ni pousser sans demande explicite.
- Ne pas commiter `dist/`, `.astro/` ni `node_modules/`.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Tout ce qui s'affiche vient de ces fichiers. On n'a jamais besoin de toucher au
| `src/data/site.json` | Titre de l'onglet, description, couleur d'accent, **ordre et titres des sections**, tous les libellés (boutons, mentions…) |
| `src/data/experiences.json` | Expérience : une entreprise, un ou plusieurs postes |
| `src/data/formations.json` | Formation |
| `src/data/certifications.json` | Licences et certifications |
| `src/data/certifications.json` | Certifications |
| `src/data/benevolat.json` | Bénévolat |
| `src/content/projets/*.md` | Projets : un fichier par projet (tuile de l'accueil + page de doc) |
| `src/data/en/*.json` | Version anglaise (voir « Langues » plus bas) |
Expand Down
Binary file added public/logos/astronomer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/logos/databricks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/logos/dbtlabs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/logos/microsoft.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/logos/snowflake.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/logos/toeic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 24 additions & 25 deletions src/components/sections/Certifications.astro
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
---
// Licences et certifications : une tuile par certification.
// Certifications : une petite carte presque carrée par certification, en grille. Toute
// la carte mène à la certification en ligne (si `url` est renseignée). Les certifications expirées
// passent après les autres, en retrait (sans verre, logo en noir et blanc) mais toujours lisibles.
import SectionHead from '../SectionHead.astro';
import Repli from '../Repli.astro';
import OrgMark from '../OrgMark.astro';
import Tags from '../Tags.astro';
import Icon from '../Icon.astro';
import { dateLisible, ordre, plusRecentsDabord } from '../../lib/dates';
import { reglages } from '../../lib/reglages';
Expand All @@ -16,39 +17,37 @@ interface Props {
const { titre } = Astro.props;
const langue = langueDe(Astro.url);
const { t } = reglages(langue);
const certifications = plusRecentsDabord(await liste('certifications', langue), (c) => c.obtention);
const aujourdhui = new Date().toISOString().slice(0, 7);
const estExpiree = (c: { expiration?: string }) => !!c.expiration && ordre(c.expiration) < ordre(aujourdhui);
const toutes = plusRecentsDabord(await liste('certifications', langue), (c) => c.obtention);
const certifications = [...toutes.filter((c) => !estExpiree(c)), ...toutes.filter(estExpiree)];
---

{
certifications.length > 0 && (
<section class="section" id="certifications" aria-labelledby="titre-certifications">
<SectionHead id="certifications" titre={titre} nombre={certifications.length} />
<Repli nombre={certifications.length}>
<ol class="tiles">
{certifications.map((c) => (
<li class="tile glass entry reveal">
<OrgMark name={c.organisme} logo={c.logo} />
<div class="entry-body">
<div class="entry-top">
<h3 class="entry-title">{c.nom}</h3>
<span class="entry-when">{dateLisible(c.obtention, langue)}</span>
<ol class="tiles certs">
{certifications.map((c) => {
const expiree = estExpiree(c);
return (
<li class:list={['tile glass cert reveal', { 'is-expired': expiree }]}>
<div class="cert-top">
<OrgMark name={c.organisme} logo={c.logo} />
{c.url && <Icon name="external" class="icon-sm cert-ext" />}
</div>
<p class="entry-org">{c.organisme}</p>
<p class="entry-meta">
{t.delivreeEn} {dateLisible(c.obtention, langue)}
{c.expiration && <> · {ordre(c.expiration) < ordre(aujourdhui) ? t.expireeEn : t.expireEn} {dateLisible(c.expiration, langue)}</>}
<h3 class="cert-title">
{c.url ? <a href={c.url} title={c.identifiant && `${t.identifiant} : ${c.identifiant}`}>{c.nom}</a> : c.nom}
</h3>
<p class="cert-org">{c.organisme}</p>
<p class="cert-when">
{dateLisible(c.obtention, langue)}
{c.expiration && <span class="cert-exp">{`${expiree ? t.expireeEn : t.expireEn} ${dateLisible(c.expiration, langue)}`}</span>}
</p>
{c.identifiant && <p class="entry-meta">{t.identifiant} : {c.identifiant}</p>}
<Tags items={c.competences} />
{c.url && (
<div class="entry-links">
<a class="btn btn-soft" href={c.url}>{t.afficherCertification} <Icon name="external" class="icon-sm" /></a>
</div>
)}
</div>
</li>
))}
</li>
);
})}
</ol>
</Repli>
</section>
Expand Down
2 changes: 1 addition & 1 deletion src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const formations = defineCollection({
}),
});

// ── Licences et certifications ────────────────────────────────────────────────────────────────
// ── Certifications ────────────────────────────────────────────────────────────────────────────
const certifications = defineCollection({
loader: file('src/data/certifications.json'),
schema: z.object({
Expand Down
75 changes: 68 additions & 7 deletions src/data/certifications.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,73 @@
[
{
"id": "dbt",
"nom": "dbt Analytics Engineering Certification",
"id": "dbt-fundamentals",
"nom": "dbt Fundamentals (dbt Studio)",
"organisme": "dbt Labs",
"obtention": "2023-05",
"expiration": "2025-05",
"identifiant": "EXEMPLE-0000",
"url": "https://www.getdbt.com/certifications",
"competences": ["dbt", "SQL"]
"logo": "logos/dbtlabs.png",
"obtention": "2026-04",
"competences": []
},
{
"id": "airflow-3-dag-authoring",
"nom": "Apache Airflow 3 - DAG Authoring",
"organisme": "Astronomer",
"logo": "logos/astronomer.png",
"obtention": "2025-09",
"competences": ["Apache Airflow", "DAGs"],
"url": "https://www.credly.com/badges/73d3aee7-a496-4f1d-98d6-5b51ec4c433a/public_url"
},
{
"id": "airflow-3-fundamentals",
"nom": "Apache Airflow 3 - Fundamentals",
"organisme": "Astronomer",
"logo": "logos/astronomer.png",
"obtention": "2025-07",
"competences": ["Apache Airflow", "DAGs"],
"url": "https://www.credly.com/badges/66f413a4-8000-4b70-b67c-a3f29e3a3cea/public_url"
},
{
"id": "airflow-2-fundamentals",
"nom": "Apache Airflow 2 - Fundamentals",
"organisme": "Astronomer",
"logo": "logos/astronomer.png",
"obtention": "2025-06",
"competences": ["Apache Airflow", "DAGs"],
"url": "https://www.credly.com/badges/e9814259-93b3-40b6-a3f9-c85eb6555ac9/public_url"
},
{
"id": "snowflake-data-warehousing-workshop",
"nom": "Data Warehousing Workshop",
"organisme": "Snowflake",
"logo": "logos/snowflake.png",
"obtention": "2025-01",
"competences": ["Snowflake Databases", "Snowflake Warehouses"],
"url": "https://achieve.snowflake.com/1893f744-49ed-4595-9b7c-055a07c3b641#acc.xtYvbXnO"
},
{
"id": "databricks-lakehouse-fundamentals",
"nom": "Databricks Lakehouse Fundamentals",
"organisme": "Databricks",
"logo": "logos/databricks.png",
"obtention": "2025-01",
"competences": ["Azure Databricks", "Data intelligence"],
"url": "https://credentials.databricks.com/10638375-1cf5-4712-9a3f-2d64f4473825#acc.QjPMTUc7"
},
{
"id": "power-bi-data-analyst-associate",
"nom": "Power BI Data Analyst Associate",
"organisme": "Microsoft",
"logo": "logos/microsoft.png",
"obtention": "2025-01",
"expiration": "2026-01",
"competences": ["Microsoft Power BI", "Data Integration"]
},
{
"id": "toeic-b2",
"nom": "TOEIC niveau B2",
"organisme": "ETS EMEA",
"logo": "logos/toeic.png",
"obtention": "2021-12",
"expiration": "2023-12",
"competences": []
}
]
6 changes: 6 additions & 0 deletions src/data/en/certifications.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"id": "toeic-b2",
"nom": "TOEIC (B2 level)"
}
]
2 changes: 1 addition & 1 deletion src/data/en/site.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{ "id": "experience", "titre": "Experience" },
{ "id": "projets", "titre": "Projects" },
{ "id": "formation", "titre": "Education" },
{ "id": "certifications", "titre": "Licenses & certifications" },
{ "id": "certifications", "titre": "Certifications" },
{ "id": "benevolat", "titre": "Volunteering" }
],

Expand Down
2 changes: 1 addition & 1 deletion src/data/site.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
{ "id": "experience", "titre": "Expérience", "afficher": true },
{ "id": "projets", "titre": "Projets", "afficher": false },
{ "id": "formation", "titre": "Formation", "afficher": true },
{ "id": "certifications", "titre": "Licences et certifications", "afficher": false },
{ "id": "certifications", "titre": "Certifications", "afficher": true },
{ "id": "benevolat", "titre": "Bénévolat", "afficher": true }
],

Expand Down
9 changes: 9 additions & 0 deletions src/scripts/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ document.addEventListener('click', (e) => {
});
document.addEventListener('keydown', (e) => e.key === 'Escape' && langs.forEach((l) => showTip(l, false)));

// ---------- Liens externes : dans un nouvel onglet ----------
// Les liens internes du site sont relatifs (« /projets/… ») : une adresse complète (https://…) mène
// toujours ailleurs, même sur le même domaine (ex. les projets en ligne sous orelink.github.io).

document.querySelectorAll<HTMLAnchorElement>('a[href^="http://"], a[href^="https://"]').forEach((a) => {
a.target = '_blank';
a.relList.add('noopener');
});

// ---------- Durées des postes en cours, au mois près ----------

const now = new Date();
Expand Down
32 changes: 31 additions & 1 deletion src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,9 @@ html:not(.js) .lang:is(:hover, :focus-within) .lang-tip,
font-size: 0.9375rem; font-weight: 700; letter-spacing: -0.02em; color: var(--muted);
background: var(--glass-strong); box-shadow: inset 0 0 0 1px var(--rule);
}
.org-mark img { width: 100%; height: 100%; object-fit: contain; background: var(--surface); }
/* Fond blanc dans les deux thèmes : les logos sont dessinés pour un fond clair, et un logo transparent
(ex. TOEIC, bleu foncé) disparaîtrait sur le fond sombre. */
.org-mark img { width: 100%; height: 100%; object-fit: contain; background: #FFFFFF; }

.entry-body { display: flex; flex-direction: column; gap: 0.125rem; min-width: 0; }
.entry-top { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; column-gap: 1rem; }
Expand Down Expand Up @@ -390,6 +392,34 @@ html:not(.js) .lang:is(:hover, :focus-within) .lang-tip,
border-radius: 1px; background: var(--rule);
}

/* ---------- Certifications : petites cartes en grille ---------- */

.tiles.certs { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 10rem), 1fr)); gap: 0.75rem; }
.cert {
display: flex; flex-direction: column; aspect-ratio: 1; padding: 1rem;
transition: opacity 0.7s var(--ease), transform 0.3s var(--ease), box-shadow 0.35s var(--ease);
}
.cert:has(a):hover { transform: translateY(-2px); box-shadow: var(--glass-shadow-hover), inset 0 1px 0 var(--glass-glint), inset 0 0 0 1px var(--glass-hairline); }
.cert:has(a:focus-visible) { outline: 2px solid var(--accent); outline-offset: 3px; }
.cert-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: auto; padding-bottom: 0.75rem; }
.cert .org-mark { width: 2.5rem; height: 2.5rem; border-radius: 11px; }
.cert-ext { color: var(--faint); }
.cert-title {
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden;
font-size: 0.9375rem; font-weight: 700; line-height: 1.3; letter-spacing: -0.01em;
}
.cert-title a { text-decoration: none; }
.cert-title a:focus-visible { outline: none; }
.cert-title a::after { content: ""; position: absolute; inset: 0; border-radius: inherit; } /* toute la carte est cliquable */
.cert-org { margin-top: 0.25rem; font-size: 0.8125rem; font-weight: 600; color: var(--muted); }
.cert-when { display: flex; flex-direction: column; margin-top: 0.125rem; font-size: 0.8125rem; color: var(--faint); font-variant-numeric: tabular-nums; }
.cert-exp { font-size: 0.75rem; }
/* Expirée : en retrait, sans verre ni ombre, logo en noir et blanc ; les textes restent contrastés. */
.cert.is-expired { background: none; box-shadow: inset 0 0 0 1px var(--rule); -webkit-backdrop-filter: none; backdrop-filter: none; }
.cert.is-expired::before { display: none; }
.cert.is-expired .org-mark { filter: grayscale(1); opacity: 0.6; }
.cert.is-expired .cert-title { font-weight: 600; color: var(--muted); }

/* ---------- Projets ---------- */

.project { display: flex; flex-direction: column; gap: 0.75rem; height: 100%; }
Expand Down
Loading