Compare commits

..

11 commits

Author SHA1 Message Date
LIABEUF, Stephane (External)
773cc71613 securisation d'acces à la page 2026-06-24 07:06:24 +02:00
LIABEUF, Stephane (External)
17d89c024d Fix Moul'Anjou Plastique 2026-05-27 10:09:55 +02:00
LIABEUF, Stephane (External)
474ec6b711 Update Moul'Anjou Plastique 2026-05-27 10:08:03 +02:00
LIABEUF, Stephane (External)
7624374130 Update Moul'Anjou Plastique et Rôle -> Objectifs 2026-05-27 10:06:20 +02:00
LIABEUF, Stephane (External)
93c5b96e5e Update Moul'Anjou Plastique 2026-05-27 10:02:08 +02:00
LIABEUF, Stephane (External)
c12bdef912 Update Moul'Anjou Plastique 2026-05-27 09:54:22 +02:00
LIABEUF, Stephane (External)
c7b8ddc1e8 update Moul'Anjou Plastique 2026-05-27 09:41:31 +02:00
LIABEUF, Stephane (External)
cb9903122a Update Doc , Readme and preview 2026-05-27 09:02:35 +02:00
LIABEUF, Stephane (External)
b10be86428 chore: update years of experience from 25+ to 26+
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:27:34 +02:00
LIABEUF, Stephane (External)
58396c6bea fix: move service card checkmarks to top-right corner badge position
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 09:49:52 +02:00
LIABEUF, Stephane (External)
b06dfedca2 fix: anchor green checkmarks inside service cards with absolute positioning
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 09:45:46 +02:00
8 changed files with 724 additions and 203 deletions

View file

@ -29,14 +29,17 @@ Aucun framework, aucun bundler, aucune dépendance serveur — le site est du **
```
cv-web-liabeuf/
├── index.html ← Moteur de rendu unique (HTML + CSS + JS)
├── cv-data.json ← Version maître (Directeur Conseil Service)
├── cv-data-ux.json ← Version UX & Digital Manager
├── cv-data-dsi.json ← Version Directeur SI & Transformation Digitale
├── infrastructure.html ← Page dédiée à la stack technique du projet
├── favicon.svg ← Favicon SVG initiales SL + dégradé Cornerstone CGI
├── DOCUMENTATION.md ← Ce fichier
└── .gitignore ← Exclut .claude/ (dossier de travail Claude Code)
├── index.html ← Moteur de rendu unique (HTML + CSS + JS)
├── cv-data.json ← Version maître (Directeur Conseil Service)
├── cv-data-ux.json ← Version UX & Digital Manager
├── cv-data-dsi.json ← Version Directeur SI & Transformation Digitale
├── infrastructure.html ← Page dédiée à la stack technique du projet (GitOps)
├── ecosystem.html ← Page dashboard présentant l'écosystème Coolify auto-hébergé
├── favicon.svg ← Favicon SVG initiales SL + dégradé Cornerstone CGI
├── ecosystem-favicon.svg ← Favicon dédié pour le dashboard écosystème
├── DOCUMENTATION.md ← Ce fichier technique
├── README.md ← Guide d'accueil principal du dépôt
└── .gitignore ← Exclut .claude/ (dossier de travail Claude Code)
```
---
@ -51,7 +54,7 @@ Le paramètre `?cv=` dans l'URL permet de charger une version différente du CV
| `cv.stephaneliabeuf.fr?cv=ux` | `cv-data-ux.json` | Recruteurs UX / Digital |
| `cv.stephaneliabeuf.fr?cv=dsi` | `cv-data-dsi.json` | DSI / Directions SI |
Le paramètre `?cv=` est **transparent** : il n'est pas affiché dans l'interface, seule l'URL change. Il est automatiquement propagé vers la page `infrastructure.html` et conservé lors du retour au CV.
Le paramètre `?cv=` est **transparent** : il n'est pas affiché dans l'interface, seule l'URL change. Il est automatiquement propagé vers les pages `infrastructure.html` et `ecosystem.html` et conservé lors du retour au CV.
---
@ -169,10 +172,10 @@ Le **dégradé Cornerstone CGI** (`#E31937 → #A82465 → #5236AB`) est appliqu
- **Routing JSON par URL**`?cv=ux``cv-data-ux.json`, `?cv=dsi``cv-data-dsi.json`, défaut → `cv-data.json`
- **Propagation du paramètre** — le lien "Voir mon infrastructure" et le bouton "Retour au CV" conservent le `?cv=` actif
### Page Infrastructure
- Page `infrastructure.html` décrivant la stack complète : Edition → Forgejo → Coolify → Nginx → HTTPS
- Lien retour dynamique vers `index.html?cv={param}` via `URLSearchParams`
- Favicon et bouton retour intégrés
### Pages Annexes Dynamiques
- **Page Infrastructure** (`infrastructure.html`) : Décrit le pipeline GitOps complet (Edition → Forgejo → Coolify → Nginx → HTTPS) avec graphismes de monitoring de flux.
- **Page Écosystème** (`ecosystem.html`) : Tableau de bord de surveillance de l'écosystème Coolify avec la synthèse des 14 ressources actives (2 applications principales, 12 services de collaboration, DevOps, base de données et productivité).
- **Routage Dynamique** : Les boutons de retour de ces pages et les boutons de navigation vers celles-ci propagent dynamiquement le paramètre `?cv={param}` pour préserver le contexte de version choisi par le visiteur.
---

169
README.md Normal file
View file

