diff --git a/ecole/public/css/global.css b/ecole/public/css/global.css index ae5e0839..1413772f 100644 --- a/ecole/public/css/global.css +++ b/ecole/public/css/global.css @@ -511,4 +511,50 @@ a.grille-garanties:hover{ @keyframes l13 { 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); } \ No newline at end of file diff --git a/ecole/public/css/loader.css b/ecole/public/css/loader.css deleted file mode 100644 index d303b759..00000000 --- a/ecole/public/css/loader.css +++ /dev/null @@ -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; -} diff --git a/ecole/public/js/global.js b/ecole/public/js/global.js index 0d3b119a..e7a9fa7b 100644 --- a/ecole/public/js/global.js +++ b/ecole/public/js/global.js @@ -107,17 +107,38 @@ function isNullOrUndefined(value) { } // Fonction pour éviter la duplication du code fetch -async function fetchWithJson(url, method, body = null) { - const options = { - method: method, - headers: { 'Content-Type': 'application/json' }, - }; - if (body) options.body = JSON.stringify(body); +async function fetchWithJson(url, method, body = null, useLoader = true) { + let loaderShown = false; + + if (useLoader && typeof showLoader === 'function') { + showLoader(); + loaderShown = true; + } - const response = await fetch(url, options); - if (!response.ok) throw new Error('Réseau ou erreur serveur'); + try { + const options = { + method: method, + headers: { 'Content-Type': 'application/json' }, + }; + if (body) options.body = JSON.stringify(body); - return await response.json(); + const response = await fetch(url, options); + if (!response.ok) throw new Error('Réseau ou erreur serveur'); + + 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 diff --git a/ecole/public/js/loader.js b/ecole/public/js/loader.js index b7d123ae..b2412cce 100644 --- a/ecole/public/js/loader.js +++ b/ecole/public/js/loader.js @@ -1,31 +1,129 @@ document.addEventListener("DOMContentLoaded", () => { - const loader = document.getElementById("loader-overlay"); // déjà présent dans le layout.ejs - const errorMessage = document.getElementById("error-message"); - let activateTimeout = null; + let timeoutWarning = null; + let currentPageLoad = null; + let safetyTimeout = null; // Timeout de sécurité pour éviter que le loader reste bloqué - //activer le loader et le montrer a l'écran + /** + * 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"); + + // Nettoyer les timeouts précédents 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"); + // Afficher le loader après 500ms (évite le flash sur les chargements rapides) activateTimeout = setTimeout(() => { - loader.classList.add("active"); + if (loader && !loader.classList.contains("hidden")) { + 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); }; - //enlever le loader et le faire disparaitre - window.hideLoader = function() { - clearTimeout(activateTimeout); - loader.classList.remove("active"); - setTimeout(() => loader.classList.add("hidden"), 500); - }; - - //cas d'erreur + /** + * Affiche un message d'erreur dans le loader + * @param {string} msg - Message d'erreur à afficher + */ 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(timeoutWarning); + + if (timeoutMessage) timeoutMessage.style.display = "none"; + errorMessage.textContent = msg; 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; + }; }); diff --git a/ecole/public/js/navigation.js b/ecole/public/js/navigation.js index f2559d91..ba31b5e1 100644 --- a/ecole/public/js/navigation.js +++ b/ecole/public/js/navigation.js @@ -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", () => { - const container = document.querySelector(".container"); - - async function loadPage(url, push = true) { - 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 + + // Intercepter les clics sur les liens internes pour afficher le loader document.body.addEventListener("click", (e) => { const link = e.target.closest("a"); - - if (link && link.getAttribute("href").startsWith("/")) { - - e.preventDefault(); - loadPage(link.href); - + + // Si c'est un lien interne (commence par /) et pas un lien spécial + if (link && + link.getAttribute("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 - window.addEventListener("popstate", () => { + // Afficher le loader lors du rechargement (F5, Cmd+R) + window.addEventListener("beforeunload", () => { + if (typeof showLoader === 'function') { + showLoader(); + } + }); - loadPage(location.pathname, false); - + // 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); + } }); }); diff --git a/ecole/src/db/pb_data/logs.db b/ecole/src/db/pb_data/logs.db index 696e894e..de9bbfb3 100644 Binary files a/ecole/src/db/pb_data/logs.db and b/ecole/src/db/pb_data/logs.db differ diff --git a/ecole/src/db/pb_data/logs.db-shm b/ecole/src/db/pb_data/logs.db-shm index 99f46fe8..fdfdf0ac 100644 Binary files a/ecole/src/db/pb_data/logs.db-shm and b/ecole/src/db/pb_data/logs.db-shm differ diff --git a/ecole/src/db/pb_data/logs.db-wal b/ecole/src/db/pb_data/logs.db-wal index b01f2c5d..e6bc7706 100644 Binary files a/ecole/src/db/pb_data/logs.db-wal and b/ecole/src/db/pb_data/logs.db-wal differ diff --git a/ecole/views/layout.ejs b/ecole/views/layout.ejs index 5a423aad..54099ed9 100644 --- a/ecole/views/layout.ejs +++ b/ecole/views/layout.ejs @@ -25,8 +25,6 @@ - - @@ -37,7 +35,10 @@
-
+
+
+ Si le chargement ne finit pas, cliquez ici pour annuler et vérifier votre connexion. +