Add Sport Enjoyer and Europe presets plus UI improvements

- New Sport Enjoyer preset with rugby, football, and basketball categories
- New Europe preset with auto-built country lists
- Define SPORT_CAT_IDS constant for sport preset
- Fix preset generation with auto-build from database
- Improve mobile UI: tooltip overflow, button interactions, reverse mode consistency
- Refactor tooltip logic: consolidated show/hide, fixed event handling
- Harmonize reverse mode styling with rounded buttons and question mark
- Fix translation key presetIslands in preset grid
- Ensure helper functions defined before use in applyPreset
- Remove duplicates from auto-generated category lists
This commit is contained in:
Mathieu VIART 2026-03-01 23:56:45 +00:00
parent 2be9826b1c
commit 1ffeeab8c8
6 changed files with 677 additions and 79 deletions

View file

@ -1,5 +1,13 @@
# LeGrandGeoQuiz
> Presets for custom mode are now automatically computed at runtime from the
> country descriptions. Previously a set of hard-coded arrays (e.g.
> `EUROPEAN_CODES`, `ISLANDS_CODES`) were required; they have been regenerated
> dynamically using keywords in `js/hints.js`. This ensures all buttons work even
> after data updates.
LeGrandGeoQuiz
A strategic geography quiz game — 8 countries, 8 categories, the lowest score wins.
**Languages:** FR | EN | UA | DE

View file

