Le site public charge l'intégralité du corpus une seconde après l'ouverture de la page (loadAllHeatPoints), pour la heatmap et la recherche. Rien ne compressait dans la chaîne — ni Fastify, ni Traefik. Ce n'était donc pas un risque théorique mais la facture de chaque visiteur, à chaque visite. Mesuré sur 100 000 groupes avec des données réalistes, en-tête Accept-Encoding d'un vrai navigateur : 21,5 Mo deviennent 1,9 Mo. gzip est préféré à brotli, contre l'intuition : br sort 2,3 Mo là où gzip fait 1,9 Mo. La qualité brotli par défaut est réglée pour la vitesse, et sur ce JSON très répétitif elle perd sur les deux tableaux à la fois — taille ET CPU. Seuil à 1 Ko, en dessous duquel compresser coûte plus que ça ne rapporte. /api/clusters plafonnait par ailleurs sa réponse à 2000 groupes ou 1000 clusters sans le dire, ni côté API ni côté client : dans une zone dense, des groupes disparaissaient de la carte sans aucun signal. La réponse porte désormais `truncated` et `limit`, et l'interface affiche un avertissement invitant à zoomer. Clé de traduction ajoutée dans les 21 langues, la convention du fichier étant une couverture complète. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
273 lines
16 KiB
HTML
273 lines
16 KiB
HTML
<!doctype html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
<title>Metal from Europe</title>
|
|
|
|
<!-- Fonts -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=UnifrakturCook:wght@700&family=Inter:wght@300;400;600;700;900&display=swap" rel="stylesheet">
|
|
|
|
<!-- Leaflet -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha384-sHL9NAb7lN7rfvG5lfHpm643Xkcjzp4jFvuavGOndn6pjVqS6ny56CAt3nsEVT4H" crossorigin="anonymous" />
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha384-cxOPjt7s7Iz04uaHJceBmS+qpjv2JkIHNVcuOrM+YHwZOmJGBXI00mdUXEq65HTH" crossorigin="anonymous"></script>
|
|
|
|
<!-- MarkerCluster -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"
|
|
integrity="sha384-pmjIAcz2bAn0xukfxADbZIb3t8oRT9Sv0rvO+BR5Csr6Dhqq+nZs59P0pPKQJkEV" crossorigin="anonymous"/>
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"
|
|
integrity="sha384-wgw+aLYNQ7dlhK47ZPK7FRACiq7ROZwgFNg0m04avm4CaXS+Z9Y7nMu8yNjBKYC+" crossorigin="anonymous"/>
|
|
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"
|
|
integrity="sha384-eXVCORTRlv4FUUgS/xmOyr66XBVraen8ATNLMESp92FKXLAMiKkerixTiBvXriZr" crossorigin="anonymous"></script>
|
|
|
|
<!-- Heatmap -->
|
|
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"
|
|
integrity="sha384-mFKkGiGvT5vo1fEyGCD3hshDdKmW3wzXW/x+fWriYJArD0R3gawT6lMvLboM22c0" crossorigin="anonymous"></script>
|
|
|
|
<!-- noUiSlider (double slider timeline) -->
|
|
<link rel="stylesheet" href="https://unpkg.com/nouislider@15.7.1/dist/nouislider.min.css"
|
|
integrity="sha384-PSZaVsyG9jDu8hFaSJev5s/9poIJlX7cuxSGdqCgXRHpo2DzIaZAyCd2rG/DJJmV" crossorigin="anonymous">
|
|
<script src="https://unpkg.com/nouislider@15.7.1/dist/nouislider.min.js"
|
|
integrity="sha384-/gBUOLHADjY2rp6bHB0IyW9AC28q4OsnirJScje4l1crgYW7Qarx3dH8zcqcUgmy" crossorigin="anonymous"></script>
|
|
|
|
<!-- GoatCounter (script d'analytics non versionné → pas de SRI possible) -->
|
|
<script data-goatcounter="https://metalfromeurope.goatcounter.com/count"
|
|
async src="//gc.zgo.at/count.js"></script>
|
|
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css"
|
|
integrity="sha384-aQuvIWWIbpu/mSqULLDiUveyYiPoJzPKAWjUmGJ+Elm+N/LJhzfZqsutsfw870JS" crossorigin="anonymous">
|
|
<link rel="stylesheet" href="./styles.css" />
|
|
</head>
|
|
|
|
<body>
|
|
<header id="topbar">
|
|
<button class="sidebar-toggle" id="sidebarToggle" type="button" aria-expanded="true" aria-controls="sidebar" title="Afficher / masquer les options">
|
|
<span class="icon">☰</span>
|
|
<span class="label" data-i18n="options_btn">Options</span>
|
|
</button>
|
|
<div class="brand">Metal from Europe</div>
|
|
<div class="topbar-right">
|
|
<div class="lang-dropdown">
|
|
<button class="lang-trigger" id="langTrigger" type="button" aria-haspopup="listbox" aria-expanded="false">
|
|
<span class="fi lang-flag"></span>
|
|
<span class="lang-code">EN</span>
|
|
<span class="lang-arrow">▾</span>
|
|
</button>
|
|
<div class="lang-options hidden" id="langOptions" role="listbox" aria-label="Choisir la langue">
|
|
<button class="lang-opt" type="button" role="option" aria-selected="false" data-lang="en"><span class="fi fi-gb"></span>English</button>
|
|
<button class="lang-opt" type="button" role="option" aria-selected="false" data-lang="fr"><span class="fi fi-fr"></span>Français</button>
|
|
<button class="lang-opt" type="button" role="option" aria-selected="false" data-lang="de"><span class="fi fi-de"></span>Deutsch</button>
|
|
<button class="lang-opt" type="button" role="option" aria-selected="false" data-lang="es"><span class="fi fi-es"></span>Español</button>
|
|
<button class="lang-opt" type="button" role="option" aria-selected="false" data-lang="it"><span class="fi fi-it"></span>Italiano</button>
|
|
<button class="lang-opt" type="button" role="option" aria-selected="false" data-lang="pl"><span class="fi fi-pl"></span>Polski</button>
|
|
<button class="lang-opt" type="button" role="option" aria-selected="false" data-lang="nl"><span class="fi fi-nl"></span>Nederlands</button>
|
|
<button class="lang-opt" type="button" role="option" aria-selected="false" data-lang="ro"><span class="fi fi-ro"></span>Română</button>
|
|
<button class="lang-opt" type="button" role="option" aria-selected="false" data-lang="pt"><span class="fi fi-pt"></span>Português</button>
|
|
<button class="lang-opt" type="button" role="option" aria-selected="false" data-lang="cs"><span class="fi fi-cz"></span>Čeština</button>
|
|
<button class="lang-opt" type="button" role="option" aria-selected="false" data-lang="sv"><span class="fi fi-se"></span>Svenska</button>
|
|
</div>
|
|
</div>
|
|
<a href="https://discord.gg/P2DE57Ab" target="_blank" rel="noopener noreferrer" class="social-link" title="Rejoindre notre Discord">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
|
|
</svg>
|
|
</a>
|
|
<a href="https://x.com/MetalFromEurope" target="_blank" rel="noopener noreferrer" class="social-link" title="Suivre sur X/Twitter">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
|
</svg>
|
|
</a>
|
|
<a href="https://www.instagram.com/metalfromeurope/" target="_blank" rel="noopener noreferrer" class="social-link" title="Suivre sur Instagram">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M7.8 2h8.4C19.4 2 22 4.6 22 7.8v8.4a5.8 5.8 0 0 1-5.8 5.8H7.8C4.6 22 2 19.4 2 16.2V7.8A5.8 5.8 0 0 1 7.8 2m-.2 2A3.6 3.6 0 0 0 4 7.6v8.8C4 18.39 5.61 20 7.6 20h8.8a3.6 3.6 0 0 0 3.6-3.6V7.6C20 5.61 18.39 4 16.4 4H7.6m9.65 1.5a1.25 1.25 0 0 1 1.25 1.25A1.25 1.25 0 0 1 17.25 8A1.25 1.25 0 0 1 16 6.75a1.25 1.25 0 0 1 1.25-1.25M12 7a5 5 0 0 1 5 5a5 5 0 0 1-5 5a5 5 0 0 1-5-5a5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3z"/>
|
|
</svg>
|
|
</a>
|
|
<a href="https://www.metal-archives.com/" target="_blank" rel="noopener noreferrer" class="social-link ma-link" title="Visiter Metal Archives">
|
|
<span class="ma-text">Metal Archives</span>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" class="external-icon">
|
|
<path d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</header>
|
|
|
|
<div id="app">
|
|
<aside id="sidebar">
|
|
<div class="sidebar-inner">
|
|
<div class="topbar">
|
|
<div class="search">
|
|
<input
|
|
id="q"
|
|
type="search"
|
|
placeholder="Rechercher (nom, genre, ville, pays)…"
|
|
data-i18n-placeholder="search_placeholder"
|
|
aria-label="Rechercher des groupes"
|
|
autocomplete="off"
|
|
>
|
|
<div class="search-suggestions" id="searchSuggestions"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats">
|
|
<span class="chip"><span class="dot"></span><b id="count">0</b> <span data-i18n="displayed">affichés</span><span id="truncNote" class="trunc-note" hidden></span></span>
|
|
<span class="chip"><b id="total">0</b> <span data-i18n="total_bands">total</span></span>
|
|
<span class="chip"><b id="unique">0</b> <span data-i18n="localities">localités</span></span>
|
|
<span class="chip"><b id="geocoded">0</b> <span data-i18n="located">localisés</span></span>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<div class="controls-head">
|
|
<div class="controls-title" data-i18n="view_section">Vue</div>
|
|
<div class="controls-actions">
|
|
<button class="btn btn-compact" id="btnReset" type="button" data-i18n="reset_view">Reset view</button>
|
|
<label class="switch" title="Activer la heatmap">
|
|
<input type="checkbox" id="toggleHeat" />
|
|
<span class="slider"></span>
|
|
<span class="switch-label" data-i18n="heatmap_label">Heatmap</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<div class="controls-head">
|
|
<div class="controls-title" data-i18n="filters_section">Filtres</div>
|
|
<button class="btn btn-compact" id="btnNoLocation" type="button">
|
|
<span data-i18n="without_coords_btn">Sans coords</span> <span class="badge" id="noLocCount">0</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Country -->
|
|
<div class="filter-box">
|
|
<div class="filter-title" data-i18n="country_label">Pays</div>
|
|
<button class="dropdown-trigger" id="countryToggle" type="button" aria-expanded="false" aria-controls="countrySelect">
|
|
<span class="label" data-i18n="selection_label">Sélection</span>
|
|
<span class="summary" id="countrySummary" data-i18n="all_label">Tous</span>
|
|
<span class="caret">▾</span>
|
|
</button>
|
|
<div class="multiselect is-collapsed" id="countrySelect"></div>
|
|
</div>
|
|
|
|
<!-- Status -->
|
|
<div class="filter-box">
|
|
<div class="filter-title" data-i18n="status_label">Statut</div>
|
|
<div class="chips" id="statusGrid"></div>
|
|
</div>
|
|
|
|
<!-- Macro genres -->
|
|
<div class="filter-box">
|
|
<div class="filter-title" data-i18n="genres_label">Genres</div>
|
|
<div class="macro-grid" id="macroGenres"></div>
|
|
</div>
|
|
|
|
<!-- Genre multiselect -->
|
|
<div class="filter-box">
|
|
<div class="filter-title" data-i18n="subgenres_label">Sous-genres</div>
|
|
<button class="dropdown-trigger" id="genreToggle" type="button" aria-expanded="false" aria-controls="genreSelect">
|
|
<span class="label" data-i18n="selection_label">Sélection</span>
|
|
<span class="summary" id="genreSummary" data-i18n="all_label">Tous</span>
|
|
<span class="caret">▾</span>
|
|
</button>
|
|
<div class="multiselect is-collapsed" id="genreSelect"></div>
|
|
</div>
|
|
|
|
<!-- Themes -->
|
|
<div class="filter-box" id="themeBox" style="display:none;">
|
|
<div class="filter-title" data-i18n="themes_label">Thèmes</div>
|
|
<button class="dropdown-trigger" id="themeToggle" type="button" aria-expanded="false" aria-controls="themeSelect">
|
|
<span class="label" data-i18n="selection_label">Sélection</span>
|
|
<span class="summary" id="themeSummary" data-i18n="all_label">Tous</span>
|
|
<span class="caret">▾</span>
|
|
</button>
|
|
<div class="multiselect is-collapsed" id="themeSelect"></div>
|
|
</div>
|
|
|
|
<!-- Timeline -->
|
|
<div class="filter-box">
|
|
<div class="filter-title" data-i18n="timeline_label">Timeline (année de formation)</div>
|
|
<div class="timeline">
|
|
<div id="yearSlider"></div>
|
|
<div class="timeline-meta">
|
|
<span class="chip"><b id="yearMin">—</b></span>
|
|
<span class="chip"><b id="yearMax">—</b></span>
|
|
<span class="chip"><b id="yearHint">—</b></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sort -->
|
|
<div class="filter-box">
|
|
<div class="filter-title" data-i18n="sort_label">Tri de la liste</div>
|
|
<select id="sortSelect" class="select" aria-label="Trier les groupes">
|
|
<option value="az" data-i18n="sort_az">A → Z</option>
|
|
<option value="status" data-i18n="sort_status">Par statut</option>
|
|
<option value="genre" data-i18n="sort_genre">Par genre</option>
|
|
<option value="country" data-i18n="sort_country">Par pays</option>
|
|
<option value="year" data-i18n="sort_year">Par année (récent → ancien)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<div class="h" data-i18n="list_section">Liste</div>
|
|
<div class="sub" id="selectionHint">—</div>
|
|
</div>
|
|
<div class="panel-body" id="list"></div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<main id="mapWrap">
|
|
<div id="map" role="application" aria-label="Carte des groupes de Metal"></div>
|
|
</main>
|
|
</div>
|
|
|
|
<footer id="legalBar">
|
|
<div class="legal-inner">
|
|
<button class="legal-link" id="openLegal" type="button" data-i18n="legal_link">Mentions légales</button>
|
|
<span class="legal-dot">•</span>
|
|
<button class="legal-link" id="openFaq" type="button" data-i18n="faq_link">FAQ</button>
|
|
|
|
<span class="legal-dot">•</span>
|
|
|
|
<span class="legal-stats" aria-label="Statistiques de visites">
|
|
<span class="legal-stat"><span data-i18n="visits_label">Visites</span> <b id="gcTotal">—</b></span>
|
|
<span class="legal-dot">/</span>
|
|
<span class="legal-stat"><span data-i18n="this_month_label">Ce mois</span> <b id="gcMonth">—</b></span>
|
|
</span>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- Modal: no location -->
|
|
<div class="modal-backdrop" id="modalBackdrop" aria-hidden="true">
|
|
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
|
<div class="modal-head">
|
|
<div class="h" id="modalTitle">Sans coords</div>
|
|
<div class="x" id="modalClose" title="Fermer" aria-label="Fermer la fenêtre">✕</div>
|
|
</div>
|
|
<div class="modal-body" id="modalBody"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: legal / faq -->
|
|
<div class="modal-backdrop" id="infoBackdrop" aria-hidden="true">
|
|
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="infoTitle">
|
|
<div class="modal-head">
|
|
<div class="h" id="infoTitle">Infos</div>
|
|
<div class="x" id="infoClose" title="Fermer" aria-label="Fermer la fenêtre">✕</div>
|
|
</div>
|
|
<div class="modal-body" id="infoBody"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="./locales.js"></script>
|
|
<!-- pure.js définit la globale BMPure : doit être chargé avant app.js -->
|
|
<script src="./pure.js"></script>
|
|
<script src="./app.js"></script>
|
|
</body>
|
|
</html>
|