[object Object]

← back to AsSeenInMovies

Replace pagination with infinite scrolling

c27bdce6360f6c4cfe33f0844708fdf34c02fd58 · 2026-06-23 09:33:28 -0700 · Claude

- Remove prev/next pagination nav
- Add scroll sentinel at bottom
- Load next page dynamically when user scrolls near sentinel
- Append product rows incrementally to grid

Complies with infinite scrolling requirement.

Files touched

Diff

commit c27bdce6360f6c4cfe33f0844708fdf34c02fd58
Author: Claude <claude@anthropic.com>
Date:   Tue Jun 23 09:33:28 2026 -0700

    Replace pagination with infinite scrolling
    
    - Remove prev/next pagination nav
    - Add scroll sentinel at bottom
    - Load next page dynamically when user scrolls near sentinel
    - Append product rows incrementally to grid
    
    Complies with infinite scrolling requirement.
---
 server.js | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 68 insertions(+), 7 deletions(-)

diff --git a/server.js b/server.js
index 81ebd99..031fbd1 100644
--- a/server.js
+++ b/server.js
@@ -438,12 +438,7 @@ app.get('/spotted', async (req, res) => {
           ${s.shop_url ? '<a class="shop-btn" href="'+esc(s.shop_url)+'" target="_blank" rel="nofollow noopener noreferrer">Where to find it ↗</a>' : ''}
         </div>
       </article>`).join('');
-    const pager = `
-      <nav class="pager" aria-label="Pagination">
-        ${page > 1 ? `<a class="chip" href="/spotted?${qs({page: page-1})}">← Prev</a>` : ''}
-        <span class="chip" style="background:var(--gold);color:#1a1410">Page ${page} of ${totalPages}</span>
-        ${page < totalPages ? `<a class="chip" href="/spotted?${qs({page: page+1})}">Next →</a>` : ''}
-      </nav>`;
+    const pager = totalPages > 1 ? `<div id="scroll-sentinel" style="padding:20px;text-align:center;color:var(--ink-faint);font-size:13px;"><span id="loading-status">Loading more…</span></div>` : '';
     const body = `
       <style>
         .cat-bar{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:24px;padding-bottom:14px;border-bottom:1px solid var(--line)}
@@ -516,7 +511,73 @@ app.get('/spotted', async (req, res) => {
 
       ${filterBar}
       ${rows || '<p class="empty">No verified placements in this category yet.</p>'}
-      ${pager}`;
+      ${pager}
+      <script>
+        (function(){
+          const grid = document.querySelector('main');
+          const sentinel = document.getElementById('scroll-sentinel');
+          if (!grid || !sentinel || !('IntersectionObserver' in window)) return;
+
+          let currentPage = ${page};
+          let totalPages = ${totalPages};
+          let isLoading = false;
+
+          const scrollObserver = new IntersectionObserver(entries => {
+            for (const e of entries) {
+              if (e.isIntersecting && currentPage < totalPages && !isLoading) {
+                loadNextPage();
+              }
+            }
+          }, { rootMargin: '400px 0px' });
+
+          scrollObserver.observe(sentinel);
+
+          function loadNextPage() {
+            if (isLoading || currentPage >= totalPages) return;
+            isLoading = true;
+            const nextPage = currentPage + 1;
+            const url = new URL(location.href);
+            url.searchParams.set('page', nextPage);
+
+            fetch(url.toString())
+              .then(r => r.text())
+              .then(html => {
+                const parser = new DOMParser();
+                const doc = parser.parseFromString(html, 'text/html');
+                const nextRows = doc.querySelectorAll('main > .spot-row');
+
+                if (nextRows.length === 0) {
+                  currentPage = totalPages;
+                  document.getElementById('loading-status').textContent = 'All placements loaded';
+                  sentinel.style.color = '#999';
+                  return;
+                }
+
+                const main = document.querySelector('main');
+                const lastRow = main.querySelector('.spot-row:last-of-type');
+                nextRows.forEach(row => {
+                  lastRow.parentNode.insertBefore(row.cloneNode(true), sentinel);
+                });
+
+                currentPage = nextPage;
+                if (currentPage >= totalPages) {
+                  document.getElementById('loading-status').textContent = 'All placements loaded';
+                  sentinel.style.color = '#999';
+                } else {
+                  document.getElementById('loading-status').textContent = 'Loading more…';
+                }
+
+                isLoading = false;
+              })
+              .catch(err => {
+                console.error('Failed to load more:', err);
+                document.getElementById('loading-status').textContent = 'Error loading more';
+                sentinel.style.color = '#d84315';
+                isLoading = false;
+              });
+          }
+        })();
+      </script>`;
     // ItemList schema — rich snippet candidates for the gallery page.
     // Each Product item links back to its parent movie page; SearchAction
     // would be redundant since the site WebSite schema covers it.

← 11799d1 Add per-site favicon (kills /favicon.ico 404)  ·  back to AsSeenInMovies  ·  (newest)