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:
Mathieu Moran
2025-12-28 02:21:03 -05:00
committed by GitHub
4 changed files with 246 additions and 81 deletions
+14 -36
View File
@@ -121,11 +121,6 @@
Année active
</button>
</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">
<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
@@ -161,25 +156,6 @@
</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 -->
<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">
@@ -254,25 +230,27 @@
<!-- Visibility settings -->
<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="text-muted small mb-3">
Contrôle la visibilité de certains éléments dans l'interface.
</div>
<div id="visibility-config-alert" class="alert d-none mb-3" role="alert"></div>
<div id="visibility-config-alert" class="alert d-none" role="alert"></div>
<div class="form-check">
<div class="form-check mb-4">
<input class="form-check-input" type="checkbox" id="visibility-show-picks-features">
<label class="form-check-label" for="visibility-show-picks-features">
Afficher les fonctionnalités "Picks"
</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 class="mt-3">
<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 class="form-check mb-4">
<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 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>
+50 -20
View File
@@ -2671,8 +2671,21 @@ window.onload = async function () {
if (visibilityConfigReloadBtn) {
visibilityConfigReloadBtn.addEventListener('click', async () => {
await loadVisibilityConfig();
showResponse('success', 'Configuration rechargée');
visibilityConfigReloadBtn.disabled = true;
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;
visibilityConfigSaveBtn.textContent = 'Enregistrement...';
const res = await fetch('/api/settings/visibility-config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
showPicksButton,
showBonPicksColumn
})
});
// Save both visibility config and player UI settings
const [visibilityRes, playerUiPayload] = await Promise.all([
fetch('/api/settings/visibility-config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
showPicksButton,
showBonPicksColumn
})
}),
Promise.resolve(getPlayerUiFormValue())
]);
if (!res.ok) {
const error = await res.json();
throw new Error(error.message || 'Failed to save');
if (!visibilityRes.ok) {
const error = await visibilityRes.json();
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
localStorage.removeItem('visibility_config_cache');
showResponse('success', 'Configuration de visibilité enregistrée');
showResponse('success', 'Configuration enregistrée');
} catch (err) {
if (visibilityConfigAlert) {
visibilityConfigAlert.className = 'alert alert-danger';
visibilityConfigAlert.textContent = 'Erreur: ' + err.message;
visibilityConfigAlert.classList.remove('d-none');
}
showResponse('danger', 'Erreur lors de l\'enregistrement: ' + err.message);
} finally {
visibilityConfigSaveBtn.disabled = false;
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');
if (visibilitySubTab) {
visibilitySubTab.addEventListener('shown.bs.tab', async () => {
await loadVisibilityConfig();
await Promise.all([
loadVisibilityConfig(),
loadPlayerUiIntoUi()
]);
});
}
+171 -24
View File
@@ -1,6 +1,125 @@
(function() {
'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');
if (!token) {
window.location.href = '/';
@@ -74,7 +193,7 @@
}, 5000);
}
async function fetchCategories() {
async function fetchCategories(pageLoader) {
try {
// Use active year if not set
if (!currentYear) {
@@ -82,6 +201,8 @@
document.title = `Pool Oscars (${currentYear}) - Mes picks`;
}
if (pageLoader) pageLoader.setProgress(18);
const res = await fetch(`/api/picks/categories?year=${currentYear}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
@@ -95,6 +216,8 @@
throw new Error('Failed to fetch categories');
}
if (pageLoader) pageLoader.setProgress(40);
const data = await res.json();
categories = data.categories || [];
currentYear = data.year || currentYear;
@@ -108,19 +231,27 @@
Un administrateur doit d'abord importer les catégories.
</div>
`;
if (pageLoader) pageLoader.done();
return;
}
if (pageLoader) pageLoader.setProgress(60);
renderCategories();
await loadMyPicks();
await loadMyPicks(pageLoader);
if (pageLoader) pageLoader.setProgress(85);
} catch (err) {
console.error('Error fetching categories:', err);
showAlert('Erreur lors du chargement des catégories: ' + err.message, 'danger');
if (pageLoader) pageLoader.fail();
}
}
async function loadMyPicks() {
async function loadMyPicks(pageLoader) {
try {
if (pageLoader) pageLoader.setProgress(70);
const res = await fetch(`/api/picks/my-picks?year=${currentYear}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
@@ -129,6 +260,8 @@
throw new Error('Failed to load picks');
}
if (pageLoader) pageLoader.setProgress(80);
const data = await res.json();
if (data.pick && data.pick.picks) {
myPicks = {};
@@ -738,28 +871,42 @@
});
// Initial load
(async () => {
currentYear = await fetchActiveYear();
document.title = `Pool Oscars (${currentYear}) - Mes choix`;
await fetchCategories();
// Auto-refresh scores on page load if scores tab is active
const scoresTab = document.getElementById('tab-scores');
const scoresPane = document.getElementById('pane-scores');
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
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
window.addEventListener('DOMContentLoaded', async function() {
const pageLoader = createPageLoader({
title: 'Chargement des picks',
subtitle: 'Récupération des données…'
});
try {
currentYear = await fetchActiveYear();
document.title = `Pool Oscars (${currentYear}) - Mes choix`;
pageLoader.setProgress(12);
await fetchCategories(pageLoader);
// Auto-refresh scores on page load if scores tab is active
const scoresTab = document.getElementById('tab-scores');
const scoresPane = document.getElementById('pane-scores');
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
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
View File
@@ -107,7 +107,17 @@
}
</style>
</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">
<div class="container">