Add gitignore, remove CSV and backup files from tracking

This commit is contained in:
2026-03-14 09:34:17 +01:00
parent 9fc4e15d3f
commit 5cc5a93c42
4 changed files with 5 additions and 711854 deletions
+5
View File
@@ -0,0 +1,5 @@
data/prenoms.csv
*~
*.bak
__pycache__/
*.pyc
-711070
View File
File diff suppressed because it is too large Load Diff
-22
View File
@@ -1,22 +0,0 @@
services:
backend:
image: python:3.11-slim
working_dir: /app
volumes:
- ./main.py:/app/main.py:ro
- ./data:/data:ro
command: >
sh -c "pip install --quiet fastapi uvicorn pandas python-multipart &&
uvicorn main:app --host 0.0.0.0 --port 8000"
restart: unless-stopped
frontend:
image: nginx:alpine
ports:
- "8070:80"
volumes:
- ./index.html:/usr/share/nginx/html/index.html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- backend
restart: unless-stopped
-762
View File
@@ -1,762 +0,0 @@
<!DOCTYPE html>
<html lang="fr" data-theme="light">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Prénoms · France</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg:#f7f4ef;--bg2:#eee9e0;--bg3:#e4ddd2;
--ink:#1a1612;--ink2:#5a5048;--ink3:#9a8e82;
--accent:#c0392b;--accent2:#e8b4a0;
--blue:#2c5282;--blue-l:#bee3f8;
--pink:#9b2c5a;--pink-l:#fed7e2;
--gold:#b7791f;--gold-l:#fefcbf;
--purple:#553c9a;--purple-l:#e9d8fd;
--border:rgba(26,22,18,0.1);--card:#ffffff;
--header-bg:#1a1612;--header-fg:#f7f4ef;
--shadow:0 4px 20px rgba(0,0,0,0.07);
}
[data-theme="dark"] {
--bg:#12100e;--bg2:#1c1916;--bg3:#252220;
--ink:#f0ebe3;--ink2:#b0a898;--ink3:#6e6458;
--accent:#e05545;--accent2:#c07060;
--blue:#63b3ed;--blue-l:#1a365d;
--pink:#d69ab8;--pink-l:#521b38;
--gold:#e2a44a;--gold-l:#3d2e0a;
--purple:#b794f4;--purple-l:#2d1f5e;
--border:rgba(240,235,227,0.1);--card:#1c1916;
--header-bg:#0c0b09;--header-fg:#f0ebe3;
--shadow:0 4px 20px rgba(0,0,0,0.3);
}
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--ink);min-height:100vh;transition:background .3s,color .3s;}
header{background:var(--header-bg);color:var(--header-fg);padding:0 20px;display:flex;align-items:center;justify-content:space-between;height:60px;position:sticky;top:0;z-index:200;gap:10px;}
.logo{font-family:'Cormorant Garamond',serif;font-size:1.25rem;font-weight:600;white-space:nowrap;flex-shrink:0;}
.logo span{color:var(--accent2);font-style:italic;}
.header-center{display:flex;background:rgba(255,255,255,0.07);border-radius:100px;padding:3px;gap:2px;}
.mode-btn{padding:6px 14px;border-radius:100px;border:none;background:transparent;color:rgba(255,255,255,0.5);font-family:'Outfit',sans-serif;font-size:0.78rem;font-weight:500;cursor:pointer;transition:all .2s;white-space:nowrap;}
.mode-btn.active{background:var(--accent);color:#fff;}
.theme-toggle{width:34px;height:34px;border-radius:50%;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.07);color:rgba(255,255,255,0.7);cursor:pointer;font-size:0.95rem;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0;}
.theme-toggle:hover{background:rgba(255,255,255,0.15);}
.page{display:none;}
.page.active{display:block;animation:fadeUp .3s ease;}
@keyframes fadeUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
/* ── EXPLORER ── */
#page-explorer{max-width:860px;margin:0 auto;padding:44px 18px 80px;}
.hero-title{font-family:'Cormorant Garamond',serif;font-size:clamp(1.9rem,5vw,3.2rem);font-weight:300;text-align:center;line-height:1.1;margin-bottom:5px;}
.hero-title em{font-style:italic;color:var(--accent);}
.hero-sub{text-align:center;font-size:0.78rem;color:var(--ink3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:26px;}
.search-area{display:flex;flex-direction:column;gap:10px;align-items:center;}
.autocomplete-wrap{position:relative;width:100%;max-width:520px;}
.search-row{display:flex;align-items:center;width:100%;background:var(--card);border:2px solid var(--border);border-radius:100px;transition:border-color .2s,box-shadow .2s;}
.search-row:focus-within{border-color:var(--accent);box-shadow:0 0 0 4px rgba(192,57,43,0.08);}
.search-row.blue-focus:focus-within{border-color:var(--blue);box-shadow:0 0 0 4px rgba(44,82,130,0.1);}
.search-input{flex:1;border:none;outline:none;background:transparent;padding:12px 16px;font-family:'Cormorant Garamond',serif;font-size:1.2rem;color:var(--ink);min-width:0;}
.search-input::placeholder{color:var(--ink3);font-style:italic;}
.search-btn{padding:8px 18px;margin:4px;background:var(--accent);color:#fff;border:none;border-radius:100px;font-family:'Outfit',sans-serif;font-size:0.8rem;font-weight:600;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:opacity .2s;}
.search-btn:hover{opacity:.88;}
.autocomplete-list{position:absolute;top:calc(100% + 6px);left:0;right:0;background:var(--card);border:1px solid var(--border);border-radius:14px;box-shadow:var(--shadow);overflow:hidden;z-index:300;display:none;}
.autocomplete-list.open{display:block;}
.ac-item{display:flex;align-items:center;justify-content:space-between;padding:9px 15px;cursor:pointer;transition:background .12s;gap:8px;}
.ac-item:hover{background:var(--bg2);}
.ac-item:not(:last-child){border-bottom:1px solid var(--border);}
.ac-name{font-family:'Cormorant Garamond',serif;font-size:1.05rem;color:var(--ink);}
.ac-name mark{background:none;color:var(--accent);font-weight:600;}
.ac-meta{font-size:0.7rem;color:var(--ink3);}
.compare-toggle-row{display:flex;align-items:center;gap:8px;font-size:0.78rem;color:var(--ink3);}
.compare-toggle-row label{cursor:pointer;display:flex;align-items:center;gap:6px;}
.compare-second{display:none;width:100%;max-width:520px;}
.compare-second.open{display:block;}
.suggestions{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-top:10px;}
.sug-chip{padding:4px 12px;background:var(--card);border:1px solid var(--border);border-radius:100px;font-size:0.78rem;color:var(--ink2);cursor:pointer;transition:all .15s;}
.sug-chip:hover{border-color:var(--accent);color:var(--accent);}
/* result */
#explorer-result{margin-top:44px;}
.prenom-hero{text-align:center;padding:28px 0 22px;border-bottom:1px solid var(--border);margin-bottom:24px;}
.prenom-big{font-family:'Cormorant Garamond',serif;font-size:clamp(2.6rem,8vw,5rem);font-weight:300;letter-spacing:-.02em;line-height:1;margin-bottom:10px;}
.prenom-badges{display:flex;gap:6px;justify-content:center;flex-wrap:wrap;}
.badge{padding:3px 10px;border-radius:100px;font-size:0.7rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;}
.badge-m{background:var(--blue-l);color:var(--blue);}
.badge-f{background:var(--pink-l);color:var(--pink);}
.badge-mix{background:var(--purple-l);color:var(--purple);}
.badge-len{background:var(--bg2);color:var(--ink2);}
.stat-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:11px;margin-bottom:20px;}
.stat-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px;text-align:center;}
.stat-card .val{font-family:'Cormorant Garamond',serif;font-size:1.8rem;font-weight:600;color:var(--ink);display:block;line-height:1;}
.stat-card .lbl{font-size:0.65rem;color:var(--ink3);text-transform:uppercase;letter-spacing:.06em;margin-top:5px;display:block;}
.peak-row{display:grid;grid-template-columns:1fr 1fr;gap:11px;margin-bottom:20px;}
@media(max-width:500px){.peak-row{grid-template-columns:1fr;}}
.peak-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px 18px;display:flex;align-items:center;gap:12px;}
.peak-icon{font-size:1.4rem;flex-shrink:0;}
.peak-year{font-family:'Cormorant Garamond',serif;font-size:1.6rem;font-weight:600;line-height:1;}
.peak-desc{font-size:0.68rem;color:var(--ink3);text-transform:uppercase;letter-spacing:.05em;margin-top:3px;}
.trend-bar{height:4px;background:var(--bg2);border-radius:2px;margin-top:8px;overflow:hidden;}
.trend-fill{height:100%;border-radius:2px;background:linear-gradient(to right,var(--accent2),var(--accent));transition:width .9s ease;}
.chart-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:22px;margin-bottom:14px;overflow:hidden;}
.chart-card-title{font-family:'Cormorant Garamond',serif;font-size:1.05rem;font-weight:600;color:var(--ink2);margin-bottom:16px;display:flex;align-items:center;justify-content:space-between;gap:8px;}
.chart-wrap{position:relative;height:250px;}
.share-btn{padding:4px 11px;border-radius:100px;border:1px solid var(--border);background:var(--bg2);font-family:'Outfit',sans-serif;font-size:0.72rem;color:var(--ink2);cursor:pointer;transition:all .15s;white-space:nowrap;flex-shrink:0;}
.share-btn:hover{border-color:var(--accent);color:var(--accent);}
.share-btn.copied{background:var(--accent);color:#fff;border-color:var(--accent);}
/* heatmap */
.heatmap-scroll-wrap{position:relative;}
.heatmap-scroll-wrap::after{content:'›››';position:absolute;top:50%;right:0;transform:translateY(-50%);padding:4px 6px;font-size:0.75rem;color:var(--ink3);pointer-events:none;transition:opacity .3s;background:linear-gradient(to right,transparent 0%,var(--card) 40%);}
.heatmap-scroll-wrap.at-end::after{opacity:0;}
.heatmap-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:6px;margin:0 -22px;padding-left:22px;padding-right:22px;}
.heatmap-inner{min-width:520px;}
.heatmap-decade-labels{display:flex;gap:3px;padding-left:52px;margin-bottom:4px;}
.heatmap-decade-lbl{flex:1;font-size:0.58rem;color:var(--ink3);text-align:center;}
.heatmap-row{display:flex;align-items:center;gap:3px;margin-bottom:3px;}
.heatmap-label{font-size:0.62rem;color:var(--ink3);width:48px;text-align:right;flex-shrink:0;padding-right:5px;}
.heatmap-cell{flex:1;height:26px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:0.62rem;font-weight:500;color:#fff;cursor:default;transition:transform .15s;position:relative;}
.heatmap-cell:hover{transform:scale(1.06);z-index:1;}
.heatmap-cell-0{background:var(--bg2);color:var(--ink3);}
/* ── FINDER ── */
#page-finder{max-width:1080px;margin:0 auto;padding:32px 18px 80px;}
.finder-layout{display:grid;grid-template-columns:268px 1fr;gap:22px;align-items:start;}
@media(max-width:720px){.finder-layout{grid-template-columns:1fr;}}
.finder-sidebar{background:var(--card);border:1px solid var(--border);border-radius:16px;overflow:hidden;position:sticky;top:72px;z-index:10;}
@media(max-width:720px){.finder-sidebar{position:static;}}
.sidebar-header{background:var(--ink);padding:14px 18px;cursor:pointer;display:flex;align-items:center;justify-content:space-between;}
.sidebar-toggle-icon{color:rgba(255,255,255,0.6);font-size:0.8rem;transition:transform .25s;flex-shrink:0;}
.finder-sidebar.collapsed .sidebar-toggle-icon{transform:rotate(-90deg);}
.finder-sidebar.collapsed .sidebar-body{display:none;}
.sidebar-title{font-family:'Cormorant Garamond',serif;font-size:0.98rem;color:var(--header-fg);font-weight:400;}
.sidebar-body{padding:16px;}
.filter-group{margin-bottom:18px;}
.filter-label{font-size:0.66rem;font-weight:600;color:var(--ink3);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;display:block;}
.pill-group{display:flex;flex-wrap:wrap;gap:5px;}
.pill{padding:5px 11px;border-radius:100px;border:1.5px solid var(--border);background:transparent;font-family:'Outfit',sans-serif;font-size:0.76rem;color:var(--ink2);cursor:pointer;transition:all .15s;}
.pill:hover{border-color:var(--ink2);}
.pill.active{border-color:var(--accent);background:var(--accent);color:#fff;}
.pill.active-blue{border-color:var(--blue);background:var(--blue);color:#fff;}
.pill.active-pink{border-color:var(--pink);background:var(--pink);color:#fff;}
.pill.active-gold{border-color:var(--gold);background:var(--gold);color:#fff;}
.pill.active-mix{border-color:var(--purple);background:var(--purple);color:#fff;}
input[type=range]{-webkit-appearance:none;width:100%;height:4px;background:var(--bg3);border-radius:2px;outline:none;cursor:pointer;}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;background:var(--accent);border-radius:50%;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.2);}
.range-vals{display:flex;justify-content:space-between;font-size:0.68rem;color:var(--ink3);margin-top:3px;}
.filter-input{width:100%;border:1.5px solid var(--border);border-radius:8px;padding:7px 10px;font-family:'Outfit',sans-serif;font-size:0.83rem;color:var(--ink);background:var(--bg);outline:none;transition:border-color .2s;}
.filter-input:focus{border-color:var(--accent);}
.btn-search-finder{width:100%;padding:11px;background:var(--accent);color:#fff;border:none;border-radius:10px;font-family:'Outfit',sans-serif;font-size:0.9rem;font-weight:600;cursor:pointer;transition:opacity .2s,transform .1s;letter-spacing:.02em;}
.btn-search-finder:hover{opacity:.9;transform:translateY(-1px);}
.hint{font-size:0.66rem;color:var(--ink3);margin-top:5px;line-height:1.4;}
.finder-results-header{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:16px;}
.results-title{font-family:'Cormorant Garamond',serif;font-size:1.45rem;font-weight:300;}
.results-count{font-size:0.76rem;color:var(--ink3);}
.results-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:11px;}
.result-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:15px 16px;cursor:pointer;transition:all .2s;position:relative;overflow:hidden;z-index:1;}
.result-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--accent);opacity:0;transition:opacity .2s;}
.result-card:hover{border-color:rgba(192,57,43,.3);transform:translateY(-2px);box-shadow:var(--shadow);}
.result-card:hover::before{opacity:1;}
.rc-name{font-family:'Cormorant Garamond',serif;font-size:1.35rem;font-weight:400;margin-bottom:6px;line-height:1;}
.rc-meta{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-bottom:7px;}
.rc-total{font-size:0.7rem;color:var(--ink3);}
.rc-mini-bar{height:3px;background:var(--bg2);border-radius:2px;margin-top:6px;overflow:hidden;}
.rc-mini-fill{height:100%;background:linear-gradient(to right,var(--accent2),var(--accent));border-radius:2px;}
.pop-label{font-size:0.63rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:100px;}
.pop-rare{background:#e6fffa;color:#276749;}
.pop-discret{background:var(--blue-l);color:var(--blue);}
.pop-connu{background:var(--gold-l);color:var(--gold);}
.pop-populaire{background:#fff5f5;color:#c53030;}
[data-theme="dark"] .pop-rare{background:#1a3a30;color:#68d391;}
[data-theme="dark"] .pop-discret{background:var(--blue-l);color:var(--blue);}
[data-theme="dark"] .pop-connu{background:var(--gold-l);color:var(--gold);}
[data-theme="dark"] .pop-populaire{background:#3d1010;color:#fc8181;}
.state-placeholder{grid-column:1/-1;text-align:center;padding:60px 20px;color:var(--ink3);}
.state-placeholder .ph-icon{font-size:2.6rem;margin-bottom:10px;opacity:.4;display:block;}
.loading-pulse{animation:pulse 1.2s ease-in-out infinite;display:inline-block;}
.top-table{width:100%;border-collapse:collapse;}
.top-table th{font-size:0.68rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink3);padding:10px 16px;text-align:left;border-bottom:1px solid var(--border);background:var(--bg2);}
.top-table td{padding:11px 16px;border-bottom:1px solid var(--border);font-size:0.88rem;color:var(--ink);}
.top-table tr:last-child td{border-bottom:none;}
.top-table tr:hover td{background:var(--bg);}
.top-table .rang{font-family:'Cormorant Garamond',serif;font-size:1.1rem;color:var(--ink3);width:48px;}
.top-table .prenom-link{font-family:'Cormorant Garamond',serif;font-size:1.15rem;color:var(--accent);cursor:pointer;text-decoration:none;}
.top-table .prenom-link:hover{text-decoration:underline;}
.top-table .bar-cell{width:160px;}
.top-mini-bar{height:5px;background:var(--bg2);border-radius:3px;overflow:hidden;margin-top:4px;}
.top-mini-fill{height:100%;background:linear-gradient(to right,var(--accent2),var(--accent));border-radius:3px;}
@media(max-width:600px){.top-table .bar-cell{display:none;}.top-table th.bar-cell{display:none;}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
</style>
</head>
<body>
<header>
<div class="logo">Prénoms <span>de France</span></div>
<div class="header-center">
<button class="mode-btn active" onclick="switchMode('explorer',this)">🔍 Explorer</button>
<button class="mode-btn" onclick="switchMode('finder',this)">✨ Trouver</button>
<button class="mode-btn" onclick="switchMode('top',this)">🏆 Classement</button>
</div>
<button class="theme-toggle" onclick="toggleTheme()" id="theme-btn">🌙</button>
</header>
<div id="page-explorer" class="page active">
<div id="page-explorer-inner">
<div id="back-to-results" onclick="goBackToFinder()" style="display:none;align-items:center;gap:8px;margin-bottom:18px;cursor:pointer;width:fit-content;padding:7px 14px;border-radius:100px;border:1.5px solid var(--border);background:var(--card);color:var(--ink2);font-size:0.8rem;font-family:'Outfit',sans-serif;transition:all .15s;">
<span>←</span> <span>Retour aux résultats</span>
</div>
<h1 class="hero-title">Explorer un <em>prénom</em></h1>
<p class="hero-sub">Données INSEE · France · depuis 1900</p>
<div class="search-area">
<div class="autocomplete-wrap" id="ac-wrap-1">
<div class="search-row">
<input class="search-input" id="search-input-1" placeholder="Entrez un prénom…"
autocomplete="off" autocorrect="off" spellcheck="false"
oninput="onAcInput(1)" onkeydown="onAcKey(event,1)"/>
<button class="search-btn" onclick="doExplore()">Analyser</button>
</div>
<div class="autocomplete-list" id="ac-list-1"></div>
</div>
<div class="compare-toggle-row">
<label><input type="checkbox" id="compare-chk" onchange="toggleCompare()"/> Comparer avec un second prénom</label>
</div>
<div class="compare-second" id="ac-wrap-2">
<div class="search-row blue-focus">
<input class="search-input" id="search-input-2" placeholder="Second prénom…"
autocomplete="off" autocorrect="off" spellcheck="false"
oninput="onAcInput(2)" onkeydown="onAcKey(event,2)"/>
<button class="search-btn" style="background:var(--blue)" onclick="doExplore()">Comparer</button>
</div>
<div class="autocomplete-list" id="ac-list-2"></div>
</div>
<div class="suggestions">
<span class="sug-chip" onclick="quickSearch('Emma')">Emma</span>
<span class="sug-chip" onclick="quickSearch('Léo')">Léo</span>
<span class="sug-chip" onclick="quickSearch('Marie')">Marie</span>
<span class="sug-chip" onclick="quickSearch('Lucas')">Lucas</span>
<span class="sug-chip" onclick="quickSearch('Jade')">Jade</span>
<span class="sug-chip" onclick="quickSearch('Jean')">Jean</span>
<span class="sug-chip" onclick="quickSearch('Camille')">Camille</span>
</div>
</div>
<div id="explorer-result"></div>
</div>
</div>
<div id="page-finder" class="page">
<div class="finder-layout">
<aside class="finder-sidebar">
<div class="sidebar-header"><div class="sidebar-title">Critères de recherche</div><span class="sidebar-toggle-icon">▼</span></div>
<div class="sidebar-body">
<div class="filter-group">
<span class="filter-label">Genre</span>
<div class="pill-group">
<button class="pill active" data-sexe="" onclick="setSexe(this,'')">Tous</button>
<button class="pill" data-sexe="1" onclick="setSexe(this,'1')">♂ Garçon</button>
<button class="pill" data-sexe="2" onclick="setSexe(this,'2')">♀ Fille</button>
<button class="pill" data-sexe="mixte" onclick="setSexe(this,'mixte')">⚥ Mixte</button>
</div>
</div>
<div class="filter-group">
<span class="filter-label">Popularité</span>
<div class="pill-group">
<button class="pill active-gold" id="pop-rare" onclick="togglePop(this,'rare')">Rare</button>
<button class="pill" id="pop-discret" onclick="togglePop(this,'discret')">Discret</button>
<button class="pill" id="pop-connu" onclick="togglePop(this,'connu')">Connu</button>
<button class="pill" id="pop-populaire" onclick="togglePop(this,'populaire')">Populaire</button>
</div>
<div class="hint">Total toutes années · Rare &lt;2000 · Discret 2k10k · Connu 10k50k · Populaire &gt;50k</div>
</div>
<div class="filter-group">
<span class="filter-label">Longueur — <span id="len-disp">3 à 8 lettres</span></span>
<input type="range" id="len-min-s" min="1" max="15" value="3" oninput="updateLenDisplay()"/>
<div class="range-vals"><span>1</span><span>min</span><span>15</span></div>
<input type="range" id="len-max-s" min="1" max="15" value="8" oninput="updateLenDisplay()"/>
<div class="range-vals"><span>1</span><span>max</span><span>15</span></div>
</div>
<div class="filter-group">
<span class="filter-label">Commence par</span>
<input type="text" class="filter-input" id="finder-q" placeholder="ex: MA, TH…" maxlength="5" oninput="this.value=this.value.toUpperCase()"/>
</div>
<div class="filter-group">
<span class="filter-label">Période de popularité</span>
<div class="pill-group">
<button class="pill" id="era-ancien" onclick="toggleEra(this,'ancien')">Classique</button>
<button class="pill" id="era-retro" onclick="toggleEra(this,'retro')">Rétro 80s</button>
<button class="pill active" id="era-recent" onclick="toggleEra(this,'recent')">Actuel</button>
</div>
<div class="hint" id="era-hint">Actuel : 20102023</div>
</div>
<button class="btn-search-finder" onclick="doFinder()">Rechercher</button>
</div>
</aside>
<div>
<div class="finder-results-header">
<div class="results-title">Résultats</div>
<div class="results-count" id="finder-count"></div>
</div>
<div class="results-grid" id="finder-grid">
<div class="state-placeholder"><span class="ph-icon">✨</span><p>Définissez vos critères et lancez la recherche</p></div>
</div>
</div>
</div>
</div>
<script>
let evoChart=null, compareChart=null;
let finderSexe='', finderPop='rare', finderEra='recent';
let acTimers=[null,null], acSelected=[-1,-1];
const eraMap={ancien:{min:1900,max:1960,label:'Classique (avant 1960)'},retro:{min:1970,max:1995,label:'Rétro 80s (19701995)'},recent:{min:2010,max:2023,label:'Actuel (20102023)'}};
const popMap={rare:{min:0,max:1999},discret:{min:2000,max:9999},connu:{min:10000,max:49999},populaire:{min:50000,max:9999999}};
// THEME
function toggleTheme(){
const d=document.documentElement,isDark=d.getAttribute('data-theme')==='dark';
d.setAttribute('data-theme',isDark?'light':'dark');
document.getElementById('theme-btn').textContent=isDark?'🌙':'☀️';
localStorage.setItem('theme',isDark?'light':'dark');
}
(function(){
const s=localStorage.getItem('theme')||'light';
document.documentElement.setAttribute('data-theme',s);
window.addEventListener('DOMContentLoaded',()=>{document.getElementById('theme-btn').textContent=s==='dark'?'☀️':'🌙';});
})();
// MODE
let _prevMode=null;
function switchMode(mode,btn){
const current=document.querySelector('.page.active')?.id?.replace('page-','');
_prevMode=(current&&current!==mode)?current:null;
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
document.querySelectorAll('.mode-btn').forEach(b=>b.classList.remove('active'));
document.getElementById('page-'+mode).classList.add('active');
btn.classList.add('active');
// Afficher/masquer le bouton retour
const backBtn=document.getElementById('back-to-results');
if(backBtn) backBtn.style.display=(mode==='explorer'&&_prevMode==='finder')?'flex':'none';
if(mode==='top') loadTop();
}
// SHARE
function sharePrenom(prenom){
const url=location.origin+location.pathname+'#'+encodeURIComponent(prenom.toUpperCase());
navigator.clipboard.writeText(url).then(()=>{
const btn=document.getElementById('share-btn-'+prenom);
if(btn){btn.textContent='✓ Copié !';btn.classList.add('copied');setTimeout(()=>{btn.textContent='🔗 Partager';btn.classList.remove('copied');},2000);}
});
}
function checkUrlHash(){const h=decodeURIComponent(location.hash.slice(1));if(h){document.getElementById('search-input-1').value=capitalize(h);explorePrenom(h);}}
// AUTOCOMPLETE
function onAcInput(idx){
const val=document.getElementById('search-input-'+idx).value.trim();
clearTimeout(acTimers[idx-1]);
if(val.length<1){closeAc(idx);return;}
acTimers[idx-1]=setTimeout(()=>fetchAc(idx,val),160);
}
async function fetchAc(idx,q){
try{const d=await fetch('/api/autocomplete?q='+encodeURIComponent(q)+'&limit=7').then(r=>r.json());renderAc(idx,d,q);}catch(e){}
}
function renderAc(idx,items,q){
const list=document.getElementById('ac-list-'+idx);
if(!items.length){closeAc(idx);return;}
list.innerHTML='';acSelected[idx-1]=-1;
items.forEach((item)=>{
const nom=capitalize(item.prenom);
const re=new RegExp('^('+escRe(capitalize(q))+')','i');
const hl=nom.replace(re,'<mark>$1</mark>');
const si=item.sexe===1?'♂':item.sexe===2?'♀':'⚥';
const div=document.createElement('div');div.className='ac-item';
div.innerHTML='<span class="ac-name">'+hl+'</span><span class="ac-meta">'+si+' · '+fmtK(item.total)+'</span>';
div.addEventListener('mousedown',e=>{e.preventDefault();selectAc(idx,item.prenom);});
list.appendChild(div);
});
list.classList.add('open');
}
function escRe(s){return s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');}
function onAcKey(e,idx){
const list=document.getElementById('ac-list-'+idx);
const items=list.querySelectorAll('.ac-item');
if(!items.length){if(e.key==='Enter')doExplore();return;}
if(e.key==='ArrowDown'){e.preventDefault();acSelected[idx-1]=Math.min(acSelected[idx-1]+1,items.length-1);highlightAc(idx,items);}
else if(e.key==='ArrowUp'){e.preventDefault();acSelected[idx-1]=Math.max(acSelected[idx-1]-1,-1);highlightAc(idx,items);}
else if(e.key==='Enter'){if(acSelected[idx-1]>=0){selectAc(idx,items[acSelected[idx-1]].querySelector('.ac-name').textContent);}else{doExplore();}}
else if(e.key==='Escape'){closeAc(idx);}
}
function highlightAc(idx,items){
items.forEach((el,i)=>el.style.background=i===acSelected[idx-1]?'var(--bg2)':'');
if(acSelected[idx-1]>=0)document.getElementById('search-input-'+idx).value=items[acSelected[idx-1]].querySelector('.ac-name').textContent;
}
function selectAc(idx,prenom){document.getElementById('search-input-'+idx).value=capitalize(prenom);closeAc(idx);if(idx===1&&!document.getElementById('compare-chk').checked)doExplore();}
function closeAc(idx){const l=document.getElementById('ac-list-'+idx);l.classList.remove('open');l.innerHTML='';}
document.addEventListener('click',e=>{if(!e.target.closest('.autocomplete-wrap')){closeAc(1);closeAc(2);}});
// COMPARE TOGGLE
function toggleCompare(){
const on=document.getElementById('compare-chk').checked;
document.getElementById('ac-wrap-2').classList.toggle('open',on);
if(!on)closeAc(2);
}
// EXPLORE
function quickSearch(p){document.getElementById('search-input-1').value=p;document.getElementById('compare-chk').checked=false;document.getElementById('ac-wrap-2').classList.remove('open');doExplore();}
function doExplore(){
closeAc(1);closeAc(2);
const p1=document.getElementById('search-input-1').value.trim();if(!p1)return;
const cmp=document.getElementById('compare-chk').checked;
const p2=cmp?document.getElementById('search-input-2').value.trim():null;
location.hash=encodeURIComponent(p1.toUpperCase());
if(cmp&&p2)doCompare(p1,p2);else explorePrenom(p1);
}
async function explorePrenom(prenom){
const res=document.getElementById('explorer-result');
res.style.display='block';
res.innerHTML='<div style="text-align:center;padding:60px;color:var(--ink3)"><span class="loading-pulse">Chargement…</span></div>';
try{
const [evo,hm]=await Promise.all([
fetch('/api/evolution/'+encodeURIComponent(prenom.toUpperCase())).then(r=>{if(!r.ok)throw new Error();return r.json();}),
fetch('/api/heatmap/'+encodeURIComponent(prenom.toUpperCase())).then(r=>r.ok?r.json():[]),
]);
renderExplorer(prenom,evo,hm);
}catch(e){res.innerHTML='<div style="text-align:center;padding:40px;color:var(--ink3)">Prénom introuvable.</div>';}
}
function renderExplorer(prenom,evo,hm){
const nom=capitalize(prenom),res=document.getElementById('explorer-result');
const hasMale=evo.some(d=>d.sexe===1),hasFemale=evo.some(d=>d.sexe===2);
const totM=evo.filter(d=>d.sexe===1).reduce((s,d)=>s+d.valeur,0);
const totF=evo.filter(d=>d.sexe===2).reduce((s,d)=>s+d.valeur,0);
const totAll=totM+totF;
const byY={};evo.forEach(d=>{byY[d.periode]=(byY[d.periode]||0)+d.valeur;});
const pe=Object.entries(byY).sort((a,b)=>b[1]-a[1])[0];
const peakYr=pe?.[0]??'—',peakVal=pe?.[1]??0;
const ys=Object.keys(byY).map(Number).sort(),maxY=Math.max(...ys);
const r10=ys.filter(y=>y>=maxY-9).reduce((s,y)=>s+(byY[y]||0),0);
const p10=ys.filter(y=>y>=maxY-19&&y<maxY-9).reduce((s,y)=>s+(byY[y]||0),0);
const tpct=p10>0?Math.round(((r10-p10)/p10)*100):null;
let badges='';
if(hasMale&&hasFemale)badges+='<span class="badge badge-mix">Mixte</span>';
else if(hasMale)badges+='<span class="badge badge-m">♂ Masculin</span>';
else if(hasFemale)badges+='<span class="badge badge-f">♀ Féminin</span>';
badges+='<span class="badge badge-len">'+prenom.replace(/\s/g,'').length+' lettres</span>';
res.innerHTML=`
<div class="prenom-hero"><div class="prenom-big">${nom}</div><div class="prenom-badges">${badges}</div></div>
<div class="stat-row">
<div class="stat-card"><span class="val">${fmt(totAll)}</span><span class="lbl">Naissances totales</span></div>
${hasMale?'<div class="stat-card"><span class="val" style="color:var(--blue)">'+fmt(totM)+'</span><span class="lbl">♂ Garçons</span></div>':''}
${hasFemale?'<div class="stat-card"><span class="val" style="color:var(--pink)">'+fmt(totF)+'</span><span class="lbl">♀ Filles</span></div>':''}
<div class="stat-card"><span class="val">${fmt(peakVal)}</span><span class="lbl">Pic annuel (${peakYr})</span></div>
</div>
<div class="peak-row">
<div class="peak-card"><span class="peak-icon">📅</span><div><div class="peak-year">${peakYr}</div><div class="peak-desc">Année la plus populaire</div><div class="trend-bar"><div class="trend-fill" style="width:100%"></div></div></div></div>
<div class="peak-card"><span class="peak-icon">${tpct===null?'📊':tpct>=0?'📈':'📉'}</span><div><div class="peak-year" style="color:${tpct===null?'inherit':tpct>=0?'#276749':'#c53030'}">${tpct===null?'—':(tpct>=0?'+':'')+tpct+'%'}</div><div class="peak-desc">Tendance 10 dernières années</div></div></div>
</div>
<div class="chart-card">
<div class="chart-card-title">Évolution des naissances<button class="share-btn" id="share-btn-${nom}" onclick="sharePrenom('${nom}')">🔗 Partager</button></div>
<div class="chart-wrap"><canvas id="evo-canvas"></canvas></div>
</div>
<div class="chart-card">
<div class="chart-card-title">Carte de chaleur par décennie</div>
<div id="heatmap-scroll"><div id="heatmap-container"></div></div>
</div>`;
buildEvoChart(evo,nom);buildHeatmap(hm);
}
// COMPARE
async function doCompare(p1,p2){
const res=document.getElementById('explorer-result');res.style.display='block';
res.innerHTML='<div style="text-align:center;padding:60px;color:var(--ink3)"><span class="loading-pulse">Chargement…</span></div>';
try{
const data=await fetch('/api/compare?a='+encodeURIComponent(p1.toUpperCase())+'&b='+encodeURIComponent(p2.toUpperCase())).then(r=>r.json());
renderCompare(p1,p2,data);
}catch(e){res.innerHTML='<div style="text-align:center;padding:40px;color:var(--ink3)">Erreur.</div>';}
}
function renderCompare(p1,p2,data){
const n1=capitalize(p1),n2=capitalize(p2);
const e1=data[p1.toUpperCase()]||[],e2=data[p2.toUpperCase()]||[];
const t1=e1.reduce((s,d)=>s+d.valeur,0),t2=e2.reduce((s,d)=>s+d.valeur,0);
const by1={};e1.forEach(d=>{by1[d.periode]=(by1[d.periode]||0)+d.valeur;});
const by2={};e2.forEach(d=>{by2[d.periode]=(by2[d.periode]||0)+d.valeur;});
const pk1=Object.entries(by1).sort((a,b)=>b[1]-a[1])[0];
const pk2=Object.entries(by2).sort((a,b)=>b[1]-a[1])[0];
document.getElementById('explorer-result').innerHTML=`
<div class="prenom-hero"><div class="prenom-big">${n1} <span style="color:var(--ink3);font-size:.5em">vs</span> ${n2}</div></div>
<div class="stat-row">
<div class="stat-card"><span class="val" style="color:var(--blue)">${fmt(t1)}</span><span class="lbl">${n1} · total</span></div>
<div class="stat-card"><span class="val" style="color:var(--accent)">${fmt(t2)}</span><span class="lbl">${n2} · total</span></div>
<div class="stat-card"><span class="val">${pk1?.[0]??'—'}</span><span class="lbl">Pic ${n1}</span></div>
<div class="stat-card"><span class="val">${pk2?.[0]??'—'}</span><span class="lbl">Pic ${n2}</span></div>
</div>
<div class="chart-card">
<div class="chart-card-title">Comparaison — ${n1} vs ${n2}</div>
<div class="chart-wrap"><canvas id="compare-canvas"></canvas></div>
</div>`;
buildCompareChart(e1,e2,n1,n2);
}
// CHARTS
function cColors(){
const dark=document.documentElement.getAttribute('data-theme')==='dark';
return{grid:dark?'rgba(255,255,255,0.05)':'rgba(0,0,0,0.04)',tick:dark?'#6e6458':'#9a8e82',ttBg:dark?'#1c1916':'#1a1612'};
}
function cOpts(c){
return{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
plugins:{legend:{labels:{color:c.tick,font:{family:'Outfit',size:11},usePointStyle:true}},
tooltip:{backgroundColor:c.ttBg,titleColor:'#f7f4ef',bodyColor:'#c8bfb0',borderColor:'rgba(255,255,255,0.08)',borderWidth:1,
callbacks:{label:ctx=>' '+ctx.dataset.label+' : '+(ctx.parsed.y?.toLocaleString('fr-FR')??'')}}},
scales:{x:{ticks:{color:c.tick,font:{family:'Outfit',size:10},maxTicksLimit:12},grid:{color:c.grid}},
y:{ticks:{color:c.tick,font:{family:'Outfit',size:10},callback:v=>v>=1000?(v/1000).toFixed(0)+'k':v},grid:{color:c.grid}}}};
}
function buildEvoChart(evo,nom){
const ctx=document.getElementById('evo-canvas').getContext('2d');
if(evoChart)evoChart.destroy();
const p=[...new Set(evo.map(d=>d.periode))].sort();
const hM=evo.some(d=>d.sexe===1),hF=evo.some(d=>d.sexe===2);
const c=cColors(),ds=[];
if(hM)ds.push({label:'Garçons',data:p.map(x=>{const r=evo.find(d=>d.periode===x&&d.sexe===1);return r?.valeur??null;}),borderColor:'#2c5282',backgroundColor:'rgba(44,82,130,0.08)',borderWidth:2,pointRadius:0,pointHoverRadius:5,fill:true,tension:.4,spanGaps:true});
if(hF)ds.push({label:'Filles',data:p.map(x=>{const r=evo.find(d=>d.periode===x&&d.sexe===2);return r?.valeur??null;}),borderColor:'#9b2c5a',backgroundColor:'rgba(155,44,90,0.07)',borderWidth:2,pointRadius:0,pointHoverRadius:5,fill:true,tension:.4,spanGaps:true});
if(hM&&hF)ds.push({label:'Total',data:p.map(x=>evo.filter(d=>d.periode===x).reduce((s,d)=>s+d.valeur,0)),borderColor:'#b7791f',backgroundColor:'transparent',borderWidth:1.5,borderDash:[4,3],pointRadius:0,pointHoverRadius:5,fill:false,tension:.4});
evoChart=new Chart(ctx,{type:'line',data:{labels:p,datasets:ds},options:cOpts(c)});
}
function buildCompareChart(e1,e2,n1,n2){
const ctx=document.getElementById('compare-canvas').getContext('2d');
if(compareChart)compareChart.destroy();
const allP=[...new Set([...e1.map(d=>d.periode),...e2.map(d=>d.periode)])].sort();
const c=cColors();
const ds=[
{label:n1,data:allP.map(p=>e1.filter(d=>d.periode===p).reduce((s,d)=>s+d.valeur,0)||null),borderColor:'#2c5282',backgroundColor:'rgba(44,82,130,0.08)',borderWidth:2.5,pointRadius:0,pointHoverRadius:5,fill:true,tension:.4,spanGaps:true},
{label:n2,data:allP.map(p=>e2.filter(d=>d.periode===p).reduce((s,d)=>s+d.valeur,0)||null),borderColor:'#c0392b',backgroundColor:'rgba(192,57,43,0.07)',borderWidth:2.5,pointRadius:0,pointHoverRadius:5,fill:true,tension:.4,spanGaps:true}
];
compareChart=new Chart(ctx,{type:'line',data:{labels:allP,datasets:ds},options:cOpts(c)});
}
// HEATMAP
function buildHeatmap(hmData){
const container=document.getElementById('heatmap-container');
if(!hmData||!hmData.length){container.innerHTML='<p style="color:var(--ink3);font-size:.83rem;padding:8px 0">Pas assez de données.</p>';return;}
const decades=[...new Set(hmData.map(d=>d.decade))].sort();
const sexes=[...new Set(hmData.map(d=>d.sexe))].sort();
const maxVal=Math.max(...hmData.map(d=>d.valeur));
const sLabel={1:'♂ Garçons',2:'♀ Filles'};
const sColor={1:(a)=>'rgba(44,82,130,'+a+')',2:(a)=>'rgba(155,44,90,'+a+')'};
let html='<div class="heatmap-decade-labels">';
decades.forEach(d=>{html+='<div class="heatmap-decade-lbl">'+d+'s</div>';});
html+='</div>';
sexes.forEach(sexe=>{
html+='<div class="heatmap-row"><div class="heatmap-label">'+(sLabel[sexe]??sexe)+'</div>';
decades.forEach(decade=>{
const entry=hmData.find(d=>d.decade===decade&&d.sexe===sexe);
const val=entry?.valeur??0;
const alpha=val>0?0.15+0.75*(val/maxVal):0;
const fmt2=val>=1000?Math.round(val/1000)+'k':val>0?val:'';
if(val===0)html+='<div class="heatmap-cell heatmap-cell-0" title="'+decade+'s : 0"></div>';
else html+='<div class="heatmap-cell" style="background:'+sColor[sexe](alpha)+'" title="'+decade+'s : '+val.toLocaleString('fr-FR')+'">'+fmt2+'</div>';
});
html+='</div>';
});
container.innerHTML='<div class="heatmap-inner">'+html+'</div>';
}
// FINDER
function setSexe(btn,val){
finderSexe=val;
document.querySelectorAll('[data-sexe]').forEach(b=>b.classList.remove('active','active-blue','active-pink','active-mix'));
if(val==='1')btn.classList.add('active-blue');
else if(val==='2')btn.classList.add('active-pink');
else if(val==='mixte')btn.classList.add('active-mix');
else btn.classList.add('active');
}
function togglePop(btn,key){
finderPop=key;
document.querySelectorAll('[id^="pop-"]').forEach(b=>b.classList.remove('active','active-gold'));
btn.classList.add('active-gold');
}
function toggleEra(btn,key){
finderEra=key;
document.querySelectorAll('[id^="era-"]').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('era-hint').textContent=eraMap[key].label;
}
function updateLenDisplay(){
const min=+document.getElementById('len-min-s').value,max=+document.getElementById('len-max-s').value;
document.getElementById('len-disp').textContent=min+' à '+max+' lettres';
}
async function doFinder(){
const grid=document.getElementById('finder-grid');
grid.innerHTML='<div class="state-placeholder"><span class="loading-pulse">Recherche en cours…</span></div>';
document.getElementById('finder-count').textContent='';
const era=eraMap[finderEra];
const lenMin=+document.getElementById('len-min-s').value,lenMax=+document.getElementById('len-max-s').value;
const q=document.getElementById('finder-q').value.trim();
const popMin=popMap[finderPop].min;
const popMax=popMap[finderPop].max;
// popularite filtre sur pic annuel all-time (via peak_min/peak_max, periode all-time)
const params=new URLSearchParams({longueur_min:lenMin,longueur_max:lenMax,popularite_min:popMin,popularite_max:popMax,periode_min:era.min,periode_max:era.max,limit:80});
if(finderSexe&&finderSexe!=='mixte')params.set('sexe',finderSexe);
if(q)params.set('q',q);
try{
let data=await fetch('/api/search?'+params).then(r=>r.json());
if(finderSexe==='mixte'){
const counts={};
data.forEach(d=>{if(!counts[d.prenom])counts[d.prenom]=new Set();counts[d.prenom].add(d.sexe);});
const mx=new Set(Object.entries(counts).filter(([,s])=>s.has(1)&&s.has(2)).map(([n])=>n));
const merged={};
data.filter(d=>mx.has(d.prenom)).forEach(d=>{if(!merged[d.prenom])merged[d.prenom]={prenom:d.prenom,sexe:'mixte',longueur:d.longueur,total:0};merged[d.prenom].total+=d.total;});
data=Object.values(merged).sort((a,b)=>b.total-a.total);
}
renderFinderResults(data);
}catch(e){grid.innerHTML='<div class="state-placeholder"><p>Erreur de connexion.</p></div>';}
}
function renderFinderResults(data){
const grid=document.getElementById('finder-grid');
document.getElementById('finder-count').textContent=data.length+' résultat'+(data.length>1?'s':'');
if(!data.length){grid.innerHTML='<div class="state-placeholder"><span class="ph-icon">🤔</span><p>Aucun résultat. Élargissez les filtres.</p></div>';return;}
const maxT=Math.max(...data.map(d=>d.total_periode||d.total));
grid.innerHTML='';
data.forEach((item,i)=>{
const nom=capitalize(item.prenom),pct=Math.round(((item.total_periode||item.total)/maxT)*100);
const sb=item.sexe===1?'<span class="badge badge-m">♂</span>':item.sexe===2?'<span class="badge badge-f">♀</span>':'<span class="badge badge-mix">⚥</span>';
const pc=item.total<2000?['pop-rare','Rare']:item.total<10000?['pop-discret','Discret']:item.total<50000?['pop-connu','Connu']:['pop-populaire','Populaire'];
const card=document.createElement('div');card.className='result-card';
card.style.animationDelay=Math.min(i*12,350)+'ms';
card.innerHTML='<div class="rc-name">'+nom+'</div><div class="rc-meta">'+sb+'<span class="pop-label '+pc[0]+'">'+pc[1]+'</span></div><div class="rc-total">'+fmt(item.total)+' naissances</div><div class="rc-mini-bar"><div class="rc-mini-fill" style="width:'+pct+'%"></div></div>';
card.addEventListener('click',()=>{
document.getElementById('search-input-1').value=nom;
document.getElementById('compare-chk').checked=false;
document.getElementById('ac-wrap-2').classList.remove('open');
switchMode('explorer',document.querySelector('.mode-btn'));
explorePrenom(item.prenom);
// Afficher bouton retour
const backBtn=document.getElementById('back-to-results');
if(backBtn) backBtn.style.display='flex';
});
grid.appendChild(card);
});
}
// UTILS
function fmt(n){return n?.toLocaleString('fr-FR')??'—';}
function fmtK(n){return n>=1000?Math.round(n/1000)+'k':n;}
function capitalize(s){return s?s.charAt(0).toUpperCase()+s.slice(1).toLowerCase():'';}
// ── CLASSEMENT ──
let topSexe='', topEra='recent', topOrder='desc', topPage=1, topTotal=0, topPerPage=50, topData=null;
const topEraMap={all:{min:1900,max:2024},recent:{min:2010,max:2024},retro:{min:1970,max:1995},ancien:{min:1900,max:1960}};
function setTopSexe(btn,val){topSexe=val;topPage=1;topData=null;document.querySelectorAll('[data-top-sexe]').forEach(b=>b.classList.remove('active','active-blue','active-pink'));if(val==='1')btn.classList.add('active-blue');else if(val==='2')btn.classList.add('active-pink');else btn.classList.add('active');loadTop();}
function setTopEra(btn,val){topEra=val;topPage=1;topData=null;document.querySelectorAll('[data-top-era]').forEach(b=>b.classList.remove('active'));btn.classList.add('active');loadTop();}
function setTopOrder(val){topOrder=val;topPage=1;topData=null;document.getElementById('top-order-desc').classList.toggle('active',val==='desc');document.getElementById('top-order-asc').classList.toggle('active',val==='asc');loadTop();}
function topChangePage(delta){const maxPage=Math.ceil(topTotal/topPerPage);topPage=Math.max(1,Math.min(maxPage,topPage+delta));topData=null;loadTop();}
function renderTopTable(res){
const wrap=document.getElementById('top-table-wrap');if(!wrap)return;
topTotal=res.total;topData=res;
const maxVal=Math.max(...res.data.map(d=>d.total_periode||1));
const maxPage=Math.ceil(res.total/topPerPage);
document.getElementById('top-count').textContent=res.total.toLocaleString('fr-FR')+' prénoms';
document.getElementById('top-page-info').textContent='Page '+res.page+' / '+maxPage;
document.getElementById('top-prev').disabled=res.page<=1;
document.getElementById('top-next').disabled=res.page>=maxPage;
let html='<table class="top-table"><thead><tr><th class="rang">#</th><th>Prénom</th><th>Genre</th><th>Naissances</th><th class="bar-cell"></th></tr></thead><tbody>';
res.data.forEach((item,i)=>{
const nom=capitalize(item.prenom);
const rang=(topOrder==='desc')?(res.page-1)*topPerPage+i+1:res.total-(res.page-1)*topPerPage-i;
const si=item.sexe===1?'<span class="badge badge-m">♂</span>':item.sexe===2?'<span class="badge badge-f">♀</span>':'<span class="badge badge-mix">⚥</span>';
const total=item.total_periode||item.total;
const pct=Math.round((total/maxVal)*100);
const medal=rang===1?'🥇 ':rang===2?'🥈 ':rang===3?'🥉 ':'';
html+=`<tr><td class="rang" style="color:${rang<=3?'var(--accent)':'var(--ink3)'};font-weight:${rang<=3?700:400}">${medal}${rang}</td><td><span class="prenom-link" onclick="goToPrenom('${item.prenom}')">${nom}</span></td><td>${si}</td><td style="font-family:'Cormorant Garamond',serif;font-size:1.1rem;">${fmt(total)}</td><td class="bar-cell"><div class="top-mini-bar"><div class="top-mini-fill" style="width:${pct}%"></div></div></td></tr>`;
});
html+='</tbody></table>';wrap.innerHTML=html;
}
async function loadTop(){
const wrap=document.getElementById('top-table-wrap');
if(wrap)wrap.innerHTML='<div class="state-placeholder"><span style="font-size:2rem">⏳</span><p style="margin-top:8px;color:var(--ink3)">Chargement…</p></div>';
const era=topEraMap[topEra];
const params=new URLSearchParams({periode_min:era.min,periode_max:era.max,order:topOrder,page:topPage,per_page:topPerPage});
if(topSexe)params.set('sexe',topSexe);
try{const res=await fetch('/api/top?'+params).then(r=>r.json());renderTopTable(res);}
catch(e){if(wrap)wrap.innerHTML='<div class="state-placeholder"><p>Erreur de connexion.</p></div>';}
}
function goToPrenom(prenom){
document.getElementById('search-input-1').value=capitalize(prenom);
switchMode('explorer',document.querySelector('.mode-btn'));
explorePrenom(prenom);
const backBtn=document.getElementById('back-to-results');
if(backBtn)backBtn.style.display='flex';
}
function goBackToFinder(){switchMode('finder',document.querySelectorAll('.mode-btn')[1]);}
// INIT
window.addEventListener('load',()=>{
// Heatmap scroll indicator
document.addEventListener('scroll', updateHeatmapIndicator, true);
function updateHeatmapIndicator(){
const el=document.getElementById('heatmap-scroll-el');
if(!el) return;
const wrap=el.closest('.heatmap-scroll-wrap');
if(wrap) wrap.classList.toggle('at-end', el.scrollLeft+el.clientWidth >= el.scrollWidth-4);
}
// Also update after heatmap is built
const origBuild=window.buildHeatmap;
setTimeout(updateHeatmapIndicator, 500);
checkUrlHash();
window.addEventListener('hashchange',checkUrlHash);
// Mobile: auto-collapse sidebar on small screens
const sidebar = document.querySelector('.finder-sidebar');
document.querySelector('.sidebar-header').addEventListener('click', () => {
sidebar.classList.toggle('collapsed');
});
if (window.innerWidth <= 720) sidebar.classList.add('collapsed');
});
</script>
<div id="page-top" class="page">
<div style="max-width:860px;margin:0 auto;padding:44px 18px 80px;">
<h1 class="hero-title" style="text-align:center;margin-bottom:6px;">Classement des <em>prénoms</em></h1>
<p class="hero-sub" style="text-align:center;margin-bottom:28px;">Total de naissances sur la période · Données INSEE</p>
<!-- Contrôles -->
<div style="display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;margin-bottom:20px;">
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
<!-- Genre -->
<div class="pill-group">
<button class="pill active" data-top-sexe="" onclick="setTopSexe(this,'')">Tous</button>
<button class="pill" data-top-sexe="1" onclick="setTopSexe(this,'1')">♂ Garçons</button>
<button class="pill" data-top-sexe="2" onclick="setTopSexe(this,'2')">♀ Filles</button>
</div>
<!-- Période -->
<div class="pill-group">
<button class="pill" data-top-era="all" onclick="setTopEra(this,'all')">Toutes années</button>
<button class="pill active" data-top-era="recent" onclick="setTopEra(this,'recent')">Actuel (20102024)</button>
<button class="pill" data-top-era="retro" onclick="setTopEra(this,'retro')">Rétro (19701995)</button>
<button class="pill" data-top-era="ancien" onclick="setTopEra(this,'ancien')">Classique (avant 1960)</button>
</div>
</div>
<!-- Ordre -->
<div style="display:flex;gap:6px;">
<button id="top-order-desc" class="pill active" onclick="setTopOrder('desc')">↓ Plus donnés</button>
<button id="top-order-asc" class="pill" onclick="setTopOrder('asc')">↑ Plus rares</button>
</div>
</div>
<!-- Tableau -->
<div style="background:var(--card);border:1px solid var(--border);border-radius:14px;overflow:hidden;">
<div id="top-table-wrap">
<div class="state-placeholder"><span class="ph-icon loading-pulse">⏳</span><p>Chargement…</p></div>
</div>
<!-- Pagination -->
<div id="top-pagination" style="display:flex;align-items:center;justify-content:space-between;padding:12px 18px;border-top:1px solid var(--border);gap:10px;flex-wrap:wrap;">
<span id="top-count" style="font-size:0.75rem;color:var(--ink3);"></span>
<div style="display:flex;gap:6px;align-items:center;">
<button id="top-prev" class="pill" onclick="topChangePage(-1)" style="padding:5px 14px;">← Préc.</button>
<span id="top-page-info" style="font-size:0.78rem;color:var(--ink2);"></span>
<button id="top-next" class="pill" onclick="topChangePage(1)" style="padding:5px 14px;">Suiv. →</button>
</div>
</div>
</div>
</div>
</div>
</body>
</html>