Refactor control panel: merge Lecteur tab into Visibilité and add loading bar to Mes picks

- Remove 'Lecteur' subtab from Réglages section
- Move player UI settings (show Source/Progress) into Visibilité tab
- Consolidate reload/save buttons to use single set for both visibility and player UI settings
- Simplify visibility tab formatting with minimalistic, consistent styling
- Add loading bar progress indicator to Mes picks tab matching movies/checklist/stats pages
- Update JavaScript to handle both visibility config and player UI settings together
- Improve code organization and user experience
This commit is contained in:
Mathieu Moran
2025-12-28 02:20:21 -05:00
parent 211c115d41
commit d10164076c
4 changed files with 246 additions and 81 deletions
+14 -36
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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">