@ -0,0 +1,169 @@
# 💼 CV Web Interactif — Stéphane Liabeuf
> Un CV en ligne professionnel, multi-versions et multi-thèmes, **100 % auto-hébergé** et déployé automatiquement via un workflow GitOps moderne (**Forgejo + Coolify + Nginx**).
---
## 🌟 Points Forts du Projet
- **Performance & Sobriété** : Rendu instantané développé en **HTML5 / CSS3 / JavaScript Vanilla**. Aucun framework lourd, aucun outil de build, aucune dépendance externe au runtime.
- **Multi-Versions Dynamique** : Moteur de rendu unique piloté par les paramètres d'URL (`?cv=ux` ou `?cv=dsi`) chargeant des structures JSON distinctes.
- **Multi-Thèmes Persistant** : 4 ambiances visuelles complètes (dont Cyberpunk, CGI ou Professionnel IT) configurables via un menu interactif et persistées dans le `localStorage`.
- **GitOps & Souveraineté** : Hébergement intégral sur serveur privé, automatisation complète des déploiements via Webhook Coolify lors de chaque commit poussé sur Forgejo.
- **Dashboard Écosystème** : Intégration d'un tableau de bord de surveillance de l'écosystème d'outils et de services auto-hébergés.
- **Optimisation SEO & Print** : Métadonnées OpenGraph dynamiques, balisage Schema.org `Person` pour les moteurs de recherche, et feuille de style d'impression optimisée (`window.print()`).
---
## 📂 Architecture des Fichiers
Voici la structure complète et simplifiée des fichiers du dépôt :
```text
cv-web-liabeuf/
├── index.html # Moteur de rendu principal (UI, thèmes, logique JS de routage)
├── infrastructure.html # Page visuelle présentant la stack de déploiement GitOps
├── ecosystem.html # Page dashboard présentant l'écosystème Coolify auto-hébergé
├── cv-data.json # Contenu maître (Version Directeur Conseil Service)
├── cv-data-ux.json # Contenu ciblé (Version UX & Digital Manager)
├── cv-data-dsi.json # Contenu ciblé (Version Directeur SI & Transformation Digitale)
├── favicon.svg # Favicon SVG principal aux initiales SL sur dégradé Cornerstone
├── ecosystem-favicon.svg # Favicon dédié à la page Écosystème
├── DOCUMENTATION.md # Fiche technique d'architecture et guides pratiques
├── README.md # Guide d'accueil du dépôt (ce fichier)
└── .gitignore # Configuration Git excluant les répertoires temporaires (.claude/)
```
---
## 🧭 Routage & Versions du CV
Le fichier unique `index.html` récupère le paramètre d'URL `cv` et charge dynamiquement le fichier JSON correspondant :
| URL | Fichier Source | Profil Cible | Thème par défaut |
| :--- | :--- | :--- | :--- |
| `cv.stephaneliabeuf.fr` | `cv-data.json` | **Directeur Conseil Service** (Général) | Thème Professionnel (`itpro`) |
| `cv.stephaneliabeuf.fr?cv=ux` | `cv-data-ux.json` | **UX & Digital Manager** | Thème Bande Dessinée (`bd`) / Cyberpunk |
| `cv.stephaneliabeuf.fr?cv=dsi` | `cv-data-dsi.json` | **Directeur SI & Transformation Digitale** | Thème Professionnel (`itpro`) |
> [!TIP]
> **Propagation Transparente** : Le paramètre `?cv=` actif est propagé automatiquement vers les pages `infrastructure.html` et `ecosystem.html`, permettant à l'utilisateur de retourner au CV d'origine sans perdre sa sélection de profil.
---
## ⚙️ Thèmes Visuels Disponibles
Le menu de navigation latéral (icône menu burger `≡`) permet d'appliquer instantanément les styles suivants :
1. **⚡ Cyberpunk** (`cyberpunk`) : Tons sombres profonds, néons électriques cyan/magenta, police technique *Orbitron* & *Share Tech Mono*.
2. **💥 Bande Dessinée** (`bd`) : Effet papier vintage, bulles de dialogue, contours noirs épais et ombres portées, police *Comic Neue*.
3. **💼 IT Professionnel** (`itpro`) : Style épuré, bleu corporate de haute qualité, lisibilité optimale avec la police *Inter*.
4. **◆ CGI Branding** (`cgi`) : Reprise de la charte officielle de **CGI** avec son dégradé iconique *Cornerstone* (`#E31937` → `#A82465``#5236AB`).
---
## 📊 Tableau de Bord de l'Écosystème (`ecosystem.html`)
Cette page propose une vue en temps réel de la stack applicative et des services utilitaires auto-hébergés par Stéphane :
```mermaid
graph TD
Coolify[Orchestration Coolify] --> WebApps[Applications Web]
Coolify --> DevOps[Outils DevOps & Code]
Coolify --> Collab[Collaboration & Design]
Coolify --> Prod[Productivité & Métriques]
subgraph WebApps [Applications]
CV[CV Interactif - cv.stephaneliabeuf.fr]
Main[Site Principal - stephaneliabeuf.fr]
end
subgraph DevOps [DevOps & Code]
Forgejo[Forgejo + PostgreSQL]
Homebox[Homebox]
N8N[n8n + Postgres + Worker]
Noco[NocoDB]
end
subgraph Collab [Collaboration & Design]
Affine[AFFiNE]
Excali[Excalidraw]
Ghost[Ghost]
end
subgraph Prod [Productivité & Métriques]
ITTools[IT-Tools]
Mailpit[Mailpit]
Umami[Umami Analytics]
Archiver[Open-Archiver]
Penpot[Penpot]
end
style Coolify fill:#00d8ff,stroke:#03070c,stroke-width:2px,color:#03070c
style CV fill:#20ef67,stroke:#fff,color:#000
style Main fill:#20ef67,stroke:#fff,color:#000
```
---
## 🏗️ Architecture du Pipeline GitOps
Chaque commit validé en local est poussé vers Forgejo, qui notifie Coolify par webhook afin de mettre à jour le conteneur Nginx en production :
```mermaid
sequenceDiagram
autonumber
actor Dev as Stéphane (Local)
participant FG as Forgejo (Serveur Privé)
participant CF as Coolify (Orchestrateur)
participant NX as Conteneur Nginx (Prod)
actor Rec as Recruteur (Client Web)
Dev->>FG: git push origin main
Note over FG: Dépôt privé sécurisé
FG-->>CF: Notification Webhook (Push Event)
Note over CF: Lancement automatique du Build
CF->>NX: Déploiement à chaud (Rolling Update)
Note over NX: Fichiers HTML/CSS/JS statiques
Rec->>NX: Requête HTTP (https://cv.stephaneliabeuf.fr)
NX-->>Rec: Rendu instantané du CV en HTML Vanilla
```
---
## 🛠️ Maintenance & Guides Pratiques
### 1. Ajouter une nouvelle version de CV
1. Dupliquez un fichier existant (ex: `cv-data.json`) sous le nom `cv-data-{nouveau}.json`.
2. Modifiez le contenu textuel et les métadonnées SEO dans ce nouveau fichier JSON.
3. Dans `index.html`, déclarez le nouveau profil dans le tableau de validation :
```javascript
const cvAllowed = ['ux', 'dsi', '{nouveau}'];
```
4. Commitez et poussez la modification. La version sera accessible à l'adresse : `https://cv.stephaneliabeuf.fr?cv={nouveau}`.
### 2. Ajouter un nouveau thème visuel
1. Ajoutez un sélecteur d'attribut CSS dans `index.html` :
```css
[data-theme="nom-du-theme"] {
--primary: #votre-couleur;
/* Définition des variables du thème */
}
```
2. Ajoutez un bouton d'activation dans le menu burger de `index.html` :
```html
<button class="theme-btn btn-nom-theme" onclick="setTheme('nom-du-theme')">🎨 MON THÈME</button>
```
3. Déclarez le nouveau thème dans la liste de validation JavaScript de la fonction `setTheme()`.
---
## 🛠️ Fiche Technique des Technologies
- **Frontend** : HTML5, CSS3 (variables natives, Grid, Flexbox, animations), JavaScript Vanilla (ES6+, Intersection Observer, URLSearchParams).
- **Analytics** : Script respectueux de la vie privée auto-hébergé via **Umami**.
- **Serveur Web** : Image Nginx légère et sécurisée exécutée sous Docker.
- **Hébergement** : VPS sécurisé, SSL Let's Encrypt géré de manière automatisée par Coolify.
---
*Développé avec soin par Stéphane Liabeuf — 2026*