@ -158,8 +158,90 @@ h1 { font-size:2.8em; font-weight:800; line-height:1; margin-bottom:6px; }
.cat-info-btn { display:inline-flex; align-items:center; justify-content:center; width:17px; height:17px; border-radius:50%; background:rgba(79,195,255,0.12); border:1px solid rgba(79,195,255,0.3); color:var(--accent3); font-family:'DM Mono',monospace; font-size:0.65em; font-weight:700; cursor:pointer; flex-shrink:0; line-height:1; transition:all 0.15s; user-select:none; }
.cat-info-btn:hover { background:rgba(79,195,255,0.28); border-color:var(--accent3); transform:scale(1.15); }
/* Groupe des boutons action dans une cat-btn */
.cat-btn-actions {
display: inline-flex;
align-items: center;
gap: 3px;
flex-shrink: 0;
}
/* Bouton de tri ⇅ */
.cat-sort-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 17px;
height: 17px;
border-radius: 50%;
background: rgba(79,255,176,0.1);
border: 1px solid rgba(79,255,176,0.25);
color: var(--accent);
font-family: 'DM Mono', monospace;
font-size: 0.72em;
font-weight: 700;
cursor: pointer;
flex-shrink: 0;
line-height: 1;
transition: all 0.15s;
user-select: none;
}
.cat-sort-btn:hover {
background: rgba(79,255,176,0.28);
border-color: var(--accent);
transform: scale(1.15);
}
/* Bouton sort dans reverse-cat-info-row */
.reverse-sort-btn {
/* Match the visual language of reverse-info-toggle: small rounded pill */
font-family: 'DM Mono', monospace;
font-size: 0.65em;
color: var(--accent3);
border: 1px solid rgba(79,195,255,0.35);
background: rgba(79,195,255,0.08);
border-radius: 99px;
padding: 3px 10px;
cursor: pointer;
transition: all 0.15s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.reverse-sort-btn:hover { background: rgba(79,195,255,0.2); }
/* Hardcore */
body.hardcore .cat-sort-btn {
background: rgba(255,61,90,0.1);
border-color: rgba(255,61,90,0.25);
color: #ff3d5a;
}
body.hardcore .cat-sort-btn:hover {
background: rgba(255,61,90,0.25);
border-color: #ff3d5a;
}
/* Custom mode */
body.custom-mode .cat-sort-btn {
background: rgba(167,139,250,0.1);
border-color: rgba(167,139,250,0.25);
color: var(--accent3);
}
body.custom-mode .cat-sort-btn:hover {
background: rgba(167,139,250,0.28);
border-color: var(--accent3);
}
/* Tooltip sort — variante couleur */
#cat-tooltip[data-mode="sort"] {
border-color: var(--accent);
}
#cat-tooltip[data-mode="sort"] .tt-title {
color: var(--accent);}
/* tooltip */
#cat-tooltip { position:fixed; z-index:300; max-width:280px; background:var(--surface); border:1px solid var(--accent3); border-radius:10px; padding:12px 14px; box-shadow:0 8px 32px rgba(0,0,0,0.5); pointer-events:none; opacity:0; transform:translateY(4px) scale(0.97); transition:opacity 0.15s ease,transform 0.15s ease; }
#cat-tooltip { position:fixed; z-index:300; max-width:280px; background:var(--surface); border:1px solid var(--accent3); border-radius:10px; padding:12px 14px; box-shadow:0 8px 32px rgba(0,0,0,0.5); pointer-events:none; opacity:0; transform:translateY(4px) scale(0.97); transition:opacity 0.08s ease,transform 0.08s ease; }
#cat-tooltip.visible { opacity:1; transform:translateY(0) scale(1); pointer-events:auto; }
.tt-title { font-size:0.8em; font-weight:700; color:var(--accent3); margin-bottom:6px; }
.tt-body { font-size:0.78em; color:var(--muted); line-height:1.5; font-family:'DM Mono',monospace; }
@ -582,13 +664,21 @@ body.reverse-mode .cats-grid { display:none !important; }
border-radius:99px; padding:3px 10px; cursor:pointer; transition:all 0.15s;
}
.reverse-info-toggle:hover { background:rgba(79,195,255,0.2); }
.reverse-sort-btn .rsq { font-weight:700; margin-left:4px; opacity:0.95; }
.reverse-cat-tooltip {
display:none; margin-top:8px; text-align:left;
font-family:'DM Mono',monospace; font-size:0.68em; color:var(--muted); line-height:1.5;
background:rgba(79,195,255,0.06); border:1px solid rgba(79,195,255,0.2);
border-radius:8px; padding:8px 10px;
opacity: 0;
transform: translateY(4px) scale(0.97);
transition: opacity 0.08s ease, transform 0.08s ease;
}
.reverse-cat-tooltip.open {
display:block;
opacity: 1;
transform: translateY(0) scale(1);
}
.reverse-cat-tooltip.open { display:block; }
/* ── Country buttons grid (reverse mode) ── */
#countries-grid {
@ -1064,22 +1154,177 @@ body.reverse-mode #countries-grid { display:grid; }
/* ── Appareils tactiles : targets min 44px, no hover ── */
@media (pointer: coarse) {
.cat-btn { min-height: 46px; }
.btn-primary, .btn-hardcore, .btn-reverse,
.btn-daily, .btn-custom { min-height: 48px; }
.btn-secondary { min-height: 44px; }
.draft-pill, .submode-pill { min-height: 58px; }
.lang-btn { min-height: 30px; min-width: 34px; }
.mob-ribbon-action { min-height: 40px; }
/* Supprimer les hover-transforms qui "collent" sur touch */
.btn-primary:hover, .btn-hardcore:hover,
.btn-daily:hover, .btn-reverse:hover { transform: none; }
.cat-btn:hover:not(:disabled) { transform: none; }
.ribbon-item:hover { background: transparent; color: var(--muted); }
/* ─── CAT BTN ACTIONS (⇅ + i) ─────────────────────────────────────────────── */
.cat-btn-actions {
display: flex;
align-items: center;
gap: 3px;
flex-shrink: 0;
}
/* Base style shared by both mini-buttons */
.cat-sort-btn,
.cat-info-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
font-family: 'DM Mono', monospace;
font-size: 0.72em;
font-weight: 700;
cursor: pointer;
transition: transform 0.15s, background 0.15s, opacity 0.15s;
user-select: none;
-webkit-user-select: none;
/* Larger tap target via padding so finger doesn't miss */
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.cat-info-btn {
background: rgba(79, 255, 176, 0.1);
border: 1px solid rgba(79, 255, 176, 0.35);
color: var(--accent);
}
.cat-info-btn:hover { transform: scale(1.15); background: rgba(79, 255, 176, 0.2); }
.cat-sort-btn {
background: rgba(79, 255, 176, 0.08);
border: 1px solid rgba(79, 255, 176, 0.28);
color: var(--accent);
}
.cat-sort-btn:hover { transform: scale(1.15); background: rgba(79, 255, 176, 0.18); }
/* Hardcore */
body.hardcore .cat-info-btn,
body.hardcore .cat-sort-btn {
border-color: rgba(255, 61, 90, 0.4);
color: #ff3d5a;
background: rgba(255, 61, 90, 0.08);
}
body.hardcore .cat-info-btn:hover,
body.hardcore .cat-sort-btn:hover {
background: rgba(255, 61, 90, 0.18);
}
/* Custom/reverse accent */
body.custom-mode .cat-info-btn,
body.custom-mode .cat-sort-btn,
body.reverse-mode .cat-info-btn,
body.reverse-mode .cat-sort-btn {
border-color: rgba(var(--accent3-rgb, 180, 100, 255), 0.4);
color: var(--accent3, #b464ff);
background: rgba(var(--accent3-rgb, 180, 100, 255), 0.08);
}
/* ─── REVERSE SORT BTN ─────────────────────────────────────────────────────── */
.reverse-sort-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 3px 10px;
border-radius: 20px;
font-family: 'DM Mono', monospace;
font-size: 0.8em;
font-weight: 700;
cursor: pointer;
background: rgba(79, 255, 176, 0.08);
border: 1px solid rgba(79, 255, 176, 0.3);
color: var(--accent);
transition: transform 0.15s, background 0.15s;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.reverse-sort-btn:hover { transform: scale(1.1); background: rgba(79, 255, 176, 0.18); }
/* ─── TOOLTIP ──────────────────────────────────────────────────────────────── */
#cat-tooltip {
pointer-events: none; /* disable pointer by default to avoid blocking taps on mobile */
z-index: 9999;
}
#cat-tooltip[data-mode="sort"] {
border-color: var(--accent);
box-shadow: 0 0 18px rgba(79, 255, 176, 0.2);
}
/* ─── MOBILE OVERRIDES ─────────────────────────────────────────────────────── */
@media (max-width: 600px), (pointer: coarse) {
/* Bigger tap zones — invisible padding trick */
.cat-sort-btn,
.cat-info-btn {
width: 28px;
height: 28px;
font-size: 0.85em;
/* extend tap area beyond visible circle */
padding: 6px;
box-sizing: border-box;
/* keep layout stable and increase tappable area without negative margins */
}
.cat-btn-actions {
gap: 8px;
}
/* Reverse sort pill also bigger */
.reverse-sort-btn {
padding: 3px 10px;
font-size: 0.65em;
min-height: auto;
}
/* Tooltip: fixed to bottom of viewport on mobile so it's always readable */
#cat-tooltip.visible {
position: fixed !important;
bottom: 16px !important;
top: auto !important;
left: 50% !important;
transform: translateX(-50%);
width: calc(100vw - 32px) !important;
max-width: 340px;
border-radius: 14px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
pointer-events: auto; /* allow interacting with tooltip content when visible */
}
}
/* Enable tooltip interactions on non-touch devices (hover-capable) */
@media (hover: hover) and (pointer: fine) {
#cat-tooltip { pointer-events: auto; }
}
/* On mobile in reverse mode, position reverse-cat-tooltip the same as cat-tooltip (bottom fixed) for consistency */
@media (max-width: 600px), (pointer: coarse) {
.reverse-cat-tooltip.open {
display: block !important;
position: fixed !important;
bottom: 16px !important;
top: auto !important;
left: 50% !important;
/* start slightly lower and slide up for a smoother effect */
transform: translateX(-50%) translateY(10px);
width: calc(100vw - 32px) !important;
max-width: 340px;
border-radius: 14px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
margin-top: 0 !important;
padding: 12px 14px;
z-index: 9999;
}
/* transition for mobile bottom slide */
.reverse-cat-tooltip {
transition: transform 0.18s cubic-bezier(.2,.9,.2,1), opacity 0.14s ease, max-height 0.18s ease;
}
.reverse-cat-tooltip.open {
transform: translateX(-50%) translateY(0) !important;
opacity: 1;
}
}
/* ── Scroll fluide iOS ─────────────────────────────────── */
#panel-custom, .ribbon-list,
.daily-modal-box, #daily-lb-overlay .daily-modal-box {
-webkit-overflow-scrolling: touch;
}
}}

