Lm : plus de AJAX, plus de soucis de token, mais loader pas fou, du a l'app en elle meme

This commit is contained in:
Alexis Burnaz 2025-12-22 17:20:33 +01:00
parent d6f06dfd7b
commit 0125e2ae69
9 changed files with 223 additions and 132 deletions

View File

@ -512,3 +512,49 @@ a.grille-garanties:hover{
@keyframes l13 { @keyframes l13 {
100% { transform: rotate(1turn); } 100% { transform: rotate(1turn); }
} }
/* Message d'erreur du loader */
#error-message {
display: none;
color: #ff4444;
font-weight: bold;
text-align: center;
margin-top: 20px;
white-space: pre-line;
font-size: 16px;
}
/* Message de timeout du loader */
#timeout-message {
display: none;
margin-top: 30px;
text-align: center;
color: #d0d0d0;
font-size: 14px;
max-width: 420px;
line-height: 1.6;
padding: 0 20px;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Lien cliquable pour annuler le chargement */
#cancel-loading-link {
color: #66B2FF;
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-thickness: 1px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
}
#cancel-loading-link:hover {
color: #90CAF9;
text-decoration-thickness: 2px;
text-shadow: 0 0 10px rgba(102, 178, 255, 0.6);
}

View File

@ -1,66 +0,0 @@
/* Overlay loader (css pris du site https://css-loaders.com/)*/
#loader-overlay {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: linear-gradient(
rgba(10, 20, 60, 0.2),
rgba(0, 0, 0, 0.4)
);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
font-family: 'Roboto', sans-serif;
opacity: 0;
backdrop-filter: blur(0px);
pointer-events: none;
transition: opacity 0.5s ease, backdrop-filter 0.5s ease;
}
#loader-overlay.active {
opacity: 1;
backdrop-filter: blur(3px);
pointer-events: all;
}
#loader-overlay.hidden {
display: none;
}
/* Spinner wrapper (fade/slide) */
.loader-spin-wrap {
opacity: 0;
transform: translateY(10px);
transition: opacity 0.5s ease, transform 0.5s ease;
transition-delay: 0.5s; /* apparaît après 0.5s */
}
#loader-overlay.active .loader-spin-wrap {
opacity: 1;
transform: translateY(0);
}
/* Spinner circulaire */
.loader-spin {
width: 50px;
aspect-ratio: 1;
border-radius: 50%;
mask:1;
background:
radial-gradient(farthest-side,darkblue 94%,transparent) top/8px 8px no-repeat,
conic-gradient(transparent 30%,darkblue);
-webkit-mask: radial-gradient(farthest-side,transparent calc(100% - 8px),#000 0);
animation: l13 1s infinite linear;
}
@keyframes l13 {
100% { transform: rotate(1turn); }
}
/* Erreur */
#error-message {
display: none;
color: red;
font-weight: bold;
text-align: center;
margin-top: 20px;
white-space: pre-line;
}

View File