View file

@ -27,7 +27,7 @@
},
"stats": [
{
"valeur": "25+",
"valeur": "26+",
"label": "années XP SI"
},
{

View file

@ -27,7 +27,7 @@
},
"stats": [
{
"valeur": "25+",
"valeur": "26+",
"label": "années XP"
},
{
@ -53,13 +53,13 @@
],
"experiences": [
{
"groupe": "CGI // Directeur Conseil Service",
"groupe": "Expériences au sein de CGI",
"groupe_style": "primary",
"postes": [
{
"periode": "Mai 2025 → Aujourd'hui",
"entreprise": "Limagrain Europe — Field Seeds Division",
"role": "Product Manager & UX Designer",
"role": "Directeur Conseil Service / Product Manager & UX Designer",
"programme": "Plateforme BI & CRM — Pilotage commercial européen",
"objectif": "Transformation digitale du pôle Field Seeds — écosystème de tableaux de bord unifiés (Power BI + CRM Dynamics) pour piloter les performances commerciales, marketing et terrain à l'échelle européenne.",
"description": "Roadmap produit LFS, conception UX/UI (wireframes, maquettes, guidelines), ateliers Design Thinking avec équipes sales / marketing / CRM / data. KPIs par profil utilisateur (Comex, Country Managers, Sales Managers, commerciaux). Gouvernance Scrum : backlog, rituels Sprint, cadencement trimestriel, coaching des équipes Data et CRM.",
@ -172,7 +172,7 @@
]
},
{
"groupe": "Parcours Autre",
"groupe": "Expériences précédentes",
"groupe_style": "secondary",
"postes": [
{
@ -253,15 +253,21 @@
{
"periode": "1999 → 2001",
"entreprise": "Moul'Anjou Plastique",
"role": "Responsable Projet ERP — Injection Plastique",
"programme": "",
"objectif": "",
"description": "Mise en œuvre ERP en production industrielle. Recueil besoins, cahier des charges, déploiement, formation utilisateurs. Intégration GPAO et systèmes supervision machine.",
"resultats": [],
"role": "Ingenieur Projet / Responsable ERP",
"programme": "Injection Plastique",
"objectif": "Amélioration de la performance de l'entreprise par l'optimisation d'un ERP",
"description": "Implémentation d'un ERP en production industrielle sur 8 mois. Recueil besoins, cahier des charges, déploiement, formation utilisateurs. Intégration GPAO et systèmes supervision machine. Implémentation de la remontée des temps de process (préparation, fabrication, montage...). Mise en place de l'analyse Devis/Commande - Coût de Réalisation",
"resultats": [
"Amélioration générale de la productivité et du pilotage de l'entreprise. Réduction des délais de production.",
"Planning de production permettant une amélioration des délais et l'ajout d'un prévisionel potentiel à partir des devis.",
"Ajout du service de rodage des moules d'injection clients grace au lancement du département de pré-série (2 presses)."
],
"tags": [
"ERP",
"Plasturgie",
"Industrie"
"Industrie",
"Planification",
"Productivité"
]
}
]

View file

@ -403,14 +403,20 @@
.service-card {
min-height: 102px;
display: grid;
grid-template-columns: 74px 1fr 28px;
grid-template-columns: 74px 1fr;
align-items: center;
gap: 0 8px;
padding: 14px 14px;
padding: 14px 36px 14px 14px;
}
.productivity .service-card {
grid-template-columns: 62px 1fr 24px;
grid-template-columns: 62px 1fr;
}
.service-card .green-dot {
position: absolute;
top: -8px;
right: -8px;
}
.service-card h4 {
@ -649,9 +655,9 @@
DevOps & code
</div>
<div class="service-grid">
<article class="service-card"><div class="mini-icon green-icon"><svg width="35" height="35" viewBox="0 0 48 48" fill="none" stroke-width="2.3"><path d="M12 10h24v15c0 9-6 15-12 17-6-2-12-8-12-17z"/><path d="M22 17v12l8 4V13"/></svg></div><h4>Forgejo +<br>PostgreSQL</h4><span class="green-dot"></span></article>
<article class="service-card"><div class="mini-icon green-icon"><svg width="35" height="35" viewBox="0 0 48 48" fill="none" stroke-width="2.3"><path d="M12 10h24v15c0 9-6 15-12 17-6-2-12-8-12-17z"/><path d="M22 17v12l8 4V13"/></svg></div><h4>Forgejo + PostgreSQL</h4><span class="green-dot"></span></article>
<article class="service-card"><div class="mini-icon blue-icon"><svg width="36" height="36" viewBox="0 0 48 48" fill="none" stroke-width="2.4"><path d="M9 24 24 12l15 12v17H9z"/><path d="M19 41V29h10v12"/></svg></div><h4>Homebox</h4><span class="green-dot"></span></article>
<article class="service-card"><div class="mini-icon green-icon"><svg width="36" height="36" viewBox="0 0 48 48" fill="none" stroke-width="2.4"><circle cx="12" cy="24" r="4"/><circle cx="36" cy="12" r="4"/><circle cx="36" cy="36" r="4"/><path d="M16 22 32 14M16 26l16 8"/></svg></div><h4>n8n + Postgres<br>+ Worker</h4><span class="green-dot"></span></article>
<article class="service-card"><div class="mini-icon green-icon"><svg width="36" height="36" viewBox="0 0 48 48" fill="none" stroke-width="2.4"><circle cx="12" cy="24" r="4"/><circle cx="36" cy="12" r="4"/><circle cx="36" cy="36" r="4"/><path d="M16 22 32 14M16 26l16 8"/></svg></div><h4>n8n + Postgres + Worker</h4><span class="green-dot"></span></article>
<article class="service-card"><div class="mini-icon blue-icon"><svg width="36" height="36" viewBox="0 0 48 48" fill="none" stroke-width="2.4"><ellipse cx="24" cy="12" rx="14" ry="6"/><path d="M10 12v12c0 3.3 6.3 6 14 6s14-2.7 14-6V12"/><path d="M10 24v12c0 3.3 6.3 6 14 6s14-2.7 14-6V24"/></svg></div><h4>NocoDB</h4><span class="green-dot"></span></article>
</div>
</div>
@ -696,8 +702,13 @@
<script>
const cv = new URLSearchParams(window.location.search).get('cv');
if (cv) document.getElementById('backBtn').href = `index.html?cv=${cv}`;
if (typeof umami !== 'undefined') umami.track('ecosystem-view');
const validCodes = ['f3b9c7a8d5e4b2d1c0a9b8d7', 'e2d1c0b9a8f7e6d5c4b3a2f1', 'a1b2c3d4e5f6a7b8c9d0e1f2'];
if (!cv || !validCodes.includes(cv)) {
window.location.href = 'index.html';
} else {
document.getElementById('backBtn').href = `index.html?cv=${cv}`;
if (typeof umami !== 'undefined') umami.track('ecosystem-view');
}
</script>
</body>
</html>

View file

@ -27,7 +27,8 @@
<!-- Schema.org JSON-LD injecté dynamiquement -->
<script type="application/ld+json" id="schema-jsonld"></script>
<!-- Analytics Umami -->
<script defer src="https://analytics.stephaneliabeuf.fr/script.js" data-website-id="e2570832-3cf4-4645-8d02-2e9676f123be"></script>
<script defer src="https://analytics.stephaneliabeuf.fr/script.js"
data-website-id="e2570832-3cf4-4645-8d02-2e9676f123be"></script>
<!-- Print CSS -->
<style>
@media print {
@ -359,7 +360,11 @@
border-radius: 4px;
transition: opacity .2s;
}
.burger-btn:hover { opacity: .7; }
.burger-btn:hover {
opacity: .7;
}
.burger-btn span {
display: block;
width: 22px;
@ -379,7 +384,10 @@
padding: 16px;
min-width: 200px;
}
#theme-menu.open { display: flex; }
#theme-menu.open {
display: flex;
}
.theme-btn {
cursor: pointer;
@ -393,8 +401,15 @@
white-space: nowrap;
text-align: left;
}
.theme-btn:hover { transform: scale(1.03); }
.theme-btn.active { outline: 3px solid #fff; outline-offset: 2px; }
.theme-btn:hover {
transform: scale(1.03);
}
.theme-btn.active {
outline: 3px solid #fff;
outline-offset: 2px;
}
.theme-btn.btn-cyberpunk {
background: #00f5ff;
@ -403,6 +418,7 @@
font-size: 10px;
letter-spacing: .12em;
}
.theme-btn.btn-bd {
background: #FFE033;
color: #1a1a1a;
@ -411,6 +427,7 @@
letter-spacing: .08em;
border: 3px solid #1a1a1a;
}
.theme-btn.btn-itpro {
background: #1a56db;
color: #fff;
@ -418,6 +435,7 @@
font-size: 12px;
font-weight: 600;
}
.theme-btn.btn-cgi {
background: linear-gradient(to right, #E31937 0%, #A82465 55%, #5236AB 100%);
color: #fff;
@ -428,24 +446,39 @@
}
/* burger par thème */
[data-theme="cyberpunk"] .burger-btn { top: 24px; right: 64px; }
[data-theme="cyberpunk"] .burger-btn span { background: #00f5ff; }
[data-theme="cyberpunk"] .burger-btn {
top: 24px;
right: 64px;
}
[data-theme="cyberpunk"] .burger-btn span {
background: #00f5ff;
}
[data-theme="cyberpunk"] #theme-menu {
background: #050810;
border: 1px solid rgba(0,245,255,.2);
box-shadow: 0 8px 32px rgba(0,0,0,.6);
border: 1px solid rgba(0, 245, 255, .2);
box-shadow: 0 8px 32px rgba(0, 0, 0, .6);
}
[data-theme="bd"] .burger-btn span { background: #1a1a1a; }
[data-theme="bd"] .burger-btn span {
background: #1a1a1a;
}
[data-theme="bd"] #theme-menu {
background: #FFE033;
border: 3px solid #1a1a1a;
box-shadow: 4px 4px 0 #1a1a1a;
}
[data-theme="itpro"] .burger-btn span { background: #1a56db; }
[data-theme="itpro"] .burger-btn span {
background: #1a56db;
}
[data-theme="itpro"] #theme-menu {
background: #fff;
border: 1px solid #e2e8f0;
box-shadow: 0 8px 24px rgba(0,0,0,.1);
box-shadow: 0 8px 24px rgba(0, 0, 0, .1);
border-radius: 8px;
}
@ -1880,9 +1913,13 @@
}
[data-theme="cgi"] body::before,
[data-theme="cgi"] body::after { display: none }
[data-theme="cgi"] body::after {
display: none
}
[data-theme="cgi"] .cursor-dot { display: none }
[data-theme="cgi"] .cursor-dot {
display: none
}
[data-theme="cgi"] .hero {
background: #200A58;
@ -1893,18 +1930,20 @@
}
[data-theme="cgi"] .hero-bg {
background: radial-gradient(ellipse 55% 70% at 95% 50%, rgba(82,54,171,.25) 0%, transparent 65%),
radial-gradient(ellipse 35% 50% at 5% 80%, rgba(227,25,55,.15) 0%, transparent 60%);
background: radial-gradient(ellipse 55% 70% at 95% 50%, rgba(82, 54, 171, .25) 0%, transparent 65%),
radial-gradient(ellipse 35% 50% at 5% 80%, rgba(227, 25, 55, .15) 0%, transparent 60%);
}
[data-theme="cgi"] .corner-bracket,
[data-theme="cgi"] .hero-scroll { display: none }
[data-theme="cgi"] .hero-scroll {
display: none
}
[data-theme="cgi"] .hero-badge {
font-family: Arial, sans-serif;
font-size: 11px;
font-weight: 400;
color: rgba(255,255,255,.55);
color: rgba(255, 255, 255, .55);
letter-spacing: .12em;
text-transform: uppercase;
background: none;
@ -1935,15 +1974,17 @@
[data-theme="cgi"] .hero-subtitle {
font-family: Arial, sans-serif;
font-size: 14px;
color: rgba(255,255,255,.5);
color: rgba(255, 255, 255, .5);
letter-spacing: .03em;
font-weight: 400;
}
[data-theme="cgi"] .hero-subtitle::before { display: none }
[data-theme="cgi"] .hero-subtitle::before {
display: none
}
[data-theme="cgi"] .stat-box {
background: rgba(255,255,255,.05);
background: rgba(255, 255, 255, .05);
border: none;
border-top: 3px solid transparent;
border-image: linear-gradient(to right, #E31937, #5236AB) 1;
@ -1964,7 +2005,7 @@
[data-theme="cgi"] .stat-label {
font-family: Arial, sans-serif;
color: rgba(255,255,255,.4);
color: rgba(255, 255, 255, .4);
font-size: 10px;
font-weight: 400;
letter-spacing: .1em;
@ -1974,7 +2015,7 @@
background: #fff;
border-bottom: none;
border-image: none;
box-shadow: 0 2px 0 0 #5236AB, 0 3px 8px rgba(0,0,0,.08);
box-shadow: 0 2px 0 0 #5236AB, 0 3px 8px rgba(0, 0, 0, .08);
backdrop-filter: none;
}
@ -1996,13 +2037,29 @@
letter-spacing: .01em;
}
[data-theme="cgi"] nav a::before { display: none }
[data-theme="cgi"] nav a:hover { color: #5236AB }
[data-theme="cgi"] nav a::before {
display: none
}
[data-theme="cgi"] #experience { background: #fff }
[data-theme="cgi"] #competences { background: #EEEEEE }
[data-theme="cgi"] #secteurs { background: #fff }
[data-theme="cgi"] #formation { background: #EEEEEE }
[data-theme="cgi"] nav a:hover {
color: #5236AB
}
[data-theme="cgi"] #experience {
background: #fff
}
[data-theme="cgi"] #competences {
background: #EEEEEE
}
[data-theme="cgi"] #secteurs {
background: #fff
}
[data-theme="cgi"] #formation {
background: #EEEEEE
}
[data-theme="cgi"] footer {
background: #200A58;
@ -2056,7 +2113,7 @@
[data-theme="cgi"] .exp-item:hover {
border-left-color: #5236AB;
background: rgba(82,54,171,.03);
background: rgba(82, 54, 171, .03);
}
[data-theme="cgi"] .exp-period {
@ -2099,7 +2156,10 @@
font-style: italic;
}
[data-theme="cgi"] .exp-objective strong { color: #5236AB; font-style: normal }
[data-theme="cgi"] .exp-objective strong {
color: #5236AB;
font-style: normal
}
[data-theme="cgi"] .exp-desc {
color: #333333;
@ -2110,7 +2170,7 @@
[data-theme="cgi"] .exp-results {
border: none;
border-left: 3px solid #5236AB;
background: rgba(82,54,171,.04);
background: rgba(82, 54, 171, .04);
clip-path: none;
border-radius: 0;
}
@ -2130,15 +2190,18 @@
font-size: 13px;
}
[data-theme="cgi"] .exp-results li::before { content: '▸ '; color: #5236AB }
[data-theme="cgi"] .exp-results li::before {
content: '▸ ';
color: #5236AB
}
[data-theme="cgi"] .tag {
font-family: Arial, sans-serif;
font-size: 11px;
font-weight: 400;
color: #5236AB;
border: 1px solid rgba(82,54,171,.3);
background: rgba(82,54,171,.06);
border: 1px solid rgba(82, 54, 171, .3);
background: rgba(82, 54, 171, .06);
border-radius: 2px;
clip-path: none;
text-transform: none;
@ -2146,14 +2209,14 @@
[data-theme="cgi"] .tag.mag {
color: #A82465;
border-color: rgba(168,36,101,.3);
background: rgba(168,36,101,.06);
border-color: rgba(168, 36, 101, .3);
background: rgba(168, 36, 101, .06);
}
[data-theme="cgi"] .tag.grn {
color: #128354;
border-color: rgba(18,131,84,.3);
background: rgba(18,131,84,.06);
border-color: rgba(18, 131, 84, .3);
background: rgba(18, 131, 84, .06);
}
[data-theme="cgi"] .skill-panel {
@ -2162,10 +2225,12 @@
clip-path: none;
border-radius: 0;
border-top: 3px solid #5236AB;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
box-shadow: 0 1px 4px rgba(0, 0, 0, .06);
}
[data-theme="cgi"] .skill-panel::before { display: none }
[data-theme="cgi"] .skill-panel::before {
display: none
}
[data-theme="cgi"] .skill-cat {
font-family: Arial, sans-serif;
@ -2202,7 +2267,9 @@
border-radius: 0;
}
[data-theme="cgi"] .skill-fill::after { display: none }
[data-theme="cgi"] .skill-fill::after {
display: none
}
[data-theme="cgi"] .sector-cell {
border: 1px solid #CCCCCC;
@ -2215,12 +2282,15 @@
text-transform: none;
}
[data-theme="cgi"] .sector-cell::before { content: ''; display: none }
[data-theme="cgi"] .sector-cell::before {
content: '';
display: none
}
[data-theme="cgi"] .sector-cell:hover {
color: #5236AB;
border-color: #5236AB;
background: rgba(82,54,171,.04);
background: rgba(82, 54, 171, .04);
}
[data-theme="cgi"] .edu-card {
@ -2230,10 +2300,12 @@
border-radius: 0;
border-top: 3px solid transparent;
border-image: linear-gradient(to right, #E31937, #A82465, #5236AB) 1;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
box-shadow: 0 1px 4px rgba(0, 0, 0, .06);
}
[data-theme="cgi"] .edu-card::before { display: none }
[data-theme="cgi"] .edu-card::before {
display: none
}
[data-theme="cgi"] .edu-year {
font-family: Arial, sans-serif;
@ -2271,7 +2343,7 @@
font-size: 11px;
font-weight: 400;
color: #5236AB;
border: 1px solid rgba(82,54,171,.3);
border: 1px solid rgba(82, 54, 171, .3);
background: #fff;
border-radius: 2px;
text-transform: none;
@ -2287,10 +2359,12 @@
[data-theme="cgi"] .footer-info {
font-family: Arial, sans-serif;
color: rgba(255,255,255,.45);
color: rgba(255, 255, 255, .45);
}
[data-theme="cgi"] .footer-status { color: #9E83F5 }
[data-theme="cgi"] .footer-status {
color: #9E83F5
}
[data-theme="cgi"] .contact-bar {
background: #fff;
@ -2300,8 +2374,8 @@
[data-theme="cgi"] .contact-link {
font-family: Arial, sans-serif;
color: #5236AB;
border: 1px solid rgba(82,54,171,.3);
background: rgba(82,54,171,.05);
border: 1px solid rgba(82, 54, 171, .3);
background: rgba(82, 54, 171, .05);
border-radius: 2px;
}
@ -2310,13 +2384,20 @@
color: #fff;
}
[data-theme="cgi"] .burger-btn { top: 20px; right: 20px; }
[data-theme="cgi"] .burger-btn span { background: #5236AB }
[data-theme="cgi"] .burger-btn {
top: 20px;
right: 20px;
}
[data-theme="cgi"] .burger-btn span {
background: #5236AB
}
[data-theme="cgi"] #theme-menu {
background: #fff;
border: 1px solid #CCCCCC;
border-top: 3px solid #5236AB;
box-shadow: 0 8px 24px rgba(0,0,0,.12);
box-shadow: 0 8px 24px rgba(0, 0, 0, .12);
border-radius: 0;
}
@ -2435,18 +2516,21 @@
text-transform: uppercase;
transition: all .25s ease;
}
[data-theme="cyberpunk"] .infra-btn {
font-family: 'Share Tech Mono', monospace;
color: #00f5ff;
border: 1px solid rgba(0,245,255,.35);
background: rgba(0,245,255,.06);
box-shadow: 0 0 20px rgba(0,245,255,.12);
border: 1px solid rgba(0, 245, 255, .35);
background: rgba(0, 245, 255, .06);
box-shadow: 0 0 20px rgba(0, 245, 255, .12);
}
[data-theme="cyberpunk"] .infra-btn:hover {
background: rgba(0,245,255,.14);
background: rgba(0, 245, 255, .14);
border-color: #00f5ff;
transform: translateY(-2px);
}
[data-theme="bd"] .infra-btn {
font-family: 'Comic Neue', cursive;
color: #1a1a1a;
@ -2454,10 +2538,12 @@
border: 3px solid #1a1a1a;
box-shadow: 4px 4px 0 #1a1a1a;
}
[data-theme="bd"] .infra-btn:hover {
transform: translate(-2px,-2px);
transform: translate(-2px, -2px);
box-shadow: 6px 6px 0 #1a1a1a;
}
[data-theme="itpro"] .infra-btn {
font-family: 'Inter', sans-serif;
color: #1a56db;
@ -2465,17 +2551,20 @@
border: 1px solid #bfdbfe;
border-radius: 30px;
}
[data-theme="itpro"] .infra-btn:hover {
background: #1a56db;
color: #fff;
}
[data-theme="cgi"] .infra-btn {
font-family: Arial, sans-serif;
color: #5236AB;
background: #fff;
border: 1px solid rgba(82,54,171,.3);
border: 1px solid rgba(82, 54, 171, .3);
border-left: 4px solid #5236AB;
}
[data-theme="cgi"] .infra-btn:hover {
background: #5236AB;
color: #fff;
@ -2970,17 +3059,29 @@
/* ════ PROGRESS BAR ════ */
#read-progress {
position: fixed;
top: 0; left: 0;
top: 0;
left: 0;
width: 0%;
height: 3px;
z-index: 10010;
transition: width .1s linear;
background: var(--progress-color, #fff);
}
[data-theme="cyberpunk"] #read-progress { background: #00f5ff; box-shadow: 0 0 8px #00f5ff; }
[data-theme="bd"] #read-progress { background: #0055cc; }
[data-theme="itpro"] #read-progress { background: #1a56db; }
[data-theme="cgi"] #read-progress {
[data-theme="cyberpunk"] #read-progress {
background: #00f5ff;
box-shadow: 0 0 8px #00f5ff;
}
[data-theme="bd"] #read-progress {
background: #0055cc;
}
[data-theme="itpro"] #read-progress {
background: #1a56db;
}
[data-theme="cgi"] #read-progress {
background: linear-gradient(to right, #E31937, #A82465, #5236AB);
}
@ -2988,11 +3089,28 @@
nav a.nav-active {
opacity: 1 !important;
}
[data-theme="cyberpunk"] nav a.nav-active { color: #00f5ff !important; }
[data-theme="cyberpunk"] nav a.nav-active::before { opacity: 1 !important; }
[data-theme="bd"] nav a.nav-active { color: #1a1a1a !important; background: #FFE033; }
[data-theme="itpro"] nav a.nav-active { color: #1a56db !important; border-bottom-color: #1a56db !important; }
[data-theme="cgi"] nav a.nav-active { color: #5236AB !important; }
[data-theme="cyberpunk"] nav a.nav-active {
color: #00f5ff !important;
}
[data-theme="cyberpunk"] nav a.nav-active::before {
opacity: 1 !important;
}
[data-theme="bd"] nav a.nav-active {
color: #1a1a1a !important;
background: #FFE033;
}
[data-theme="itpro"] nav a.nav-active {
color: #1a56db !important;
border-bottom-color: #1a56db !important;
}
[data-theme="cgi"] nav a.nav-active {
color: #5236AB !important;
}
/* ════ DOWNLOAD BTN ════ */
.dl-btn {
@ -3008,14 +3126,20 @@
border: none;
transition: all .25s;
}
[data-theme="cyberpunk"] .dl-btn {
font-family: 'Share Tech Mono', monospace;
color: #00f5ff;
background: rgba(0,245,255,.08);
border: 1px solid rgba(0,245,255,.4);
box-shadow: 0 0 14px rgba(0,245,255,.15);
background: rgba(0, 245, 255, .08);
border: 1px solid rgba(0, 245, 255, .4);
box-shadow: 0 0 14px rgba(0, 245, 255, .15);
}
[data-theme="cyberpunk"] .dl-btn:hover { background: rgba(0,245,255,.18); box-shadow: 0 0 24px rgba(0,245,255,.3); }
[data-theme="cyberpunk"] .dl-btn:hover {
background: rgba(0, 245, 255, .18);
box-shadow: 0 0 24px rgba(0, 245, 255, .3);
}
[data-theme="bd"] .dl-btn {
font-family: 'Comic Neue', cursive;
color: #1a1a1a;
@ -3023,21 +3147,33 @@
border: 3px solid #1a1a1a;
box-shadow: 3px 3px 0 #1a1a1a;
}
[data-theme="bd"] .dl-btn:hover { transform: translate(-2px,-2px); box-shadow: 5px 5px 0 #1a1a1a; }
[data-theme="bd"] .dl-btn:hover {
transform: translate(-2px, -2px);
box-shadow: 5px 5px 0 #1a1a1a;
}
[data-theme="itpro"] .dl-btn {
font-family: 'Inter', sans-serif;
color: #fff;
background: #1a56db;
border-radius: 30px;
}
[data-theme="itpro"] .dl-btn:hover { background: #1345b5; }
[data-theme="itpro"] .dl-btn:hover {
background: #1345b5;
}
[data-theme="cgi"] .dl-btn {
font-family: Arial, sans-serif;
color: #fff;
background: #5236AB;
border-left: 4px solid #E31937;
}
[data-theme="cgi"] .dl-btn:hover { background: #200A58; }
[data-theme="cgi"] .dl-btn:hover {
background: #200A58;
}
</style>
</head>
@ -3224,7 +3360,7 @@
<div>
<span class="exp-role">${esc(p.role)}</span>
${p.programme ? `<span class="exp-program">// ${esc(p.programme)}</span>` : ''}
${p.objectif ? `<div class="exp-objective"><strong>RÔLE :</strong> ${esc(p.objectif)}</div>` : ''}
${p.objectif ? `<div class="exp-objective"><strong>Objectifs :</strong> ${esc(p.objectif)}</div>` : ''}
${p.description ? `<span class="exp-desc">${esc(p.description)}</span>` : ''}
${renderResultats(p.resultats)}
${renderTags(p.tags)}
@ -3349,107 +3485,287 @@
// Sélection du fichier JSON selon le paramètre ?cv=
const cvParam = new URLSearchParams(window.location.search).get('cv');
const cvAllowed = ['ux', 'dsi'];
const cvFile = cvAllowed.includes(cvParam) ? `cv-data-${cvParam}.json` : 'cv-data.json';
fetch(cvFile)
.then(r => {
if (!r.ok) throw new Error('Impossible de charger ' + cvFile + ' (code ' + r.status + ')');
return r.json();
})
.then(data => {
render(data);
injectSEO(data);
// Mapping des codes de 24 caractères vers les fichiers de CV correspondants
const cvMap = {
'f3b9c7a8d5e4b2d1c0a9b8d7': 'cv-data.json', // Version Générale (Directeur Conseil Service)
'e2d1c0b9a8f7e6d5c4b3a2f1': 'cv-data-ux.json', // Version UX & Digital Manager
'a1b2c3d4e5f6a7b8c9d0e1f2': 'cv-data-dsi.json' // Version Directeur SI & Transformation
};
// Mettre à jour le lien infra APRÈS render (l'élément existe maintenant dans le DOM)
const infraBtn = document.getElementById('infraBtn');
if (infraBtn && cvParam) infraBtn.href = `infrastructure.html?cv=${cvParam}`;
const ecosystemBtn = document.getElementById('ecosystemBtn');
if (ecosystemBtn && cvParam) ecosystemBtn.href = `ecosystem.html?cv=${cvParam}`;
if (!cvParam || !cvMap[cvParam]) {
// Masquer l'écran de chargement
const ld = document.getElementById('loading');
if (ld) ld.remove();
// Lang dynamique depuis le JSON (défaut : fr)
document.documentElement.lang = data.identity.lang || 'fr';
// Masquer les contrôles globaux non pertinents pour l'écran de verrouillage
const burger = document.getElementById('burgerBtn');
if (burger) burger.style.display = 'none';
const readProgress = document.getElementById('read-progress');
if (readProgress) readProgress.style.display = 'none';
// Tracking version CV
if (typeof umami !== 'undefined') umami.track('cv-view', { version: cvParam || 'master' });
// Restaurer le thème sauvegardé
const saved = localStorage.getItem('cv-theme');
if (saved && ['cyberpunk','bd','itpro','cgi'].includes(saved)) setTheme(saved);
// Scroll reveal — fix: sections already in viewport au chargement
document.querySelectorAll('section').forEach(s => {
const rect = s.getBoundingClientRect();
if (rect.top < window.innerHeight) s.classList.add('visible');
});
// Scroll reveal
const obs = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
if (entry.target.id === 'competences') {
setTimeout(() => {
document.querySelectorAll('.skill-row').forEach(r => {
r.querySelector('.skill-fill').style.width = r.dataset.level + '%';
});
}, 300);
}
}
});
}, { threshold: 0.08 });
document.querySelectorAll('section').forEach(s => obs.observe(s));
// Typing effect hero badge
const badge = document.getElementById('heroBadge');
if (badge) {
const txt = badge.textContent;
badge.textContent = '';
let i = 0;
setTimeout(() => {
const t = setInterval(() => {
badge.textContent = txt.slice(0, i++);
if (i > txt.length) clearInterval(t);
}, 40);
}, 500);
// Injecter le CSS pour la page privée
const style = document.createElement('style');
style.textContent = `
.private-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: #02050a;
color: #f4f7fb;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
padding: 20px;
box-sizing: border-box;
position: fixed;
inset: 0;
z-index: 9999;
}
.private-card {
background: rgba(7, 18, 29, 0.75);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(0, 215, 255, 0.2);
border-radius: 16px;
padding: 40px 30px;
max-width: 440px;
width: 100%;
text-align: center;
box-shadow: 0 0 40px rgba(0, 215, 255, 0.08);
}
.lock-icon {
color: #00d8ff;
margin-bottom: 24px;
animation: pulse-glow 3s infinite ease-in-out;
display: inline-flex;
justify-content: center;
align-items: center;
}
@keyframes pulse-glow {
0%, 100% {
transform: scale(1);
filter: drop-shadow(0 0 8px rgba(0, 216, 255, 0.4));
}
50% {
transform: scale(1.05);
filter: drop-shadow(0 0 20px rgba(0, 216, 255, 0.8));
}
}
.private-card h1 {
font-family: 'Orbitron', 'Arial Black', sans-serif;
font-size: 22px;
letter-spacing: 0.15em;
margin-bottom: 16px;
color: #00d8ff;
text-shadow: 0 0 10px rgba(0, 216, 255, 0.3);
}
.private-card p {
color: #9aa8b8;
font-size: 14px;
line-height: 1.6;
margin-bottom: 30px;
}
.input-group {
position: relative;
margin-bottom: 20px;
}
.private-card input {
width: 100%;
padding: 14px 16px;
background: rgba(2, 5, 10, 0.8);
border: 1px solid rgba(0, 215, 255, 0.15);
border-radius: 8px;
color: #fff;
font-family: 'Share Tech Mono', 'Courier New', monospace;
font-size: 15px;
letter-spacing: 0.05em;
text-align: center;
transition: all 0.3s ease;
box-sizing: border-box;
}
.private-card input:focus {
outline: none;
border-color: #00d8ff;
box-shadow: 0 0 15px rgba(0, 216, 255, 0.25);
}
.private-card button {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #00d8ff 0%, #228cff 100%);
border: none;
border-radius: 8px;
color: #02050a;
font-family: 'Orbitron', sans-serif;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.1em;
cursor: pointer;
transition: all 0.3s ease;
}
.private-card button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(0, 216, 255, 0.4);
}
.private-card button:active {
transform: translateY(0);
}
.error-message {
color: #ff3c74;
font-size: 12px;
margin-top: 15px;
font-family: 'Share Tech Mono', monospace;
min-height: 18px;
}
`;
document.head.appendChild(style);
// ── Progress bar ──
const progressBar = document.getElementById('read-progress');
window.addEventListener('scroll', () => {
const scrolled = window.scrollY;
const total = document.documentElement.scrollHeight - window.innerHeight;
progressBar.style.width = (total > 0 ? (scrolled / total) * 100 : 0) + '%';
}, { passive: true });
// Injecter le HTML de la page privée dans #app
const app = document.getElementById('app');
app.innerHTML = `
<div class="private-container">
<div class="private-card">
<div class="lock-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</div>
<h1>PAGE PRIVÉE</h1>
<p>Ce curriculum vitæ est confidentiel. Veuillez saisir votre code d'accès de 24 caractères pour y accéder.</p>
<form id="accessForm">
<div class="input-group">
<input type="text" id="accessCode" placeholder="Code d'accès (24 caractères)" maxlength="24" autocomplete="off" required>
</div>
<button type="submit">VALIDER</button>
</form>
<div id="errorMessage" class="error-message"></div>
</div>
</div>
`;
// ── Nav active (Intersection Observer) ──
const navObs = new IntersectionObserver(entries => {
entries.forEach(entry => {
const link = document.querySelector(`nav a[data-section="${entry.target.id}"]`);
if (link) link.classList.toggle('nav-active', entry.isIntersecting);
});
}, { threshold: 0.3 });
['experience', 'competences', 'secteurs', 'formation'].forEach(id => {
const el = document.getElementById(id);
if (el) navObs.observe(el);
});
// Se focaliser sur le champ de saisie
const accessCodeInput = document.getElementById('accessCode');
if (accessCodeInput) accessCodeInput.focus();
// Cacher le loading
const ld = document.getElementById('loading');
ld.style.opacity = '0';
setTimeout(() => ld.remove(), 400);
})
.catch(err => {
document.getElementById('loading').innerHTML = `
<div style="font-family:'Orbitron',monospace;color:#ff00aa;font-size:13px;letter-spacing:.1em;text-align:center;padding:40px">
⚠️ ERREUR DE CHARGEMENT<br><br>
<span style="font-family:'Courier New';font-size:11px;color:#7a9ab8">${err.message}</span><br><br>
<span style="font-size:11px;color:#4a6080">
Assurez-vous que <strong style="color:#00f5ff">cv-data.json</strong> est dans le même dossier<br>
et que la page est servie via un serveur web (pas file://).
</span>
</div>`;
// Écouter la soumission du formulaire
document.getElementById('accessForm').addEventListener('submit', (e) => {
e.preventDefault();
const code = accessCodeInput.value.trim();
if (cvMap[code]) {
// Rediriger avec le bon paramètre d'URL
window.location.search = `?cv=${code}`;
} else {
// Afficher le message d'erreur
const errEl = document.getElementById('errorMessage');
if (errEl) {
errEl.textContent = '❌ Code d\'accès invalide';
accessCodeInput.style.borderColor = '#ff3c74';
accessCodeInput.style.boxShadow = '0 0 15px rgba(255, 60, 116, 0.25)';
}
}
});
} else {
const cvFile = cvMap[cvParam];
fetch(cvFile)
.then(r => {
if (!r.ok) throw new Error('Impossible de charger ' + cvFile + ' (code ' + r.status + ')');
return r.json();
})
.then(data => {
render(data);
injectSEO(data);
// Mettre à jour le lien infra APRÈS render (l'élément existe maintenant dans le DOM)
const infraBtn = document.getElementById('infraBtn');
if (infraBtn && cvParam) infraBtn.href = `infrastructure.html?cv=${cvParam}`;
const ecosystemBtn = document.getElementById('ecosystemBtn');
if (ecosystemBtn && cvParam) ecosystemBtn.href = `ecosystem.html?cv=${cvParam}`;
// Lang dynamique depuis le JSON (défaut : fr)
document.documentElement.lang = data.identity.lang || 'fr';
// Tracking version CV
if (typeof umami !== 'undefined') umami.track('cv-view', { version: cvParam || 'master' });
// Restaurer le thème sauvegardé
const saved = localStorage.getItem('cv-theme');
if (saved && ['cyberpunk', 'bd', 'itpro', 'cgi'].includes(saved)) setTheme(saved);
// Scroll reveal — fix: sections already in viewport au chargement
document.querySelectorAll('section').forEach(s => {
const rect = s.getBoundingClientRect();
if (rect.top < window.innerHeight) s.classList.add('visible');
});
// Scroll reveal
const obs = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
if (entry.target.id === 'competences') {
setTimeout(() => {
document.querySelectorAll('.skill-row').forEach(r => {
r.querySelector('.skill-fill').style.width = r.dataset.level + '%';
});
}, 300);
}
}
});
}, { threshold: 0.08 });
document.querySelectorAll('section').forEach(s => obs.observe(s));
// Typing effect hero badge
const badge = document.getElementById('heroBadge');
if (badge) {
const txt = badge.textContent;
badge.textContent = '';
let i = 0;
setTimeout(() => {
const t = setInterval(() => {
badge.textContent = txt.slice(0, i++);
if (i > txt.length) clearInterval(t);
}, 40);
}, 500);
}
// ── Progress bar ──
const progressBar = document.getElementById('read-progress');
window.addEventListener('scroll', () => {
const scrolled = window.scrollY;
const total = document.documentElement.scrollHeight - window.innerHeight;
progressBar.style.width = (total > 0 ? (scrolled / total) * 100 : 0) + '%';
}, { passive: true });
// ── Nav active (Intersection Observer) ──
const navObs = new IntersectionObserver(entries => {
entries.forEach(entry => {
const link = document.querySelector(`nav a[data-section="${entry.target.id}"]`);
if (link) link.classList.toggle('nav-active', entry.isIntersecting);
});
}, { threshold: 0.3 });
['experience', 'competences', 'secteurs', 'formation'].forEach(id => {
const el = document.getElementById(id);
if (el) navObs.observe(el);
});
// Cacher le loading
const ld = document.getElementById('loading');
ld.style.opacity = '0';
setTimeout(() => ld.remove(), 400);
})
.catch(err => {
document.getElementById('loading').innerHTML = `
<div style="font-family:'Orbitron',monospace;color:#ff00aa;font-size:13px;letter-spacing:.1em;text-align:center;padding:40px">
⚠️ ERREUR DE CHARGEMENT<br><br>
<span style="font-family:'Courier New';font-size:11px;color:#7a9ab8">${err.message}</span><br><br>
<span style="font-size:11px;color:#4a6080">
Assurez-vous que <strong style="color:#00f5ff">cv-data.json</strong> est dans le même dossier<br>
et que la page est servie via un serveur web (pas file://).
</span>
</div>`;
});
}
// ══════════════════════════════════════════════
// TÉLÉCHARGEMENT PDF

View file

@ -980,7 +980,12 @@
</main>
<script>
const cv = new URLSearchParams(window.location.search).get('cv');
if (cv) document.getElementById('backBtn').href = `index.html?cv=${cv}`;
const validCodes = ['f3b9c7a8d5e4b2d1c0a9b8d7', 'e2d1c0b9a8f7e6d5c4b3a2f1', 'a1b2c3d4e5f6a7b8c9d0e1f2'];
if (!cv || !validCodes.includes(cv)) {
window.location.href = 'index.html';
} else {
document.getElementById('backBtn').href = `index.html?cv=${cv}`;
}
</script>
</body>

View file

@ -0,0 +1,11 @@
---
name: preview-before-push
description: Toujours montrer un aperçu visuel (screenshot) avant de commiter et pousser
type: feedback
---
Toujours faire un screenshot de prévisualisation et attendre la validation de l'utilisateur avant de lancer le commit & push.
**Why:** L'utilisateur veut valider le rendu visuel avant que les changements partent en production sur Coolify.
**How to apply:** Après chaque modification visible dans le navigateur, prendre un screenshot, le montrer, et demander confirmation avant de commiter/pousser.