View file

@ -321,7 +321,7 @@
</button>
<button class="preset-btn" onclick="applyPreset('islands',this)">
<span class="p-icon">🏝️</span>
<span class="p-name"> data-i18n="presetIslands"></span>
<span class="p-name" data-i18n="presetIslands"></span>
</button>
<button class="preset-btn" onclick="applyPreset('flag-guesser',this)">
<span class="p-icon">🚩</span>
@ -355,7 +355,8 @@
<div class="reverse-cat-icon" id="reverse-cat-icon">?</div>
<div class="reverse-cat-name" id="reverse-cat-name"></div>
<div class="reverse-cat-info-row">
<span class="reverse-info-toggle" onclick="toggleReverseCatInfo()" data-i18n="reverseInfoBtn"></span>
<span class="reverse-sort-btn" id="reverse-sort-btn" data-catid=""><span class="rsq">?</span></span>
<span class="reverse-info-toggle" onclick="toggleReverseCatInfo()" data-i18n="reverseInfoBtn"></span>
</div>
<div class="reverse-cat-tooltip" id="reverse-cat-tooltip"></div>
</div>

View file

@ -68,7 +68,6 @@ function showCountdown(onDone) {
numEl.className = 'countdown-num';
void numEl.offsetWidth;
numEl.textContent = counts[i];
// On GO: show draft emoji as subtitle
if (i === counts.length - 1) {
subEl.textContent = lastDraftUsed === 'countries' ? '🌍' : '📊';
} else {
@ -102,7 +101,6 @@ function startGame(modeOrSeed, seedOverride) {
seedOverride = modeOrSeed;
}
// Apply N_TURNS and time per mode
document.body.classList.remove('hardcore', 'custom-mode', 'reverse-mode');
if (gameMode === 'custom') {
if (!seedOverride) {
@ -112,7 +110,6 @@ function startGame(modeOrSeed, seedOverride) {
document.body.classList.add('custom-mode');
document.getElementById('panel-custom').classList.add('hidden');
document.getElementById('panel-setup').classList.add('hidden');
// Apply sub-mode
if (customSubMode === 'hardcore') {
document.body.classList.add('hardcore');
} else if (customSubMode === 'reverse') {
@ -134,7 +131,6 @@ function startGame(modeOrSeed, seedOverride) {
var seed = seedOverride || generateSeed();
if (!applyGameFromSeed(seed)) return;
// Sync N_TURNS to actual game size (important when loading a seed)
N_TURNS = gameCountries.length;
currentSeed = seed;
currentStep = 0;
@ -144,11 +140,9 @@ function startGame(modeOrSeed, seedOverride) {
isRevealing = false;
if (typeof resetHints === 'function') resetHints();
// Show countdown, then launch
showCountdown(function() {
document.getElementById('panel-game').classList.remove('hidden');
document.getElementById('game-seed-display').textContent = currentSeed;
// Mettre à jour le hash URL pour permettre le partage
history.replaceState(null, '', '#seed=' + encodeURIComponent(currentSeed));
renderCategoryButtons();
showTurn();
@ -158,10 +152,7 @@ function startGame(modeOrSeed, seedOverride) {
function startWithSeedInput() {
var raw = document.getElementById('seed-input').value.trim();
if (!raw) { alert(t('enterSeed')); return; }
// Support ancien format legacy (12-45_0-5) et nouveau format Base62
var seed = raw;
// Si l'utilisateur a collé du texte avec une seed entourée d'espaces, on nettoie
seed = seed.replace(/\s+/g, '');
var seed = raw.replace(/\s+/g, '');
startGame('custom', seed);
}
@ -169,7 +160,10 @@ function startWithSeedInput() {
function makeCatBtnHTML(cat) {
return '<div class="cat-btn-top">'
+ '<span class="cat-icon">' + cat.icon + '</span>'
+ '<span class="cat-btn-actions">'
+ '<span class="cat-sort-btn" data-catid="' + cat.id + '">⇅</span>'
+ '<span class="cat-info-btn" data-catid="' + cat.id + '">i</span>'
+ '</span>'
+ '</div>'
+ '<span class="cat-label-text">' + catName(cat) + '</span>';
}
@ -184,6 +178,7 @@ function renderCategoryButtons() {
btn.innerHTML = makeCatBtnHTML(cat);
btn.addEventListener('click', function(e) {
if (e.target.closest('.cat-info-btn')) return;
if (e.target.closest('.cat-sort-btn')) return;
handleChoice(cat.id);
});
grid.appendChild(btn);
@ -191,10 +186,7 @@ function renderCategoryButtons() {
attachInfoListeners();
}
// ─── RERENDER CURRENT TURN ON LANG CHANGE ─────────────────────────────────────
// Appelé par setLang() pour mettre à jour immédiatement le tour en cours
// sans attendre le prochain tour.
function rerenderCurrentTurn() {
var inGame = (document.getElementById('panel-game') &&
!document.getElementById('panel-game').classList.contains('hidden'));
@ -205,7 +197,6 @@ function rerenderCurrentTurn() {
(gameMode === 'custom' && customSubMode === 'reverse'));
if (isReverse) {
// ── Reverse : mettre à jour la catégorie affichée ─────────────────────
if (currentStep < gameCategories.length) {
var cat = gameCategories[currentStep];
document.getElementById('reverse-cat-name').textContent = catName(cat);
@ -214,14 +205,12 @@ function rerenderCurrentTurn() {
t('reverseTurn') + ' ' + (currentStep + 1) + ' ' + t('of') + ' ' + gameCategories.length;
}
} else {
// ── Normal / Hardcore / Custom ─────────────────────────────────────────
if (currentStep < gameCountries.length) {
var c = gameCountries[currentStep];
document.getElementById('country-name').textContent = getCountryName(c);
document.getElementById('turn-label').textContent =
t('turn') + ' ' + (currentStep + 1) + ' ' + t('of') + ' ' + gameCountries.length;
// Hint buttons : état selon s'ils ont été révélés ou non
var btn1 = document.getElementById('hint-btn-1');
var btn2 = document.getElementById('hint-btn-2');
if (btn1 && !btn1.classList.contains('revealed')) {
@ -231,7 +220,6 @@ function rerenderCurrentTurn() {
btn2.innerHTML = '\uD83D\uDD0D ' + t('hint2btn');
}
// Labels des hints déjà révélés (titre de section)
if (hintState.hint1Revealed) {
var lbl1 = document.getElementById('hint-label-1');
if (lbl1) lbl1.innerHTML = t('hintLabel1') + ' <span class="cost-badge cost-badge-1">-25 pts</span>';
@ -258,19 +246,279 @@ function rerenderCategoryButtonNames() {
attachInfoListeners();
}
function attachInfoListeners() {
document.querySelectorAll('.cat-info-btn').forEach(function(el) {
var clone = el.cloneNode(true);
el.parentNode.replaceChild(clone, el);
clone.addEventListener('mouseenter', function() { showTooltip(clone.dataset.catid, clone); });
clone.addEventListener('mouseleave', hideTooltip);
// ─── TOOLTIP SYSTEM ──────────────────────────────────────────────────────────
// Desktop : mouseenter shows, mouseleave schedules hide (counter-based to
// survive rapid i→⇅ transitions without flicker).
// Mobile : purely tap-to-toggle. mouseenter/mouseleave are IGNORED on touch
// devices because they fire unreliably after touchend and cause the
// flash-then-close bug. A global touchstart listener on the document
// closes any open tooltip when the user taps outside it.
var _tooltipHideTimer = null;
var _mouseOverTooltipUI = 0; // counter for desktop hover zone
// after hiding, suppress any re-opening for a short window to avoid ghost flashes
var _tooltipSuppressUntil = 0;
// Detect touch device once (re-evaluated each call is fine too)
function _isTouchDevice() {
return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
}
function _tooltipUIEnter() {
_mouseOverTooltipUI++;
_cancelTooltipHide();
}
function _tooltipUILeave() {
_mouseOverTooltipUI--;
_cancelTooltipHide();
_tooltipHideTimer = setTimeout(function() {
if (_mouseOverTooltipUI <= 0) {
_mouseOverTooltipUI = 0;
hideTooltip();
}
}, 60);
}
function _cancelTooltipHide() {
if (_tooltipHideTimer) { clearTimeout(_tooltipHideTimer); _tooltipHideTimer = null; }
}
function hideTooltip() {
_cancelTooltipHide();
_mouseOverTooltipUI = 0;
var tip = document.getElementById('cat-tooltip');
if (!tip) return;
tip.classList.remove('visible');
tip.dataset.openedBy = '';
tip.dataset.mode = '';
// Also close reverse info tooltip for consistency
var reverseTt = document.getElementById('reverse-cat-tooltip');
if (reverseTt) reverseTt.classList.remove('open');
// blur any focused element to clear :active styling on buttons
if (document.activeElement && document.activeElement.classList.contains('reverse-sort-btn')) {
document.activeElement.blur();
}
// suppress immediate reopening (e.g. synthetic click) for 400ms
_tooltipSuppressUntil = Date.now() + 400;
}
function _positionTooltip(tip, anchor) {
// On mobile: center horizontally on screen, appear above the button
// so it's not hidden under the finger.
var rect = anchor.getBoundingClientRect();
var tw = Math.min(280, window.innerWidth - 16);
var left, top;
if (_isTouchDevice()) {
left = (window.innerWidth - tw) / 2;
// prefer above; fall back to below if not enough room
top = rect.top - 8 + window.scrollY;
if (top - 120 < window.scrollY) {
top = rect.bottom + 8 + window.scrollY;
} else {
top = rect.top - 8 + window.scrollY; // will be shifted up by CSS transform
}
} else {
left = rect.left + rect.width / 2 - tw / 2;
top = rect.bottom + 8 + window.scrollY;
left = Math.max(8, Math.min(left, window.innerWidth - tw - 8));
}
tip.style.left = left + 'px';
tip.style.top = top + 'px';
tip.style.width = tw + 'px';
}
// Helper to show the shared cat-tooltip; we keep the old positioning logic but
// ensure the bubble is hidden while we measure its height/width so that the CSS
// transition never fires prematurely (this was causing the "ghost" flash on mobile).
function _showCatTooltip(catId, anchor, mode) {
// suppress reopening if it was just hidden a moment ago
if (Date.now() < _tooltipSuppressUntil) return;
var cat = gameCategories.find(function(c) { return c.id === catId; });
if (!cat) return;
var tip = document.getElementById('cat-tooltip');
_cancelTooltipHide();
tip.dataset.mode = mode;
tip.dataset.openedBy = mode + '-' + catId;
if (mode === 'info') {
document.getElementById('tt-title').textContent = cat.icon + ' ' + catName(cat);
document.getElementById('tt-body').textContent = catDesc(cat);
} else {
document.getElementById('tt-title').innerHTML = '⇅ ' + catName(cat);
document.getElementById('tt-body').textContent = catSort(cat);
}
// temporarily hide the tip off-screen while we compute its size so the
// CSS transition can't flash; this mirrors the logic added to hints.js.
tip.style.visibility = 'hidden';
tip.style.position = 'fixed';
tip.style.top = '-9999px';
tip.classList.add('visible');
var tipH = tip.offsetHeight;
tip.classList.remove('visible');
tip.style.visibility = '';
// Only position on desktop; on mobile, CSS .visible handles all positioning
if (!_isTouchDevice()) {
_positionTooltip(tip, anchor);
}
tip.classList.add('visible');
}
function showTooltip(catId, anchor) {
_showCatTooltip(catId, anchor, 'info');
}
function showSortTooltip(catId, anchor) {
// If we're in reverse mode, show the info-style dropdown (reverse-cat-tooltip)
var isReverse = (gameMode === 'reverse' || (gameMode === 'custom' && customSubMode === 'reverse'));
if (isReverse) {
// close floating cat-tooltip if present
var floating = document.getElementById('cat-tooltip');
if (floating) { floating.classList.remove('visible'); floating.dataset.openedBy = ''; floating.dataset.mode = ''; }
var el = document.getElementById('reverse-cat-tooltip');
var cat = gameCategories.find(function(c) { return c.id === catId; });
if (!el || !cat) return;
// toggle behavior: if already open for this sort, close it
if (el.classList.contains('open') && el.dataset.openedBy === 'sort-' + catId) {
el.classList.remove('open'); el.dataset.openedBy = '';
} else {
el.dataset.openedBy = 'sort-' + catId;
el.textContent = catSort(cat);
el.classList.add('open');
}
return;
}
_showCatTooltip(catId, anchor, 'sort');
}
// Close tooltip when tapping outside on mobile
function _initGlobalTouchClose() {
if (document._tooltipTouchCloseAttached) return;
document._tooltipTouchCloseAttached = true;
// Close tooltips when tapping outside on mobile
document.addEventListener('touchstart', function(e) {
var tip = document.getElementById('cat-tooltip');
if (tip && tip.classList.contains('visible')) {
// If touch is on the tooltip itself → do nothing (let it stay open)
if (tip.contains(e.target)) return;
// If touch is on a tooltip-trigger button → the button handler will deal with it
if (e.target.closest('.cat-info-btn') || e.target.closest('.cat-sort-btn') ||
e.target.closest('.reverse-sort-btn')) return;
hideTooltip();
}
}, { passive: true });
// Also close reverse info tooltip when clicking outside in reverse mode
document.addEventListener('click', function(e) {
var reverseTt = document.getElementById('reverse-cat-tooltip');
if (!reverseTt || !reverseTt.classList.contains('open')) return;
// If click is on the tooltip itself → do nothing (let it stay open)
if (reverseTt.contains(e.target)) return;
// If click is on the info button → the button handler will deal with it
if (e.target.closest('.reverse-info-toggle')) return;
reverseTt.classList.remove('open');
}, { passive: true });
}
// Helper: attach touch-and-click to a tooltip trigger button
// showFn(catId, anchor) — the function that populates + shows the tooltip
// openedByKey — the string stored in tip.dataset.openedBy when this btn is active
function _attachTooltipTrigger(clone, openedByKey, showFn) {
var touch = _isTouchDevice();
if (touch) {
// ── MOBILE: tap-to-toggle only ────────────────────────────────────────
clone.addEventListener('touchend', function(e) {
e.preventDefault(); // prevent ghost click + page scroll
e.stopPropagation();
var tip = document.getElementById('cat-tooltip');
if (tip.classList.contains('visible') && tip.dataset.openedBy === openedByKey) {
hideTooltip();
} else {
showFn(clone.dataset.catid, clone);
}
});
} else {
// ── DESKTOP: hover + click-to-toggle ─────────────────────────────────
clone.addEventListener('mouseenter', function() {
_tooltipUIEnter();
showFn(clone.dataset.catid, clone);
});
clone.addEventListener('mouseleave', _tooltipUILeave);
clone.addEventListener('click', function(e) {
e.stopPropagation();
var tip = document.getElementById('cat-tooltip');
if (tip.classList.contains('visible')) hideTooltip();
else showTooltip(clone.dataset.catid, clone);
if (tip.classList.contains('visible') && tip.dataset.openedBy === openedByKey) {
hideTooltip();
} else {
showFn(clone.dataset.catid, clone);
}
});
}
}
// Variant that only responds to explicit clicks/taps no hover logic.
// Used for reverse mode: click on desktop, tap on mobile (same as normal mode on mobile, but no hover on desktop).
function _attachTooltipClickOnly(el, openedByKey, showFn) {
if (_isTouchDevice()) {
// ── MOBILE: exactly like normal mode (tap-to-toggle) ────────────────
el.addEventListener('touchend', function(e) {
e.preventDefault();
e.stopPropagation();
var tip = document.getElementById('cat-tooltip');
if (tip.classList.contains('visible') && tip.dataset.openedBy === openedByKey) {
hideTooltip();
} else {
showFn(el.dataset.catid, el);
}
});
} else {
// ── DESKTOP: click-only, no hover ──────────────────────────────────
el.addEventListener('click', function(e) {
e.stopPropagation();
var tip = document.getElementById('cat-tooltip');
if (tip.classList.contains('visible') && tip.dataset.openedBy === openedByKey) {
hideTooltip();
} else {
showFn(el.dataset.catid, el);
}
});
}
}
function attachInfoListeners() {
_initGlobalTouchClose();
// ── Info buttons (i) ──────────────────────────────────────────────────────
document.querySelectorAll('.cat-info-btn').forEach(function(el) {
var clone = el.cloneNode(true);
el.parentNode.replaceChild(clone, el);
_attachTooltipTrigger(clone, 'info-' + clone.dataset.catid, showTooltip);
});
// ── Sort buttons (⇅) ─────────────────────────────────────────────────────
document.querySelectorAll('.cat-sort-btn').forEach(function(el) {
var clone = el.cloneNode(true);
el.parentNode.replaceChild(clone, el);
_attachTooltipTrigger(clone, 'sort-' + clone.dataset.catid, showSortTooltip);
});
// ── Tooltip bubble: keep open while hovered (desktop only) ───────────────
var tip = document.getElementById('cat-tooltip');
if (!tip.dataset.listenerAttached) {
tip.dataset.listenerAttached = '1';
if (!_isTouchDevice()) {
tip.addEventListener('mouseenter', _tooltipUIEnter);
tip.addEventListener('mouseleave', _tooltipUILeave);
}
// On touch: tapping inside the tooltip does nothing (global handler ignores it)
}
}
// ─── TURN DISPLAY ─────────────────────────────────────────────────────────────
@ -304,12 +552,12 @@ function showTurn() {
btn.innerHTML = makeCatBtnHTML(cat);
btn.style.borderColor = '';
btn.style.color = '';
// re-bind click
var newBtn = btn.cloneNode(true);
btn.parentNode.replaceChild(newBtn, btn);
(function(catId) {
newBtn.addEventListener('click', function(e) {
if (e.target.closest('.cat-info-btn')) return;
if (e.target.closest('.cat-sort-btn')) return;
handleChoice(catId);
});
})(cat.id);
@ -326,30 +574,57 @@ function showTurn() {
// ─── REVERSE MODE TURN ────────────────────────────────────────────────────────
function showTurnReverse() {
// In reverse: currentStep = which category is active
// All countries shown as buttons; user picks which country gets this category
// make sure any open cat-tooltip from normal mode is shut, removing ghost flashes
hideTooltip();
var cat = gameCategories[currentStep];
document.getElementById('turn-label').textContent = t('reverseTurn') + ' ' + (currentStep+1) + ' ' + t('of') + ' ' + gameCategories.length;
document.getElementById('total-score').textContent = totalScore;
// Show the active category
document.getElementById('reverse-cat-icon').textContent = cat.icon;
applyEmoji(document.getElementById('reverse-cat-icon'));
document.getElementById('reverse-cat-name').textContent = catName(cat);
document.getElementById('reverse-cat-tooltip').textContent = catDesc(cat);
document.getElementById('reverse-cat-tooltip').classList.remove('open');
// Update reverse sort button
var sortBtn = document.getElementById('reverse-sort-btn');
if (sortBtn) {
sortBtn.dataset.catid = cat.id;
var newSortBtn = sortBtn.cloneNode(true);
sortBtn.parentNode.replaceChild(newSortBtn, sortBtn);
// reverse mode should use click-only tooltips to avoid hover activation
if (gameMode === 'reverse') {
_attachTooltipClickOnly(newSortBtn, 'sort-' + cat.id, showSortTooltip);
} else {
_attachTooltipTrigger(newSortBtn, 'sort-' + cat.id, showSortTooltip);
}
}
renderCountryButtons();
clearFeedback();
startTimer();
}
function toggleReverseCatInfo() {
// Close the sort tooltip before toggling info
var sortTt = document.getElementById('cat-tooltip');
if (sortTt && sortTt.classList.contains('visible')) {
sortTt.classList.remove('visible');
sortTt.dataset.openedBy = '';
sortTt.dataset.mode = '';
}
// Now toggle the info tooltip
var el = document.getElementById('reverse-cat-tooltip');
el.classList.toggle('open');
// refresh text in case lang changed
var cat = gameCategories[currentStep];
if (cat) el.textContent = catDesc(cat);
if (!el) return;
if (el.classList.contains('open') && el.dataset.openedBy === 'info-' + (cat ? cat.id : '')) {
el.classList.remove('open');
el.dataset.openedBy = '';
} else {
el.dataset.openedBy = 'info-' + (cat ? cat.id : '');
el.textContent = cat ? catDesc(cat) : '';
el.classList.add('open');
}
}
function renderCountryButtons() {
@ -402,7 +677,6 @@ function handleCountryChoice(countryIdx) {
reverseAssignments[countryIdx] = cat.id;
isRevealing = true;
// Mark chosen button
document.querySelectorAll('.country-btn').forEach(function(b) { b.disabled = true; });
btn.style.borderColor = '#f5c842';
btn.style.boxShadow = '0 0 14px rgba(245,200,66,0.35)';
@ -423,7 +697,6 @@ function handleCountryChoice(countryIdx) {
}
function autoPick_reverse() {
// Timeout in reverse: penalty, assign to first available country
var firstFree = -1;
for (var i = 0; i < gameCountries.length; i++) {
if (!reverseAssignments[i]) { firstFree = i; break; }
@ -471,7 +744,6 @@ function autoPick_reverse() {
function startTimer() {
if (timerInterval && timerInterval._raf) timerInterval._raf(); timerInterval = null;
// Infinite time: show ∞, no countdown, no autopick
if (TIME_PER_TURN === 0) {
var numEl2 = document.getElementById('timer-num');
var fillEl2 = document.getElementById('progress-fill');
@ -513,7 +785,6 @@ function startTimer() {
function updateTimerUI(remaining) {
var numEl = document.getElementById('timer-num');
var fillEl = document.getElementById('progress-fill');
// remaining can be undefined on first call
var pct = (remaining !== undefined)
? (remaining / TIME_PER_TURN * 100)
: 100;
@ -523,7 +794,6 @@ function updateTimerUI(remaining) {
var urgent = displayNum <= (isHardcoreActive() ? 4 : 8);
numEl.classList.toggle('urgent', urgent);
fillEl.classList.toggle('urgent', urgent);
// Hardcore danger pulse
if (isHardcoreActive()) {
numEl.classList.toggle('danger', displayNum <= 4);
} else {
@ -546,13 +816,11 @@ function autoPick() {
var flag = country.flag || country.emoji || '🌍';
gameLog.push({flag:flag, name:cName, catObj:null, points:PENALTY, isPenalty:true, country:country});
// HC: no overlay, advance silently
if (isHardcoreActive()) {
setTimeout(function() { isRevealing = false; currentStep++; showTurn(); }, 350);
return;
}
// Normal mode: show penalty overlay
document.getElementById('total-score').textContent = totalScore;
var overlay = document.createElement('div');
overlay.id = 'reveal-overlay';
@ -637,11 +905,9 @@ function disableAllCatButtons() {
// ─── STAR RATING HELPERS ─────────────────────────────────────────────────────
function computeStarData(country, chosenCatId) {
// Le jeu = MINIMISER le score : etoile = valeur la plus BASSE choisie
var chosenRaw = country[chosenCatId];
var chosenVal = (chosenRaw === false || chosenRaw === null || chosenRaw === undefined || isNaN(Number(chosenRaw))) ? null : Number(chosenRaw);
if (chosenVal === null) return { chosenVal:0, gameBest:0, globalBest:0, isGameBest:false, isGlobalBest:false };
// gameBest = valeur minimale parmi les categories disponibles pour ce pays
var gb = Infinity;
gameCategories.forEach(function(cat) {
var v = country[cat.id];
@ -649,7 +915,6 @@ function computeStarData(country, chosenCatId) {
var n = Number(v); if (n < gb) gb = n;
}
});
// globalBest = valeur minimale sur TOUTES les categories connues
var ab = Infinity;
ALL_CATEGORIES.forEach(function(cat) {
var v = country[cat.id];
@ -663,8 +928,8 @@ function computeStarData(country, chosenCatId) {
var isGlobalBest = (chosenVal > 0 && chosenVal <= ab);
return { chosenVal:chosenVal, gameBest:gb, globalBest:ab, isGameBest:isGameBest, isGlobalBest:isGlobalBest };
}
function computeStarDataReverse(cat, chosenIdx) {
// Mode reverse : etoile = pays avec la valeur la plus BASSE pour cette categorie
var country = gameCountries[chosenIdx];
var chosenRaw = country[cat.id];
var chosenVal = (chosenRaw===false||chosenRaw===null||chosenRaw===undefined||isNaN(Number(chosenRaw))) ? null : Number(chosenRaw);
@ -689,7 +954,7 @@ function computeStarDataReverse(cat, chosenIdx) {
var isGlobalBest = (chosenVal > 0 && chosenVal <= ab);
return { chosenVal:chosenVal, gameBest:gb, globalBest:ab, isGameBest:isGameBest, isGlobalBest:isGlobalBest };
}
// Panneau 'meilleurs choix' : trier par valeur ASCENDANTE (bas = meilleur score)
function buildBestCatsPanel(country, chosenCatId) {
var rows=[];
gameCategories.forEach(function(cat){var v=country[cat.id];if(v!==false&&v!==null&&v!==undefined&&!isNaN(Number(v)))rows.push({cat:cat,val:Number(v)});});
@ -699,6 +964,7 @@ function buildBestCatsPanel(country, chosenCatId) {
return '<div class="reveal-best-row"><span class="rbr-cat">'+r.cat.icon+' '+catName(r.cat)+mark+'</span><span class="rbr-val">'+r.val+'</span></div>';
}).join('');
}
function buildBestCountriesPanel(cat, chosenIdx) {
var rows=[];
gameCountries.forEach(function(c,i){var v=c[cat.id];if(v!==false&&v!==null&&v!==undefined&&!isNaN(Number(v)))rows.push({c:c,i:i,val:Number(v)});});
@ -709,6 +975,7 @@ function buildBestCountriesPanel(cat, chosenIdx) {
return '<div class="reveal-best-row"><span class="rbr-cat">'+flag+' '+getCountryName(r.c)+mark+'</span><span class="rbr-val">'+r.val+'</span></div>';
}).join('');
}
function showReveal(flag, cName, catObj, finalValue, onDone, starData, bestPanelHtml) {
var overlay = document.createElement('div');
overlay.id = 'reveal-overlay';
@ -835,4 +1102,3 @@ function clearFeedback() {
var el = document.getElementById('feedback');
el.textContent = ''; el.className = 'feedback';
}

View file

@ -12,11 +12,15 @@ function showTooltip(catId, targetEl) {
var left = Math.max(8, Math.min(rect.left + rect.width/2 - tipWidth/2, window.innerWidth - tipWidth - 8));
tip.style.width = tipWidth + 'px';
tip.style.left = left + 'px';
/* Measure height without triggering transition: use offsetHeight with visibility hidden + position off-screen */
tip.style.visibility = 'hidden';
tip.classList.add('visible');
tip.style.position = 'fixed';
tip.style.top = '-9999px';
tip.classList.add('visible'); /* Add class to get full computed size */
var tipH = tip.offsetHeight;
tip.classList.remove('visible');
tip.classList.remove('visible'); /* Remove before showing to avoid flash */
tip.style.visibility = '';
/* Now set correct position and show with transition */
tip.style.top = (rect.top - 8 - tipH < 8) ? (rect.bottom + 8) + 'px' : (rect.top - 8 - tipH) + 'px';
clearTimeout(tooltipTimeout);
tip.classList.add('visible');
@ -24,9 +28,8 @@ function showTooltip(catId, targetEl) {
function hideTooltip() {
clearTimeout(tooltipTimeout);
tooltipTimeout = setTimeout(function() {
document.getElementById('cat-tooltip').classList.remove('visible');
}, 120);
/* Remove visible class immediately to avoid flash on rapid clicks */
document.getElementById('cat-tooltip').classList.remove('visible');
}
document.addEventListener('click', function(e) {

View file

@ -2,6 +2,15 @@
var customTimeSelected = 20;
var customSubMode = 'normal'; // 'normal' | 'hardcore' | 'reverse'
// list of category IDs used by the “Sport Enjoyer” preset; keeps only
// rugby (men), football men/women and mens basketball.
var SPORT_CAT_IDS = [
'classement_rugby_H',
'classement_fifa_H',
'classement_fifa_F',
'classement_bball_M'
];
function selectSubModePill(mode) {
customSubMode = mode;
['normal','hardcore','reverse'].forEach(function(m) {
@ -77,6 +86,9 @@ function applyPreset(name, btn) {
if (Object.keys(ribbonState).length === 0) ribbonInit();
if (Object.keys(catRibbonState).length === 0) catRibbonInit();
// make sure preset code lists exist (build once from country data)
ensurePresets();
// Helper : set country ribbon by code whitelist
function setCountryByList(allowedCodes) {
for (var i = 0; i < countriesDB.length; i++) {
@ -96,6 +108,70 @@ function applyPreset(name, btn) {
catRibbonSelected = {};
}
// Helper to safely set country list from a global codes array, with a fallback
function safeSetCodes(varName) {
try {
var codes = window[varName];
if (codes && Array.isArray(codes)) {
setCountryByList(codes);
} else {
console.warn('Preset codes not found or invalid:', varName);
resetCountries();
}
} catch (e) {
console.warn('Error accessing preset codes:', varName, e);
resetCountries();
}
resetCats();
setPresetDefaults();
}
// ensurePresets() builds the various *_CODES arrays from the country database
var _presetsDone = false;
function ensurePresets() {
if (_presetsDone) return;
if (!countriesDB || countriesDB.length === 0) return; // can't build yet
_presetsDone = true;
// utility for description text (fr preferred)
function descText(code) {
var d = COUNTRY_DESCRIPTIONS[code];
if (!d) return '';
return (d.fr || d.en || '').toString().toLowerCase();
}
// initialize empty arrays on window
window.EUROPEAN_CODES = [];
window.AFRICAN_CODES = [];
window.AMERICAS_CODES = [];
window.ASIAN_CODES = [];
window.OCEANIA_CODES = [];
window.TOP100_PIB_CODES = [];
window.SMALL_COUNTRIES_CODES = [];
window.FRANCE_NEIGHBORS_CODES = ['BE','LU','DE','CH','IT','ES','AD','MC'];
window.LANDLOCKED_CODES = [];
window.ISLANDS_CODES = [];
countriesDB.forEach(function(c) {
var code = (c.country_code||'').toUpperCase();
var d = descText(code);
if (/afrique/.test(d)) window.AFRICAN_CODES.push(code);
if (/am[eé]rique/.test(d)) window.AMERICAS_CODES.push(code);
if (/asie/.test(d)) window.ASIAN_CODES.push(code);
if (/europ/.test(d)) window.EUROPEAN_CODES.push(code);
if (/oc[eé]an|pacifique/.test(d) || code==='AU' || code==='NZ') window.OCEANIA_CODES.push(code);
if (/île|archipel|island/.test(d)) window.ISLANDS_CODES.push(code);
if (/enclav|landlocked/.test(d)) window.LANDLOCKED_CODES.push(code);
if (typeof c.pib === 'number' && c.pib <= 100) window.TOP100_PIB_CODES.push(code);
if (typeof c.nb_habitant === 'number' && c.nb_habitant >= 200) window.SMALL_COUNTRIES_CODES.push(code);
});
// remove duplicates just in case
['EUROPEAN_CODES','AFRICAN_CODES','AMERICAS_CODES','ASIAN_CODES','OCEANIA_CODES','LANDLOCKED_CODES','ISLANDS_CODES'].forEach(function(varName) {
window[varName] = Array.from(new Set(window[varName]));
});
}
if (name === 'no-russia') {
resetCountries();
for (var i = 0; i < countriesDB.length; i++) {
@ -103,17 +179,16 @@ function applyPreset(name, btn) {
}
resetCats();
setPresetDefaults();
} else if (name === 'europe') { setCountryByList(EUROPEAN_CODES); resetCats(); setPresetDefaults();
} else if (name === 'africa') { setCountryByList(AFRICAN_CODES); resetCats(); setPresetDefaults();
} else if (name === 'americas') { setCountryByList(AMERICAS_CODES); resetCats(); setPresetDefaults();
} else if (name === 'asia') { setCountryByList(ASIAN_CODES); resetCats(); setPresetDefaults();
} else if (name === 'oceania') { setCountryByList(OCEANIA_CODES); resetCats(); setPresetDefaults();
} else if (name === 'top100pib'){ setCountryByList(TOP100_PIB_CODES);resetCats(); setPresetDefaults();
} else if (name === 'small') { setCountryByList(SMALL_COUNTRIES_CODES); resetCats(); setPresetDefaults();
} else if (name === 'france-neighbors') { setCountryByList(FRANCE_NEIGHBORS_CODES); resetCats(); setPresetDefaults();
} else if (name === 'landlocked') { setCountryByList(LANDLOCKED_CODES); resetCats(); setPresetDefaults();
} else if (name === 'islands') { setCountryByList(ISLANDS_CODES); resetCats(); setPresetDefaults();
} else if (name === 'africa') { safeSetCodes('AFRICAN_CODES');
} else if (name === 'americas') { safeSetCodes('AMERICAS_CODES');
} else if (name === 'asia') { safeSetCodes('ASIAN_CODES');
} else if (name === 'europe') { safeSetCodes('EUROPEAN_CODES');
} else if (name === 'oceania') { safeSetCodes('OCEANIA_CODES');
} else if (name === 'top100pib'){ safeSetCodes('TOP100_PIB_CODES');
} else if (name === 'small') { safeSetCodes('SMALL_COUNTRIES_CODES');
} else if (name === 'france-neighbors') { safeSetCodes('FRANCE_NEIGHBORS_CODES');
} else if (name === 'landlocked') { safeSetCodes('LANDLOCKED_CODES');
} else if (name === 'islands') { safeSetCodes('ISLANDS_CODES');
} else if (name === 'flag-guesser') {
resetCountries(); resetCats();
@ -130,7 +205,7 @@ function applyPreset(name, btn) {
} else if (name === 'sport') {
resetCountries();
for (var i = 0; i < ALL_CATEGORIES.length; i++) {
catRibbonState[i] = (SPORT_CAT_IDS.indexOf(ALL_CATEGORIES[i].id) !== -1) ? 'incl' : 'poss';
catRibbonState[i] = (SPORT_CAT_IDS.indexOf(ALL_CATEGORIES[i].id) !== -1) ? 'incl' : 'excl';
}
catRibbonSelected = {};
setPresetDefaults();