Merge pull request #71 from quoije/2026
Refactor control panel and add loading bar to Mes picks
This commit was merged in pull request #71.
This commit is contained in:
+14
-36
@@ -121,11 +121,6 @@
|
|||||||
Année active
|
Année active
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link" id="admin-settings-subtab-player" data-bs-toggle="tab" data-bs-target="#admin-settings-pane-player" type="button" role="tab" aria-controls="admin-settings-pane-player" aria-selected="false">
|
|
||||||
Lecteur
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="admin-settings-subtab-oscar-date" data-bs-toggle="tab" data-bs-target="#admin-settings-pane-oscar-date" type="button" role="tab" aria-controls="admin-settings-pane-oscar-date" aria-selected="false">
|
<button class="nav-link" id="admin-settings-subtab-oscar-date" data-bs-toggle="tab" data-bs-target="#admin-settings-pane-oscar-date" type="button" role="tab" aria-controls="admin-settings-pane-oscar-date" aria-selected="false">
|
||||||
Date Oscars
|
Date Oscars
|
||||||
@@ -161,25 +156,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Player -->
|
|
||||||
<div class="tab-pane fade" id="admin-settings-pane-player" role="tabpanel" aria-labelledby="admin-settings-subtab-player" tabindex="0">
|
|
||||||
<div class="border rounded p-3 bg-light">
|
|
||||||
<div class="d-flex flex-wrap gap-2 align-items-center justify-content-between">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="player_ui_show_status">
|
|
||||||
<label class="form-check-label" for="player_ui_show_status">Afficher “Source” + “Progress” (admins)</label>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="button" id="player_ui_reload" class="btn btn-outline-secondary">Recharger</button>
|
|
||||||
<button type="button" id="player_ui_save" class="btn btn-warning">Enregistrer</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-text mt-2">
|
|
||||||
Contrôle l’affichage du bloc debug dans <code>player.html</code> (admins uniquement).
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Oscar date -->
|
<!-- Oscar date -->
|
||||||
<div class="tab-pane fade" id="admin-settings-pane-oscar-date" role="tabpanel" aria-labelledby="admin-settings-subtab-oscar-date" tabindex="0">
|
<div class="tab-pane fade" id="admin-settings-pane-oscar-date" role="tabpanel" aria-labelledby="admin-settings-subtab-oscar-date" tabindex="0">
|
||||||
<div class="border rounded p-3 bg-light">
|
<div class="border rounded p-3 bg-light">
|
||||||
@@ -254,25 +230,27 @@
|
|||||||
<!-- Visibility settings -->
|
<!-- Visibility settings -->
|
||||||
<div class="tab-pane fade" id="admin-settings-pane-visibility" role="tabpanel" aria-labelledby="admin-settings-subtab-visibility" tabindex="0">
|
<div class="tab-pane fade" id="admin-settings-pane-visibility" role="tabpanel" aria-labelledby="admin-settings-subtab-visibility" tabindex="0">
|
||||||
<div class="border rounded p-3 bg-light">
|
<div class="border rounded p-3 bg-light">
|
||||||
<div class="text-muted small mb-3">
|
<div id="visibility-config-alert" class="alert d-none mb-3" role="alert"></div>
|
||||||
Contrôle la visibilité de certains éléments dans l'interface.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="visibility-config-alert" class="alert d-none" role="alert"></div>
|
<div class="form-check mb-4">
|
||||||
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="visibility-show-picks-features">
|
<input class="form-check-input" type="checkbox" id="visibility-show-picks-features">
|
||||||
<label class="form-check-label" for="visibility-show-picks-features">
|
<label class="form-check-label" for="visibility-show-picks-features">
|
||||||
Afficher les fonctionnalités "Picks"
|
Afficher les fonctionnalités "Picks"
|
||||||
</label>
|
</label>
|
||||||
<div class="form-text">Contrôle l'affichage du bouton "Mes picks" dans la navigation et de la colonne "Bon picks" dans les statistiques.</div>
|
<div class="form-text small text-muted">Contrôle l'affichage du bouton "Mes picks" dans la navigation et de la colonne "Bon picks" dans les statistiques.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="form-check mb-4">
|
||||||
<div class="d-flex gap-2">
|
<input class="form-check-input" type="checkbox" id="player_ui_show_status">
|
||||||
<button type="button" id="visibility-config-reload" class="btn btn-outline-secondary">Recharger</button>
|
<label class="form-check-label" for="player_ui_show_status">
|
||||||
<button type="button" id="visibility-config-save" class="btn btn-warning">Enregistrer</button>
|
Afficher "Source" + "Progress" (admins)
|
||||||
</div>
|
</label>
|
||||||
|
<div class="form-text small text-muted">Contrôle l'affichage du bloc debug dans <code>player.html</code> (admins uniquement).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" id="visibility-config-reload" class="btn btn-outline-secondary">Recharger</button>
|
||||||
|
<button type="button" id="visibility-config-save" class="btn btn-warning">Enregistrer</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+50
-20
@@ -2671,8 +2671,21 @@ window.onload = async function () {
|
|||||||
|
|
||||||
if (visibilityConfigReloadBtn) {
|
if (visibilityConfigReloadBtn) {
|
||||||
visibilityConfigReloadBtn.addEventListener('click', async () => {
|
visibilityConfigReloadBtn.addEventListener('click', async () => {
|
||||||
await loadVisibilityConfig();
|
visibilityConfigReloadBtn.disabled = true;
|
||||||
showResponse('success', 'Configuration rechargée');
|
const oldText = visibilityConfigReloadBtn.textContent;
|
||||||
|
visibilityConfigReloadBtn.textContent = 'Chargement...';
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
loadVisibilityConfig(),
|
||||||
|
loadPlayerUiIntoUi()
|
||||||
|
]);
|
||||||
|
showResponse('success', 'Configuration rechargée');
|
||||||
|
} catch (err) {
|
||||||
|
showResponse('warning', 'Erreur lors du rechargement: ' + (err.message || 'Erreur inconnue'));
|
||||||
|
} finally {
|
||||||
|
visibilityConfigReloadBtn.disabled = false;
|
||||||
|
visibilityConfigReloadBtn.textContent = oldText;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2687,35 +2700,49 @@ window.onload = async function () {
|
|||||||
const oldText = visibilityConfigSaveBtn.textContent;
|
const oldText = visibilityConfigSaveBtn.textContent;
|
||||||
visibilityConfigSaveBtn.textContent = 'Enregistrement...';
|
visibilityConfigSaveBtn.textContent = 'Enregistrement...';
|
||||||
|
|
||||||
const res = await fetch('/api/settings/visibility-config', {
|
// Save both visibility config and player UI settings
|
||||||
method: 'PUT',
|
const [visibilityRes, playerUiPayload] = await Promise.all([
|
||||||
headers: {
|
fetch('/api/settings/visibility-config', {
|
||||||
'Content-Type': 'application/json',
|
method: 'PUT',
|
||||||
'Authorization': `Bearer ${token}`
|
headers: {
|
||||||
},
|
'Content-Type': 'application/json',
|
||||||
body: JSON.stringify({
|
'Authorization': `Bearer ${token}`
|
||||||
showPicksButton,
|
},
|
||||||
showBonPicksColumn
|
body: JSON.stringify({
|
||||||
})
|
showPicksButton,
|
||||||
});
|
showBonPicksColumn
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
Promise.resolve(getPlayerUiFormValue())
|
||||||
|
]);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!visibilityRes.ok) {
|
||||||
const error = await res.json();
|
const error = await visibilityRes.json();
|
||||||
throw new Error(error.message || 'Failed to save');
|
throw new Error(error.message || 'Failed to save visibility config');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
// Save player UI settings
|
||||||
|
try {
|
||||||
|
const saved = await savePlayerUi(playerUiPayload);
|
||||||
|
setPlayerUiForm(saved);
|
||||||
|
} catch (playerErr) {
|
||||||
|
// If player UI save fails, still show success for visibility but warn
|
||||||
|
console.warn('Player UI save failed:', playerErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await visibilityRes.json();
|
||||||
|
|
||||||
// Clear cache so all pages get the new setting immediately
|
// Clear cache so all pages get the new setting immediately
|
||||||
localStorage.removeItem('visibility_config_cache');
|
localStorage.removeItem('visibility_config_cache');
|
||||||
|
|
||||||
showResponse('success', 'Configuration de visibilité enregistrée');
|
showResponse('success', 'Configuration enregistrée');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (visibilityConfigAlert) {
|
if (visibilityConfigAlert) {
|
||||||
visibilityConfigAlert.className = 'alert alert-danger';
|
visibilityConfigAlert.className = 'alert alert-danger';
|
||||||
visibilityConfigAlert.textContent = 'Erreur: ' + err.message;
|
visibilityConfigAlert.textContent = 'Erreur: ' + err.message;
|
||||||
visibilityConfigAlert.classList.remove('d-none');
|
visibilityConfigAlert.classList.remove('d-none');
|
||||||
}
|
}
|
||||||
|
showResponse('danger', 'Erreur lors de l\'enregistrement: ' + err.message);
|
||||||
} finally {
|
} finally {
|
||||||
visibilityConfigSaveBtn.disabled = false;
|
visibilityConfigSaveBtn.disabled = false;
|
||||||
visibilityConfigSaveBtn.textContent = oldText;
|
visibilityConfigSaveBtn.textContent = oldText;
|
||||||
@@ -2723,11 +2750,14 @@ window.onload = async function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load visibility config when visibility tab is shown
|
// Load visibility config and player UI when visibility tab is shown
|
||||||
const visibilitySubTab = document.getElementById('admin-settings-subtab-visibility');
|
const visibilitySubTab = document.getElementById('admin-settings-subtab-visibility');
|
||||||
if (visibilitySubTab) {
|
if (visibilitySubTab) {
|
||||||
visibilitySubTab.addEventListener('shown.bs.tab', async () => {
|
visibilitySubTab.addEventListener('shown.bs.tab', async () => {
|
||||||
await loadVisibilityConfig();
|
await Promise.all([
|
||||||
|
loadVisibilityConfig(),
|
||||||
|
loadPlayerUiIntoUi()
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+169
-22
@@ -1,6 +1,125 @@
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
function createPageLoader(options = {}) {
|
||||||
|
const title = String(options.title || 'Chargement…');
|
||||||
|
// Subtitle kept for backwards-compat with callers, but we no longer render text in the UI.
|
||||||
|
const subtitle = String(options.subtitle || 'Préparation de la page…');
|
||||||
|
|
||||||
|
let progress = 0;
|
||||||
|
let removed = false;
|
||||||
|
let navResizeObserver = null;
|
||||||
|
|
||||||
|
// Reuse a preloaded overlay if present (prevents initial "clear" flash).
|
||||||
|
const preloadedOverlay = document.getElementById('page-loader-overlay');
|
||||||
|
const overlay = preloadedOverlay || document.createElement('div');
|
||||||
|
if (!preloadedOverlay) {
|
||||||
|
overlay.className = 'page-loader-overlay';
|
||||||
|
overlay.setAttribute('role', 'status');
|
||||||
|
overlay.setAttribute('aria-live', 'polite');
|
||||||
|
overlay.setAttribute('aria-busy', 'true');
|
||||||
|
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="page-loader-card page-loader-card--baronly">
|
||||||
|
<span class="visually-hidden">${title}</span>
|
||||||
|
<div class="page-loader-progress" aria-hidden="true">
|
||||||
|
<div class="page-loader-bar" id="page-loader-bar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Keep screen reader label up to date.
|
||||||
|
const label = overlay.querySelector('.visually-hidden');
|
||||||
|
if (label) label.textContent = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
const barEl = () => overlay.querySelector('#page-loader-bar');
|
||||||
|
|
||||||
|
function clampPct(p) {
|
||||||
|
const n = Number(p);
|
||||||
|
if (!Number.isFinite(n)) return 0;
|
||||||
|
return Math.max(0, Math.min(100, n));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProgress(p) {
|
||||||
|
progress = clampPct(p);
|
||||||
|
const bar = barEl();
|
||||||
|
if (bar) bar.style.width = `${progress}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSubtitle(_) {
|
||||||
|
// Intentionally no-op (we only show the bar now).
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNavHeightPx() {
|
||||||
|
const nav = document.querySelector('nav.navbar');
|
||||||
|
if (!nav) return 0;
|
||||||
|
const rect = nav.getBoundingClientRect();
|
||||||
|
const h = Number(rect?.height) || 0;
|
||||||
|
return h > 0 ? Math.round(h) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOverlayTopOffset() {
|
||||||
|
// Scope the offset to this overlay instance (no global CSS var).
|
||||||
|
overlay.style.setProperty('--page-loader-top', `${getNavHeightPx()}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureMounted() {
|
||||||
|
if (removed) return;
|
||||||
|
// IMPORTANT: even if the overlay is preloaded in HTML (already connected),
|
||||||
|
// we still need to compute the navbar offset. Otherwise it defaults to 0px
|
||||||
|
// and the blur covers the header.
|
||||||
|
updateOverlayTopOffset();
|
||||||
|
document.body.classList.add('page-loading');
|
||||||
|
|
||||||
|
if (!overlay.isConnected) document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// Keep the overlay aligned if the navbar height changes (mobile collapse, resize, etc.)
|
||||||
|
if (!navResizeObserver) {
|
||||||
|
const nav = document.querySelector('nav.navbar');
|
||||||
|
if (nav && typeof ResizeObserver !== 'undefined') {
|
||||||
|
try {
|
||||||
|
navResizeObserver = new ResizeObserver(() => updateOverlayTopOffset());
|
||||||
|
navResizeObserver.observe(nav);
|
||||||
|
} catch (_) {
|
||||||
|
navResizeObserver = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAndRemoveSoon() {
|
||||||
|
if (removed) return;
|
||||||
|
overlay.classList.add('page-loader-hide');
|
||||||
|
overlay.setAttribute('aria-busy', 'false');
|
||||||
|
document.body.classList.remove('page-loading');
|
||||||
|
window.setTimeout(() => {
|
||||||
|
removed = true;
|
||||||
|
if (navResizeObserver) {
|
||||||
|
try { navResizeObserver.disconnect(); } catch (_) {}
|
||||||
|
navResizeObserver = null;
|
||||||
|
}
|
||||||
|
try { overlay.remove(); } catch (_) {}
|
||||||
|
}, 220);
|
||||||
|
}
|
||||||
|
|
||||||
|
function done() {
|
||||||
|
setProgress(100);
|
||||||
|
hideAndRemoveSoon();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(_) {
|
||||||
|
// Don't block the UI on errors: let the page render its own error message.
|
||||||
|
setProgress(Math.max(progress, 95));
|
||||||
|
window.setTimeout(() => hideAndRemoveSoon(), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureMounted();
|
||||||
|
setProgress(8);
|
||||||
|
|
||||||
|
return { setProgress, setSubtitle, done, fail };
|
||||||
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
if (!token) {
|
if (!token) {
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
@@ -74,7 +193,7 @@
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCategories() {
|
async function fetchCategories(pageLoader) {
|
||||||
try {
|
try {
|
||||||
// Use active year if not set
|
// Use active year if not set
|
||||||
if (!currentYear) {
|
if (!currentYear) {
|
||||||
@@ -82,6 +201,8 @@
|
|||||||
document.title = `Pool Oscars (${currentYear}) - Mes picks`;
|
document.title = `Pool Oscars (${currentYear}) - Mes picks`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pageLoader) pageLoader.setProgress(18);
|
||||||
|
|
||||||
const res = await fetch(`/api/picks/categories?year=${currentYear}`, {
|
const res = await fetch(`/api/picks/categories?year=${currentYear}`, {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
@@ -95,6 +216,8 @@
|
|||||||
throw new Error('Failed to fetch categories');
|
throw new Error('Failed to fetch categories');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pageLoader) pageLoader.setProgress(40);
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
categories = data.categories || [];
|
categories = data.categories || [];
|
||||||
currentYear = data.year || currentYear;
|
currentYear = data.year || currentYear;
|
||||||
@@ -108,19 +231,27 @@
|
|||||||
Un administrateur doit d'abord importer les catégories.
|
Un administrateur doit d'abord importer les catégories.
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
if (pageLoader) pageLoader.done();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pageLoader) pageLoader.setProgress(60);
|
||||||
|
|
||||||
renderCategories();
|
renderCategories();
|
||||||
await loadMyPicks();
|
await loadMyPicks(pageLoader);
|
||||||
|
|
||||||
|
if (pageLoader) pageLoader.setProgress(85);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching categories:', err);
|
console.error('Error fetching categories:', err);
|
||||||
showAlert('Erreur lors du chargement des catégories: ' + err.message, 'danger');
|
showAlert('Erreur lors du chargement des catégories: ' + err.message, 'danger');
|
||||||
|
if (pageLoader) pageLoader.fail();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMyPicks() {
|
async function loadMyPicks(pageLoader) {
|
||||||
try {
|
try {
|
||||||
|
if (pageLoader) pageLoader.setProgress(70);
|
||||||
|
|
||||||
const res = await fetch(`/api/picks/my-picks?year=${currentYear}`, {
|
const res = await fetch(`/api/picks/my-picks?year=${currentYear}`, {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
@@ -129,6 +260,8 @@
|
|||||||
throw new Error('Failed to load picks');
|
throw new Error('Failed to load picks');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pageLoader) pageLoader.setProgress(80);
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.pick && data.pick.picks) {
|
if (data.pick && data.pick.picks) {
|
||||||
myPicks = {};
|
myPicks = {};
|
||||||
@@ -738,28 +871,42 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
(async () => {
|
window.addEventListener('DOMContentLoaded', async function() {
|
||||||
currentYear = await fetchActiveYear();
|
const pageLoader = createPageLoader({
|
||||||
document.title = `Pool Oscars (${currentYear}) - Mes choix`;
|
title: 'Chargement des picks',
|
||||||
await fetchCategories();
|
subtitle: 'Récupération des données…'
|
||||||
|
});
|
||||||
|
|
||||||
// Auto-refresh scores on page load if scores tab is active
|
try {
|
||||||
const scoresTab = document.getElementById('tab-scores');
|
currentYear = await fetchActiveYear();
|
||||||
const scoresPane = document.getElementById('pane-scores');
|
document.title = `Pool Oscars (${currentYear}) - Mes choix`;
|
||||||
if (scoresTab && scoresPane && (scoresTab.classList.contains('active') || scoresPane.classList.contains('active'))) {
|
|
||||||
await fetchScores();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-refresh winners on page load if admin and winners tab is active
|
pageLoader.setProgress(12);
|
||||||
if (decoded.admin) {
|
await fetchCategories(pageLoader);
|
||||||
const winnersTab = document.getElementById('tab-winners');
|
|
||||||
const winnersPane = document.getElementById('pane-winners');
|
// Auto-refresh scores on page load if scores tab is active
|
||||||
// Check if winners tab is active (either by default or if it becomes active)
|
const scoresTab = document.getElementById('tab-scores');
|
||||||
if (winnersTab && winnersPane && (winnersTab.classList.contains('active') || winnersPane.classList.contains('active'))) {
|
const scoresPane = document.getElementById('pane-scores');
|
||||||
await fetchWinnersCategories();
|
if (scoresTab && scoresPane && (scoresTab.classList.contains('active') || scoresPane.classList.contains('active'))) {
|
||||||
await calculateScores(true); // Silent mode for auto-refresh
|
await fetchScores();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-refresh winners on page load if admin and winners tab is active
|
||||||
|
if (decoded.admin) {
|
||||||
|
const winnersTab = document.getElementById('tab-winners');
|
||||||
|
const winnersPane = document.getElementById('pane-winners');
|
||||||
|
// Check if winners tab is active (either by default or if it becomes active)
|
||||||
|
if (winnersTab && winnersPane && (winnersTab.classList.contains('active') || winnersPane.classList.contains('active'))) {
|
||||||
|
await fetchWinnersCategories();
|
||||||
|
await calculateScores(true); // Silent mode for auto-refresh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageLoader.done();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error during initial load:', err);
|
||||||
|
pageLoader.fail();
|
||||||
}
|
}
|
||||||
})();
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -107,7 +107,17 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light page-loading">
|
||||||
|
|
||||||
|
<!-- Preloaded loader overlay to avoid initial "clear" flash -->
|
||||||
|
<div id="page-loader-overlay" class="page-loader-overlay" data-preloaded="true" role="status" aria-live="polite" aria-busy="true">
|
||||||
|
<div class="page-loader-card page-loader-card--baronly">
|
||||||
|
<span class="visually-hidden">Chargement…</span>
|
||||||
|
<div class="page-loader-progress" aria-hidden="true">
|
||||||
|
<div class="page-loader-bar" id="page-loader-bar" style="width: 8%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-custom">
|
<nav class="navbar navbar-expand-lg navbar-custom">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
Reference in New Issue
Block a user