diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f26bbe9 --- /dev/null +++ b/AGENTS.md @@ -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` | ``, 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 `` 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/.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/`. diff --git a/README.md b/README.md index 3d727c1..58ff687 100644 --- a/README.md +++ b/README.md @@ -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) | diff --git a/public/logos/astronomer.png b/public/logos/astronomer.png new file mode 100644 index 0000000..65cf7cb Binary files /dev/null and b/public/logos/astronomer.png differ diff --git a/public/logos/databricks.png b/public/logos/databricks.png new file mode 100644 index 0000000..b274ac7 Binary files /dev/null and b/public/logos/databricks.png differ diff --git a/public/logos/dbtlabs.png b/public/logos/dbtlabs.png new file mode 100644 index 0000000..550747a Binary files /dev/null and b/public/logos/dbtlabs.png differ diff --git a/public/logos/microsoft.png b/public/logos/microsoft.png new file mode 100644 index 0000000..40a6671 Binary files /dev/null and b/public/logos/microsoft.png differ diff --git a/public/logos/snowflake.png b/public/logos/snowflake.png new file mode 100644 index 0000000..4e77c2a Binary files /dev/null and b/public/logos/snowflake.png differ diff --git a/public/logos/toeic.png b/public/logos/toeic.png new file mode 100644 index 0000000..4b305c9 Binary files /dev/null and b/public/logos/toeic.png differ diff --git a/src/components/sections/Certifications.astro b/src/components/sections/Certifications.astro index b109d73..f19ef10 100644 --- a/src/components/sections/Certifications.astro +++ b/src/components/sections/Certifications.astro @@ -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'; @@ -16,8 +17,10 @@ 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)]; --- { @@ -25,30 +28,26 @@ const aujourdhui = new Date().toISOString().slice(0, 7);
-
    - {certifications.map((c) => ( -
  1. - -
    -
    -

    {c.nom}

    - {dateLisible(c.obtention, langue)} +
      + {certifications.map((c) => { + const expiree = estExpiree(c); + return ( +
    1. +
      + + {c.url && }
      -

      {c.organisme}

      -

      + {c.url ? {c.nom} : c.nom} +

      +

      {c.organisme}

      +

      + {dateLisible(c.obtention, langue)} + {c.expiration && {`${expiree ? t.expireeEn : t.expireEn} ${dateLisible(c.expiration, langue)}`}}

      - {c.identifiant && } - - {c.url && ( -
      - {t.afficherCertification} -
      - )} -
    -
  2. - ))} + + ); + })}
diff --git a/src/content.config.ts b/src/content.config.ts index 0051a09..28ec053 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -55,7 +55,7 @@ const formations = defineCollection({ }), }); -// ── Licences et certifications ──────────────────────────────────────────────────────────────── +// ── Certifications ──────────────────────────────────────────────────────────────────────────── const certifications = defineCollection({ loader: file('src/data/certifications.json'), schema: z.object({ diff --git a/src/data/certifications.json b/src/data/certifications.json index d023597..34be56b 100644 --- a/src/data/certifications.json +++ b/src/data/certifications.json @@ -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": [] } ] diff --git a/src/data/en/certifications.json b/src/data/en/certifications.json new file mode 100644 index 0000000..96c976f --- /dev/null +++ b/src/data/en/certifications.json @@ -0,0 +1,6 @@ +[ + { + "id": "toeic-b2", + "nom": "TOEIC (B2 level)" + } +] diff --git a/src/data/en/site.json b/src/data/en/site.json index 44e11c5..a234c86 100644 --- a/src/data/en/site.json +++ b/src/data/en/site.json @@ -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" } ], diff --git a/src/data/site.json b/src/data/site.json index c69cb0d..4e60c46 100644 --- a/src/data/site.json +++ b/src/data/site.json @@ -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 } ], diff --git a/src/scripts/ui.ts b/src/scripts/ui.ts index 53c927e..881770c 100644 --- a/src/scripts/ui.ts +++ b/src/scripts/ui.ts @@ -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('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(); diff --git a/src/styles/global.css b/src/styles/global.css index 8d9fd8a..a72448f 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -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; } @@ -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%; }