[object Object]

← back to Trending Dw

trending: fix infinite-scroll stall (fill while sentinel visible) + default sort to imaged-first

c96132fd8c2313b66cab4e619f7fae015157af82 · 2026-07-07 08:42:36 -0700 · Steve

Files touched

Diff

commit c96132fd8c2313b66cab4e619f7fae015157af82
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 7 08:42:36 2026 -0700

    trending: fix infinite-scroll stall (fill while sentinel visible) + default sort to imaged-first
---
 public/index.html | 10 +++++++++-
 server.js         |  3 ++-
 2 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/public/index.html b/public/index.html
index 20d006f..3f9855f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -108,6 +108,7 @@
       <button class="queuebtn" id="queueBtn" onclick="openQueue()">🛍 Shopify queue <b id="qn">0</b></button>
       <div><label>Sort</label>
         <select id="sort">
+          <option value="have">Imaged first</option>
           <option value="signal">Bestseller signal</option>
           <option value="gap">Gaps first</option>
           <option value="newest">Newest</option>
@@ -129,7 +130,7 @@
 const store = k => { try{ return JSON.parse(localStorage.getItem(k)); }catch(e){ return null; } };
 const state = {
   filters: store('tdw_filters2') || {},          // {dim: [values]}
-  q:'', sort: localStorage.getItem('tdw_sort')||'signal', view: localStorage.getItem('tdw_view')||'grid',
+  q:'', sort: localStorage.getItem('tdw_sort')||'have', view: localStorage.getItem('tdw_view')||'grid',
   page:1, size:12, loading:false, done:false
 };
 const DIMS = [['ourCoverage','Coverage'],['marketplace','Marketplace'],['company','Company'],['style','Style'],['color','Color'],['priceBand','Price']];
@@ -213,6 +214,13 @@ async function fetchPage(){
   d.items.forEach(it=>document.getElementById('grid').appendChild(card(it)));
   if(state.page*state.size>=d.total) state.done=true;
   state.page++; state.loading=false; document.getElementById('loading').style.display='none';
+  // If the loaded cards didn't fill the viewport, the IntersectionObserver won't
+  // re-fire (it only fires on an intersection *transition*) and the board stalls on
+  // page 1. Keep loading while the sentinel is still within (viewport + 400px).
+  if(!state.done){
+    const s=document.getElementById('sentinel'), r=s&&s.getBoundingClientRect();
+    if(r && r.top < (window.innerHeight||document.documentElement.clientHeight)+400) return fetchPage();
+  }
 }
 async function renderLanes(){
   const d=await (await fetch('/api/lanes?'+qs())).json();
diff --git a/server.js b/server.js
index 050efce..16e8121 100644
--- a/server.js
+++ b/server.js
@@ -179,8 +179,9 @@ const server = http.createServer((req,res)=>{
 
   if (u.pathname === '/api/items'){
     let items = applyFilters(loadItems(), u.searchParams);
-    const sort = u.searchParams.get('sort') || 'signal';
+    const sort = u.searchParams.get('sort') || 'have';
     const cmp = {
+      have: (a,b)=> ((b.image?1:0)-(a.image?1:0)) || (b.signalRank||0)-(a.signalRank||0),
       signal: (a,b)=> (b.signalRank||0)-(a.signalRank||0),
       gap: (a,b)=> (a.ourCoverage===b.ourCoverage?0:(a.ourCoverage==='gap'?-1:1)) || (b.signalRank||0)-(a.signalRank||0),
       titleAZ: (a,b)=> (a.title||'').localeCompare(b.title||''),

← 7ee3b0f trending: expand seed to ~50 bestsellers/trending PER PLATFO  ·  back to Trending Dw  ·  gapgen: data-driven style-keyed briefs from live gap items ( bedb55d →