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:
+11
-33
@@ -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,21 +230,24 @@
|
||||
<!-- 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="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="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>
|
||||
@@ -278,7 +257,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="admin-version" role="tabpanel" aria-labelledby="admin-version-tab">
|
||||
<div class="card p-3">
|
||||
|
||||
+40
-10
@@ -2671,8 +2671,21 @@ window.onload = async function () {
|
||||
|
||||
if (visibilityConfigReloadBtn) {
|
||||
visibilityConfigReloadBtn.addEventListener('click', async () => {
|
||||
await loadVisibilityConfig();
|
||||
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,7 +2700,9 @@ window.onload = async function () {
|
||||
const oldText = visibilityConfigSaveBtn.textContent;
|
||||
visibilityConfigSaveBtn.textContent = 'Enregistrement...';
|
||||
|
||||
const res = await fetch('/api/settings/visibility-config', {
|
||||
// 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',
|
||||
@@ -2697,25 +2712,37 @@ window.onload = async function () {
|
||||
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()
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+153
-6
@@ -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,10 +871,18 @@
|
||||
});
|
||||
|
||||
// Initial load
|
||||
(async () => {
|
||||
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`;
|
||||
await fetchCategories();
|
||||
|
||||
pageLoader.setProgress(12);
|
||||
await fetchCategories(pageLoader);
|
||||
|
||||
// Auto-refresh scores on page load if scores tab is active
|
||||
const scoresTab = document.getElementById('tab-scores');
|
||||
@@ -760,6 +901,12 @@
|
||||
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>
|
||||
</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">
|
||||
|
||||
Reference in New Issue
Block a user