Les cinq bibliothèques chargées depuis unpkg et jsDelivr (Leaflet, MarkerCluster, leaflet.heat, noUiSlider, flag-icons) n'avaient aucun contrôle d'intégrité : un CDN compromis exécutait du code arbitraire sur le site public. Les neuf hashes ont été recalculés depuis les URL versionnées. GoatCounter reste sans SRI : son script n'est pas versionné. apiGet() reçoit un timeout : sans lui, une API qui ne répond jamais laissait l'interface et le rechargement du viewport bloqués indéfiniment. 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>
|
|
<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>
|