diff --git a/css/style.css b/css/style.css index cfa11d5..d0c1ca8 100755 --- a/css/style.css +++ b/css/style.css @@ -1530,6 +1530,40 @@ footer { } } */ +.scroll-spinner { + display: flex; + justify-content: center; + align-items: center; + gap: 0.35rem; + padding: 3.5rem 0 1.5rem; } + .scroll-spinner__dot { + width: 5px; + height: 5px; + border-radius: 50%; + background-color: #bbbbbb; + animation: scroll-spinner-bounce 1.2s infinite ease-in-out; } + .scroll-spinner__dot:nth-child(2) { + animation-delay: 0.2s; } + .scroll-spinner__dot:nth-child(3) { + animation-delay: 0.4s; } + .scroll-spinner--reserve { + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; } + .scroll-spinner--reserve .scroll-spinner__dot { + animation-play-state: paused; } + .scroll-spinner--reserve.is-loading { + opacity: 1; } + .scroll-spinner--reserve.is-loading .scroll-spinner__dot { + animation-play-state: running; } + +@keyframes scroll-spinner-bounce { + 0%, 80%, 100% { + transform: scale(0.6); + opacity: 0.4; } + 40% { + transform: scale(1); + opacity: 1; } } .agenda-view-container { display: none; margin-top: 3rem; } diff --git a/js/agendaView.js b/js/agendaView.js index dba2fe0..c4e1ea1 100644 --- a/js/agendaView.js +++ b/js/agendaView.js @@ -12,7 +12,6 @@ var agendaDone = false; var agendaLoading = false; var swiperWrapper = document.getElementById('agenda-swiper-wrapper'); - var agendaSpinner = document.getElementById('agenda-spinner'); var categoryId = agendaEl.dataset.category || ''; var includeChildren = agendaEl.dataset.includeChildren || '0'; @@ -82,7 +81,6 @@ if (agendaLoading || agendaDone) return; agendaLoading = true; agendaPage++; - agendaSpinner.style.display = 'flex'; var data = new FormData(); data.append('action', 'load_more_agenda'); @@ -98,7 +96,6 @@ fetch(agendaViewData.ajaxUrl, { method: 'POST', body: data }) .then(function (r) { return r.json(); }) .then(function (result) { - agendaSpinner.style.display = 'none'; agendaLoading = false; if (result.success && result.data.html) { var tmp = document.createElement('div'); @@ -118,7 +115,6 @@ } }) .catch(function () { - agendaSpinner.style.display = 'none'; agendaLoading = false; }); } diff --git a/js/infiniteScroll.js b/js/infiniteScroll.js index 8d4e739..513a24e 100644 --- a/js/infiniteScroll.js +++ b/js/infiniteScroll.js @@ -25,7 +25,7 @@ document.addEventListener('DOMContentLoaded', function () { if (loading || done) return; loading = true; page++; - spinner.style.display = 'flex'; + if (spinner) spinner.classList.add('is-loading'); var data = new FormData(); data.append('action', 'load_more_posts'); @@ -46,7 +46,7 @@ document.addEventListener('DOMContentLoaded', function () { fetch(infiniteScrollData.ajaxUrl, { method: 'POST', body: data }) .then(function (response) { return response.json(); }) .then(function (result) { - spinner.style.display = 'none'; + if (spinner) spinner.classList.remove('is-loading'); if (result.success && result.data.html) { grid.insertAdjacentHTML('beforeend', result.data.html); loading = false; @@ -56,16 +56,18 @@ document.addEventListener('DOMContentLoaded', function () { } }) .catch(function () { - spinner.style.display = 'none'; + if (spinner) spinner.classList.remove('is-loading'); loading = false; }); } + // Marge large : la requête part bien avant que la sentinelle soit à + // l'écran, les cards sont souvent déjà en place quand on arrive en bas var observer = new IntersectionObserver(function (entries) { if (entries[0].isIntersecting) { fetchPosts(); } - }, { rootMargin: '200px' }); + }, { rootMargin: '700px' }); observer.observe(sentinel); }); diff --git a/scss/_category.scss b/scss/_category.scss index 361262a..7dc15c0 100644 --- a/scss/_category.scss +++ b/scss/_category.scss @@ -405,6 +405,64 @@ } */ +// ── Infinite scroll spinner ─────────────────────────────────────────────────── + +.scroll-spinner { + display: flex; + justify-content: center; + align-items: center; + gap: 0.35rem; + padding: 3.5rem 0 1.5rem; + + &__dot { + width: 5px; + height: 5px; + border-radius: 50%; + background-color: $less-less-light-gray; + animation: scroll-spinner-bounce 1.2s infinite ease-in-out; + + &:nth-child(2) { + animation-delay: 0.2s; + } + + &:nth-child(3) { + animation-delay: 0.4s; + } + } + + // Scroll infini : la place est réservée en permanence (on ne bascule pas + // display), sinon l'apparition des points décale la mise en page juste + // avant l'insertion des cards + &--reserve { + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + + .scroll-spinner__dot { + animation-play-state: paused; + } + + &.is-loading { + opacity: 1; + + .scroll-spinner__dot { + animation-play-state: running; + } + } + } +} + +@keyframes scroll-spinner-bounce { + 0%, 80%, 100% { + transform: scale(0.6); + opacity: 0.4; + } + 40% { + transform: scale(1); + opacity: 1; + } +} + // ── Agenda slider view ──────────────────────────────────────────────────────── .agenda-view-container { diff --git a/templates/category.twig b/templates/category.twig index e5b0fe0..b207f7e 100644 --- a/templates/category.twig +++ b/templates/category.twig @@ -98,7 +98,7 @@ data-axe="{{ active_axe }}" data-date-from="{{ active_date_from }}" data-date-to="{{ active_date_to }}"> - - diff --git a/templates/page-annonces.twig b/templates/page-annonces.twig index cf3096a..c49b4fe 100644 --- a/templates/page-annonces.twig +++ b/templates/page-annonces.twig @@ -31,7 +31,7 @@ data-axe="{{ active_axe }}" data-date-from="{{ active_date_from }}" data-date-to="{{ active_date_to }}"> -