@ -107,7 +107,15 @@ function isNullOrUndefined(value) {
} }
// Fonction pour éviter la duplication du code fetch // Fonction pour éviter la duplication du code fetch
async function fetchWithJson(url, method, body = null) { async function fetchWithJson(url, method, body = null, useLoader = true) {
let loaderShown = false;
if (useLoader && typeof showLoader === 'function') {
showLoader();
loaderShown = true;
}
try {
const options = { const options = {
method: method, method: method,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -117,7 +125,20 @@ async function fetchWithJson(url, method, body = null) {
const response = await fetch(url, options); const response = await fetch(url, options);
if (!response.ok) throw new Error('Réseau ou erreur serveur'); if (!response.ok) throw new Error('Réseau ou erreur serveur');
return await response.json(); const data = await response.json();
if (loaderShown && typeof hideLoader === 'function') {
hideLoader();
}
return data;
} catch (error) {
// Toujours cacher le loader en cas d'erreur
if (loaderShown && typeof hideLoader === 'function') {
hideLoader();
}
throw error;
}
} }
// Fonction de load du parcours en session storage // Fonction de load du parcours en session storage

View File

@ -1,31 +1,129 @@
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const loader = document.getElementById("loader-overlay"); // déjà présent dans le layout.ejs let activateTimeout = null;
let timeoutWarning = null;
let currentPageLoad = null;
let safetyTimeout = null; // Timeout de sécurité pour éviter que le loader reste bloqué
/**
* Active le loader et le rend visible
* Affiche un message d'annulation après 8 secondes
*/
window.showLoader = function() {
const loader = document.getElementById("loader-overlay");
if (!loader) return; // Protection : si l'élément n'existe pas, on ne fait rien
const timeoutMessage = document.getElementById("timeout-message");
const errorMessage = document.getElementById("error-message"); const errorMessage = document.getElementById("error-message");
let activateTimeout = null; // Nettoyer les timeouts précédents
//activer le loader et le montrer a l'écran
window.showLoader = function() {
clearTimeout(activateTimeout); clearTimeout(activateTimeout);
errorMessage.style.display = "none"; clearTimeout(timeoutWarning);
// Réinitialiser l'affichage
if (errorMessage) errorMessage.style.display = "none";
if (timeoutMessage) timeoutMessage.style.display = "none";
loader.classList.remove("hidden"); loader.classList.remove("hidden");
// Afficher le loader après 500ms (évite le flash sur les chargements rapides)
activateTimeout = setTimeout(() => { activateTimeout = setTimeout(() => {
if (loader && !loader.classList.contains("hidden")) {
loader.classList.add("active"); loader.classList.add("active");
}
}, 500);
// Afficher le message de timeout après 8 secondes
timeoutWarning = setTimeout(() => {
if (loader && loader.classList.contains("active") && !loader.classList.contains("hidden")) {
if (timeoutMessage) {
timeoutMessage.style.display = "block";
}
}
}, 8000);
// Protection : cacher automatiquement le loader après 30 secondes maximum
// Au cas où hideLoader ne serait jamais appelé (page qui ne charge pas, erreur, etc.)
if (safetyTimeout) clearTimeout(safetyTimeout);
safetyTimeout = setTimeout(() => {
if (loader && !loader.classList.contains("hidden")) {
hideLoader();
}
}, 30000); // 30 secondes max
};
/**
* Désactive et cache le loader
*/
window.hideLoader = function() {
const loader = document.getElementById("loader-overlay");
if (!loader) return; // Protection : si l'élément n'existe pas, on ne fait rien
const timeoutMessage = document.getElementById("timeout-message");
clearTimeout(activateTimeout);
clearTimeout(timeoutWarning);
if (safetyTimeout) {
clearTimeout(safetyTimeout);
safetyTimeout = null;
}
loader.classList.remove("active");
if (timeoutMessage) timeoutMessage.style.display = "none";
setTimeout(() => {
if (loader) {
loader.classList.add("hidden");
}
}, 500); }, 500);
}; };
//enlever le loader et le faire disparaitre /**
window.hideLoader = function() { * Affiche un message d'erreur dans le loader
clearTimeout(activateTimeout); * @param {string} msg - Message d'erreur à afficher
loader.classList.remove("active"); */
setTimeout(() => loader.classList.add("hidden"), 500);
};
//cas d'erreur
window.showError = function(msg) { window.showError = function(msg) {
const errorMessage = document.getElementById("error-message");
const timeoutMessage = document.getElementById("timeout-message");
if (!errorMessage) return; // Protection : si l'élément n'existe pas, on ne fait rien
clearTimeout(activateTimeout); clearTimeout(activateTimeout);
clearTimeout(timeoutWarning);
if (timeoutMessage) timeoutMessage.style.display = "none";
errorMessage.textContent = msg; errorMessage.textContent = msg;
errorMessage.style.display = "block"; errorMessage.style.display = "block";
}; };
/**
* Annule le chargement en cours et cache le loader
*/
const cancelLoadingLink = document.getElementById("cancel-loading-link");
if (cancelLoadingLink) {
cancelLoadingLink.addEventListener("click", () => {
// Annuler le chargement de la page en cours si possible
if (currentPageLoad && currentPageLoad.abort) {
currentPageLoad.abort();
}
hideLoader();
// Afficher une notification discrète
if (typeof M !== 'undefined' && M.toast) {
M.toast({
html: 'Chargement annulé',
classes: 'rounded grey darken-3',
displayLength: 2500
});
}
});
}
/**
* Permet de stocker l'abort controller du chargement en cours
* @param {AbortController} controller - Controller de la requête fetch
*/
window.setCurrentPageLoad = function(controller) {
currentPageLoad = controller;
};
}); });

