Merge pull request #40 from quoije/cursor/blur-below-header-b6ef
Blur below header
This commit was merged in pull request #40.
This commit is contained in:
@@ -63,12 +63,16 @@ function createPageLoader(options = {}) {
|
||||
|
||||
function ensureMounted() {
|
||||
if (removed) return;
|
||||
if (!overlay.isConnected) {
|
||||
updateOverlayTopOffset();
|
||||
document.body.classList.add('page-loading');
|
||||
document.body.appendChild(overlay);
|
||||
// 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');
|
||||
|
||||
// Keep the overlay aligned if the navbar height changes (mobile collapse, resize, etc.)
|
||||
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 {
|
||||
|
||||
@@ -63,12 +63,16 @@ function createPageLoader(options = {}) {
|
||||
|
||||
function ensureMounted() {
|
||||
if (removed) return;
|
||||
if (!overlay.isConnected) {
|
||||
updateOverlayTopOffset();
|
||||
document.body.classList.add('page-loading');
|
||||
document.body.appendChild(overlay);
|
||||
// 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');
|
||||
|
||||
// Keep the overlay aligned if the navbar height changes (mobile collapse, resize, etc.)
|
||||
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 {
|
||||
|
||||
+137
-28
@@ -1,26 +1,133 @@
|
||||
window.onload = async function () {
|
||||
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 };
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', async function () {
|
||||
const pageLoader = createPageLoader({
|
||||
title: 'Chargement des statistiques',
|
||||
subtitle: 'Récupération des données…'
|
||||
});
|
||||
|
||||
const token = localStorage.getItem('auth_token');
|
||||
let winnersCache = null;
|
||||
let completionsCache = null;
|
||||
|
||||
function setTableBodyLoading(tbodyId, colspan) {
|
||||
const tbody = document.getElementById(tbodyId);
|
||||
if (!tbody) return;
|
||||
const span = Number.isFinite(Number(colspan)) ? Number(colspan) : 1;
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="${span}" class="text-center py-4" aria-live="polite" aria-busy="true">
|
||||
<div class="d-inline-flex align-items-center gap-2">
|
||||
<div class="spinner-border text-secondary" role="status" aria-label="Chargement">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
<span class="text-muted">Chargement…</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
async function fetchActiveYear() {
|
||||
try {
|
||||
const res = await fetch('/api/settings/year', { method: 'GET' });
|
||||
@@ -247,16 +354,15 @@ window.onload = async function () {
|
||||
}
|
||||
|
||||
try {
|
||||
pageLoader.setProgress(12);
|
||||
|
||||
// Show loading states immediately (avoid "blank" panels)
|
||||
const table = document.querySelector('.user-table');
|
||||
const spinner = document.getElementById('loading-spinner');
|
||||
const statsError = document.getElementById('stats-error');
|
||||
if (spinner) spinner.classList.remove('d-none');
|
||||
if (table) table.classList.add('d-none');
|
||||
if (statsError) statsError.classList.add('d-none');
|
||||
setTableBodyLoading('winners-table-body', 2);
|
||||
setTableBodyLoading('completers-table-body', 4);
|
||||
|
||||
pageLoader.setProgress(20);
|
||||
const activeYear = await fetchActiveYear();
|
||||
if (activeYear) {
|
||||
document.title = `Pool Oscars ${activeYear} - Statistiques des utilisateurs`;
|
||||
@@ -265,6 +371,7 @@ window.onload = async function () {
|
||||
}
|
||||
|
||||
const statsUrl = activeYear ? `/api/users/stats?year=${encodeURIComponent(String(activeYear))}` : '/api/users/stats';
|
||||
pageLoader.setProgress(32);
|
||||
const [statsRes, winners, completions] = await Promise.all([
|
||||
fetch(statsUrl, {
|
||||
method: 'GET',
|
||||
@@ -283,6 +390,7 @@ window.onload = async function () {
|
||||
throw new Error('Failed to fetch user stats');
|
||||
}
|
||||
|
||||
pageLoader.setProgress(70);
|
||||
const stats = await statsRes.json();
|
||||
const userTableBody = document.getElementById('user-table-body');
|
||||
|
||||
@@ -304,10 +412,10 @@ window.onload = async function () {
|
||||
userTableBody.appendChild(userRow);
|
||||
});
|
||||
|
||||
// Hide spinner and show table after loading
|
||||
if (spinner) spinner.classList.add('d-none');
|
||||
// Show table after loading
|
||||
if (table) table.classList.remove('d-none');
|
||||
if (statsError) statsError.classList.add('d-none');
|
||||
pageLoader.setProgress(92);
|
||||
|
||||
// Add event listener for user links
|
||||
document.querySelectorAll('.user-link').forEach(link => {
|
||||
@@ -334,12 +442,12 @@ window.onload = async function () {
|
||||
modal.show();
|
||||
});
|
||||
});
|
||||
|
||||
pageLoader.done();
|
||||
} catch (error) {
|
||||
console.error('Error loading user statistics:', error);
|
||||
const spinner = document.getElementById('loading-spinner');
|
||||
const table = document.querySelector('.user-table');
|
||||
const statsError = document.getElementById('stats-error');
|
||||
if (spinner) spinner.classList.add('d-none');
|
||||
if (table) table.classList.add('d-none');
|
||||
if (statsError) {
|
||||
statsError.textContent = "Impossible de charger le classement pour le moment.";
|
||||
@@ -354,6 +462,7 @@ window.onload = async function () {
|
||||
if (completionsCache === null) completionsCache = await fetchCompletions();
|
||||
renderCompletions(completionsCache);
|
||||
} catch (_) {}
|
||||
pageLoader.fail();
|
||||
}
|
||||
|
||||
// Log-off functionality
|
||||
@@ -361,4 +470,4 @@ window.onload = async function () {
|
||||
localStorage.removeItem('auth_token');
|
||||
window.location.href = '/';
|
||||
});
|
||||
};
|
||||
});
|
||||
+14
-24
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pool Oscars - Statistiques des utilisateurs</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="./css/app.css" rel="stylesheet">
|
||||
<style>
|
||||
.navbar-custom {
|
||||
background-color: black;
|
||||
@@ -36,7 +37,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>
|
||||
|
||||
<!-- Black banner with buttons and user info -->
|
||||
<nav class="navbar navbar-expand-lg navbar-custom">
|
||||
@@ -90,13 +101,6 @@
|
||||
<div class="tab-pane fade show active" id="pane-ranking" role="tabpanel" aria-labelledby="tab-ranking" tabindex="0">
|
||||
<div id="stats-error" class="alert alert-danger d-none" role="alert"></div>
|
||||
|
||||
<!-- Spinner shown while loading -->
|
||||
<div id="loading-spinner" class="d-flex justify-content-center align-items-center my-5" aria-live="polite" aria-busy="true">
|
||||
<div class="spinner-border text-secondary" role="status" aria-label="Chargement">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table for users and movies watched -->
|
||||
<table class="table table-striped table-hover align-middle user-table d-none">
|
||||
<thead>
|
||||
@@ -125,14 +129,7 @@
|
||||
</thead>
|
||||
<tbody id="winners-table-body">
|
||||
<tr>
|
||||
<td colspan="2" class="text-center py-4" aria-live="polite" aria-busy="true">
|
||||
<div class="d-inline-flex align-items-center gap-2">
|
||||
<div class="spinner-border text-secondary" role="status" aria-label="Chargement">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
<span class="text-muted">Chargement…</span>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="2" class="text-center py-4 text-muted" aria-live="polite" aria-busy="true">Chargement…</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -155,14 +152,7 @@
|
||||
</thead>
|
||||
<tbody id="completers-table-body">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4" aria-live="polite" aria-busy="true">
|
||||
<div class="d-inline-flex align-items-center gap-2">
|
||||
<div class="spinner-border text-secondary" role="status" aria-label="Chargement">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
<span class="text-muted">Chargement…</span>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="4" class="text-center py-4 text-muted" aria-live="polite" aria-busy="true">Chargement…</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user