[object Object]

← back to Wallco Ai

/designs: infinite scroll, replace pagination links

65dcd74741cff1011676fa63ecef847aafec0521 · 2026-05-13 08:31:44 -0700 · SteveStudio2

Steve standing rule: every site needs infinite scrolling. Was clicking
through 41 numbered pages.

- Grid carries data-current-page / data-total-pages / data-total
- Below the grid: infinite-sentinel (IntersectionObserver target with
  600px rootMargin so loading kicks in before the user hits the bottom)
- infinite-status pill below: 'loading page N of 41' → 'scroll for
  page N+1' → 'all 2429 designs loaded' at the end
- Fetches same /designs URL with page+1, DOMParser-parses the response,
  appends only the children of #catalog-grid to the existing grid
- Single-flight (loading guard), increments cursor on success, disconnects
  observer when last page reached. <noscript> fallback shows original
  pager so JS-disabled clients still navigate.
- Works from any starting page — Steve's URL ?page=16 will load 17..41
  as he scrolls.

Smoke-tested: HTML renders infinite-sentinel + script; 41 pages × 60
per page = 2429 designs total ready to stream.

Files touched

Diff

commit 65dcd74741cff1011676fa63ecef847aafec0521
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 13 08:31:44 2026 -0700

    /designs: infinite scroll, replace pagination links
    
    Steve standing rule: every site needs infinite scrolling. Was clicking
    through 41 numbered pages.
    
    - Grid carries data-current-page / data-total-pages / data-total
    - Below the grid: infinite-sentinel (IntersectionObserver target with
      600px rootMargin so loading kicks in before the user hits the bottom)
    - infinite-status pill below: 'loading page N of 41' → 'scroll for
      page N+1' → 'all 2429 designs loaded' at the end
    - Fetches same /designs URL with page+1, DOMParser-parses the response,
      appends only the children of #catalog-grid to the existing grid
    - Single-flight (loading guard), increments cursor on success, disconnects
      observer when last page reached. <noscript> fallback shows original
      pager so JS-disabled clients still navigate.
    - Works from any starting page — Steve's URL ?page=16 will load 17..41
      as he scrolls.
    
    Smoke-tested: HTML renders infinite-sentinel + script; 41 pages × 60
    per page = 2429 designs total ready to stream.
---
 server.js | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 72 insertions(+), 2 deletions(-)

diff --git a/server.js b/server.js
index 4e3e532..a57bc0a 100644
--- a/server.js
+++ b/server.js
@@ -2910,17 +2910,87 @@ ${cat === 'stoned-animals' ? `
     </div>` : ''}
   </div>
 
-  <div class="design-grid catalog-grid" id="catalog-grid">
+  <div class="design-grid catalog-grid" id="catalog-grid" data-current-page="${page}" data-total-pages="${pages}" data-total="${total}">
     ${cards || '<p class="empty-state">No designs found.</p>'}
   </div>
 
-  ${pages > 1 ? `<div class="pager">${pagerLinks}</div>` : ''}
+  ${pages > 1 ? `
+  <div id="infinite-sentinel" style="height:1px;width:100%"></div>
+  <div id="infinite-status" style="text-align:center;padding:24px 0;font:11px/1 var(--sans,system-ui);letter-spacing:.18em;text-transform:uppercase;color:var(--ink-faint,#888)">${page < pages ? `loading more · page ${page} of ${pages}` : `all ${total} designs loaded`}</div>
+  <noscript><div class="pager">${pagerLinks}</div></noscript>
+  ` : ''}
 </section>
 </main>
 ${FOOTER}
 
 <script type="application/ld+json">${schema}</script>
 <script>
+// ── Infinite scroll — fetch successive /designs?page=N HTML pages, extract
+// just the design-card children, append to the grid when the sentinel
+// approaches the viewport. Single-flight + page cursor + done-state.
+(function(){
+  var grid = document.getElementById('catalog-grid');
+  var sentinel = document.getElementById('infinite-sentinel');
+  var statusEl = document.getElementById('infinite-status');
+  if (!grid || !sentinel || !statusEl) return;
+  var currentPage = parseInt(grid.dataset.currentPage || '1', 10);
+  var totalPages  = parseInt(grid.dataset.totalPages || '1', 10);
+  if (currentPage >= totalPages) return;  // already on last page
+  var loading = false;
+
+  function nextUrl() {
+    var u = new URL(window.location.href);
+    u.searchParams.set('page', String(currentPage + 1));
+    return u.toString();
+  }
+
+  function appendCards(html) {
+    var doc = new DOMParser().parseFromString(html, 'text/html');
+    var nextGrid = doc.getElementById('catalog-grid');
+    if (!nextGrid) return 0;
+    var added = 0;
+    Array.from(nextGrid.children).forEach(function(node){
+      grid.appendChild(node);
+      added++;
+    });
+    return added;
+  }
+
+  function loadMore() {
+    if (loading) return;
+    if (currentPage >= totalPages) {
+      statusEl.textContent = 'all ' + (grid.dataset.total||'') + ' designs loaded';
+      return;
+    }
+    loading = true;
+    statusEl.textContent = 'loading page ' + (currentPage + 1) + ' of ' + totalPages + '…';
+    fetch(nextUrl(), { credentials: 'same-origin' })
+      .then(function(r){ return r.text(); })
+      .then(function(html){
+        var n = appendCards(html);
+        currentPage++;
+        if (n === 0) {
+          // no more rows — stop polling
+          statusEl.textContent = 'all ' + (grid.dataset.total||'') + ' designs loaded';
+          io.disconnect();
+        } else if (currentPage >= totalPages) {
+          statusEl.textContent = 'all ' + (grid.dataset.total||'') + ' designs loaded';
+          io.disconnect();
+        } else {
+          statusEl.textContent = 'scroll for page ' + (currentPage + 1) + ' of ' + totalPages;
+        }
+        loading = false;
+      })
+      .catch(function(){ statusEl.textContent = 'load error — scroll to retry'; loading = false; });
+  }
+
+  var io = new IntersectionObserver(function(entries){
+    entries.forEach(function(e){ if (e.isIntersecting) loadMore(); });
+  }, { rootMargin: '600px 0px 600px 0px' });
+  io.observe(sentinel);
+})();
+</script>
+<script>
 (function(){
   var slider = document.getElementById('density-slider');
   var grid   = document.getElementById('catalog-grid');

← e54cb62 pairings: Dunn-Edwards + Sherwin-Williams paint chip suggest  ·  back to Wallco Ai  ·  daemons: max-it mode — default cadence 240s → 90s ed965df →