View File

@ -1,49 +1,40 @@
/**
* Affiche le loader lors des navigations entre pages (localhost)
* Comme on est en localhost, on peut se permettre d'afficher le loader
* au clic sur un lien, il disparaîtra automatiquement au chargement de la nouvelle page
*/
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const container = document.querySelector(".container");
async function loadPage(url, push = true) { // Intercepter les clics sur les liens internes pour afficher le loader
showLoader();
try {
const res = await fetch(url, { headers: { "X-Requested-With": "fetch" }});
const html = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const newContent = doc.querySelector(".container").innerHTML;
container.innerHTML = newContent;
if (push) history.pushState({}, "", url);
}
catch (err) {
showError("Impossible de charger la page. Vérifiez votre connexion.");
}
finally {
hideLoader();
}
}
// Intercepter les clics sur les liens internes
document.body.addEventListener("click", (e) => { document.body.addEventListener("click", (e) => {
const link = e.target.closest("a"); const link = e.target.closest("a");
if (link && link.getAttribute("href").startsWith("/")) { // Si c'est un lien interne (commence par /) et pas un lien spécial
if (link &&
e.preventDefault(); link.getAttribute("href") &&
loadPage(link.href); link.getAttribute("href").startsWith("/") &&
!link.classList.contains("modal-close") &&
!link.getAttribute("href").startsWith("/#")) {
// Afficher le loader avant la navigation
if (typeof showLoader === 'function') {
showLoader();
}
} }
}); });
// Gérer le bouton retour // Afficher le loader lors du rechargement (F5, Cmd+R)
window.addEventListener("popstate", () => { window.addEventListener("beforeunload", () => {
if (typeof showLoader === 'function') {
loadPage(location.pathname, false); showLoader();
}
});
// Cacher le loader quand la page est complètement chargée
window.addEventListener("load", () => {
if (typeof hideLoader === 'function') {
// Petit délai pour éviter le flash
setTimeout(hideLoader, 100);
}
}); });
}); });

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -25,8 +25,6 @@
<link rel="stylesheet" href="/css/global.css"> <link rel="stylesheet" href="/css/global.css">
<!-- DataTables CSS --> <!-- DataTables CSS -->
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/2.0.8/css/dataTables.dataTables.css"> <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/2.0.8/css/dataTables.dataTables.css">
<!-- Loader CSS : style pour l'affichage du loader -->
<link rel="stylesheet" href="/css/loader.css">
</head> </head>
<body> <body>
@ -37,7 +35,10 @@
<div class="loader-spin-wrap"> <div class="loader-spin-wrap">
<div class="loader-spin"></div> <div class="loader-spin"></div>
</div> </div>
<div id="error-message" class="helper-text error"></div> <div id="error-message"></div>
<div id="timeout-message">
Si le chargement ne finit pas, <span id="cancel-loading-link">cliquez ici</span> pour annuler et vérifier votre connexion.
</div>
</div> </div>
<!-- Navbar --> <!-- Navbar -->