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() {
|
function ensureMounted() {
|
||||||
if (removed) return;
|
if (removed) return;
|
||||||
if (!overlay.isConnected) {
|
// 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();
|
updateOverlayTopOffset();
|
||||||
document.body.classList.add('page-loading');
|
document.body.classList.add('page-loading');
|
||||||
document.body.appendChild(overlay);
|
|
||||||
|
if (!overlay.isConnected) document.body.appendChild(overlay);
|
||||||
|
|
||||||
// Keep the overlay aligned if the navbar height changes (mobile collapse, resize, etc.)
|
// Keep the overlay aligned if the navbar height changes (mobile collapse, resize, etc.)
|
||||||
|
if (!navResizeObserver) {
|
||||||
const nav = document.querySelector('nav.navbar');
|
const nav = document.querySelector('nav.navbar');
|
||||||
if (nav && typeof ResizeObserver !== 'undefined') {
|
if (nav && typeof ResizeObserver !== 'undefined') {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -63,12 +63,16 @@ function createPageLoader(options = {}) {
|
|||||||
|
|
||||||
function ensureMounted() {
|
function ensureMounted() {
|
||||||
if (removed) return;
|
if (removed) return;
|
||||||
if (!overlay.isConnected) {
|
// 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();
|
updateOverlayTopOffset();
|
||||||
document.body.classList.add('page-loading');
|
document.body.classList.add('page-loading');
|
||||||
document.body.appendChild(overlay);
|
|
||||||
|
if (!overlay.isConnected) document.body.appendChild(overlay);
|
||||||
|
|
||||||
// Keep the overlay aligned if the navbar height changes (mobile collapse, resize, etc.)
|
// Keep the overlay aligned if the navbar height changes (mobile collapse, resize, etc.)
|
||||||
|
if (!navResizeObserver) {
|
||||||
const nav = document.querySelector('nav.navbar');
|
const nav = document.querySelector('nav.navbar');
|
||||||
if (nav && typeof ResizeObserver !== 'undefined') {
|
if (nav && typeof ResizeObserver !== 'undefined') {
|
||||||
try {
|
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');
|
const token = localStorage.getItem('auth_token');
|
||||||
let winnersCache = null;
|
let winnersCache = null;
|
||||||
let completionsCache = 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() {
|
async function fetchActiveYear() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/settings/year', { method: 'GET' });
|
const res = await fetch('/api/settings/year', { method: 'GET' });
|
||||||
@@ -247,16 +354,15 @@ window.onload = async function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
pageLoader.setProgress(12);
|
||||||
|
|
||||||
// Show loading states immediately (avoid "blank" panels)
|
// Show loading states immediately (avoid "blank" panels)
|
||||||
const table = document.querySelector('.user-table');
|
const table = document.querySelector('.user-table');
|
||||||
const spinner = document.getElementById('loading-spinner');
|
|
||||||
const statsError = document.getElementById('stats-error');
|
const statsError = document.getElementById('stats-error');
|
||||||
if (spinner) spinner.classList.remove('d-none');
|
|
||||||
if (table) table.classList.add('d-none');
|
if (table) table.classList.add('d-none');
|
||||||
if (statsError) statsError.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();
|
const activeYear = await fetchActiveYear();
|
||||||
if (activeYear) {
|
if (activeYear) {
|
||||||
document.title = `Pool Oscars ${activeYear} - Statistiques des utilisateurs`;
|
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';
|
const statsUrl = activeYear ? `/api/users/stats?year=${encodeURIComponent(String(activeYear))}` : '/api/users/stats';
|
||||||
|
pageLoader.setProgress(32);
|
||||||
const [statsRes, winners, completions] = await Promise.all([
|
const [statsRes, winners, completions] = await Promise.all([
|
||||||
fetch(statsUrl, {
|
fetch(statsUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -283,6 +390,7 @@ window.onload = async function () {
|
|||||||
throw new Error('Failed to fetch user stats');
|
throw new Error('Failed to fetch user stats');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pageLoader.setProgress(70);
|
||||||
const stats = await statsRes.json();
|
const stats = await statsRes.json();
|
||||||
const userTableBody = document.getElementById('user-table-body');
|
const userTableBody = document.getElementById('user-table-body');
|
||||||
|
|
||||||
@@ -304,10 +412,10 @@ window.onload = async function () {
|
|||||||
userTableBody.appendChild(userRow);
|
userTableBody.appendChild(userRow);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hide spinner and show table after loading
|
// Show table after loading
|
||||||
if (spinner) spinner.classList.add('d-none');
|
|
||||||
if (table) table.classList.remove('d-none');
|
if (table) table.classList.remove('d-none');
|
||||||
if (statsError) statsError.classList.add('d-none');
|
if (statsError) statsError.classList.add('d-none');
|
||||||
|
pageLoader.setProgress(92);
|
||||||
|
|
||||||
// Add event listener for user links
|
// Add event listener for user links
|
||||||
document.querySelectorAll('.user-link').forEach(link => {
|
document.querySelectorAll('.user-link').forEach(link => {
|
||||||
@@ -334,12 +442,12 @@ window.onload = async function () {
|
|||||||
modal.show();
|
modal.show();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pageLoader.done();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading user statistics:', error);
|
console.error('Error loading user statistics:', error);
|
||||||
const spinner = document.getElementById('loading-spinner');
|
|
||||||
const table = document.querySelector('.user-table');
|
const table = document.querySelector('.user-table');
|
||||||
const statsError = document.getElementById('stats-error');
|
const statsError = document.getElementById('stats-error');
|
||||||
if (spinner) spinner.classList.add('d-none');
|
|
||||||
if (table) table.classList.add('d-none');
|
if (table) table.classList.add('d-none');
|
||||||
if (statsError) {
|
if (statsError) {
|
||||||
statsError.textContent = "Impossible de charger le classement pour le moment.";
|
statsError.textContent = "Impossible de charger le classement pour le moment.";
|
||||||
@@ -354,6 +462,7 @@ window.onload = async function () {
|
|||||||
if (completionsCache === null) completionsCache = await fetchCompletions();
|
if (completionsCache === null) completionsCache = await fetchCompletions();
|
||||||
renderCompletions(completionsCache);
|
renderCompletions(completionsCache);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
pageLoader.fail();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log-off functionality
|
// Log-off functionality
|
||||||
@@ -361,4 +470,4 @@ window.onload = async function () {
|
|||||||
localStorage.removeItem('auth_token');
|
localStorage.removeItem('auth_token');
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
+14
-24
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Pool Oscars - Statistiques des utilisateurs</title>
|
<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="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="./css/app.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
.navbar-custom {
|
.navbar-custom {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
@@ -36,7 +37,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>
|
||||||
|
|
||||||
<!-- Black banner with buttons and user info -->
|
<!-- Black banner with buttons and user info -->
|
||||||
<nav class="navbar navbar-expand-lg navbar-custom">
|
<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 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>
|
<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 for users and movies watched -->
|
||||||
<table class="table table-striped table-hover align-middle user-table d-none">
|
<table class="table table-striped table-hover align-middle user-table d-none">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -125,14 +129,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="winners-table-body">
|
<tbody id="winners-table-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="text-center py-4" aria-live="polite" aria-busy="true">
|
<td colspan="2" class="text-center py-4 text-muted" aria-live="polite" aria-busy="true">Chargement…</td>
|
||||||
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -155,14 +152,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="completers-table-body">
|
<tbody id="completers-table-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="text-center py-4" aria-live="polite" aria-busy="true">
|
<td colspan="4" class="text-center py-4 text-muted" aria-live="polite" aria-busy="true">Chargement…</td>
|
||||||
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user