← back to Commercialrealestate
just-listed: add drag-to-reorder columns + persisted column order
373b3d143b8f4f37ecae2f073661a021fe9290d6 · 2026-08-12 06:59:14 -0700 · Steve Abrams
Files touched
M public/just-listed.html
Diff
commit 373b3d143b8f4f37ecae2f073661a021fe9290d6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 12 06:59:14 2026 -0700
just-listed: add drag-to-reorder columns + persisted column order
---
public/just-listed.html | 24 +++++++++++++++++++-----
1 file changed, 19 insertions(+), 5 deletions(-)
diff --git a/public/just-listed.html b/public/just-listed.html
index bf804b3..31bfaa1 100644
--- a/public/just-listed.html
+++ b/public/just-listed.html
@@ -59,6 +59,9 @@ table{border-collapse:collapse;width:100%;font-size:12.5px}
thead th{position:sticky;top:0;background:#11161d;z-index:2;text-align:left;padding:9px 11px;border-bottom:1px solid var(--line);white-space:nowrap;cursor:pointer;user-select:none;color:var(--mut);font-weight:700;font-size:10.5px;text-transform:uppercase;letter-spacing:.4px}
thead th:hover{color:var(--ink)}
thead th.sorted{color:var(--blue)}
+thead th[draggable=true]{cursor:grab}
+thead th.dragging{opacity:.45}
+thead th.dropzone{box-shadow:inset 3px 0 0 var(--blue);color:var(--ink)}
thead th .ar{opacity:.35;font-size:9px;margin-left:4px}
thead th.sorted .ar{opacity:1}
thead th.num{text-align:right}
@@ -139,11 +142,16 @@ const COLS=[
{k:'verified',label:'Verified',on:false,render:r=>r.verified?'✓':'—'},
{k:'upside_note',label:'Upside',on:true,cls:'wrap',render:r=>esc(String(r.upside_note||'—').slice(0,200))},
];
-// restore saved column visibility
-try{const saved=JSON.parse(localStorage.getItem('jlCols')||'null');if(Array.isArray(saved)&&saved.length)COLS.forEach(c=>c.on=saved.includes(c.k));}catch(e){}
+// restore saved column ORDER, then visibility
+try{
+ const savedOrder=JSON.parse(localStorage.getItem('jlColOrder')||'null');
+ if(Array.isArray(savedOrder)&&savedOrder.length)COLS.sort((a,b)=>{const ia=savedOrder.indexOf(a.k),ib=savedOrder.indexOf(b.k);return(ia<0?999:ia)-(ib<0?999:ib);});
+ const savedVis=JSON.parse(localStorage.getItem('jlCols')||'null');
+ if(Array.isArray(savedVis)&&savedVis.length)COLS.forEach(c=>c.on=savedVis.includes(c.k));
+}catch(e){}
const visCols=()=>COLS.filter(c=>c.on);
const sortVal=(r,key)=>{const c=COLS.find(x=>x.k===key);return c&&c.val?c.val(r):r[key];};
-const saveCols=()=>localStorage.setItem('jlCols',JSON.stringify(COLS.filter(c=>c.on).map(c=>c.k)));
+const saveCols=()=>{localStorage.setItem('jlCols',JSON.stringify(COLS.filter(c=>c.on).map(c=>c.k)));localStorage.setItem('jlColOrder',JSON.stringify(COLS.map(c=>c.k)));};
// space-separated search = AND across every field (Steve standing rule)
function passSearch(r){
@@ -178,7 +186,7 @@ function buildRail(){
function renderHead(){
$('#thead').innerHTML='<tr>'+visCols().map(c=>{
const sorted=c.k===sortKey,ar=sorted?(sortDir===1?'▲':'▼'):'↕';
- return `<th data-k="${c.k}" class="${c.num?'num':''}${sorted?' sorted':''}" title="Sort by ${esc(c.label)}">${esc(c.label)}<span class="ar">${ar}</span></th>`;
+ return `<th draggable="true" data-k="${c.k}" class="${c.num?'num':''}${sorted?' sorted':''}" title="Click to sort · drag to reorder">${esc(c.label)}<span class="ar">${ar}</span></th>`;
}).join('')+'</tr>';
}
// ── body ──
@@ -219,6 +227,12 @@ $('#thead').addEventListener('click',e=>{
if(key===sortKey)setSort(key,-sortDir);
else setSort(key,col&&col.num?-1:1);
});
+// header drag-to-reorder (a real drag suppresses the trailing click, so click-sort still works)
+let dragK=null;
+$('#thead').addEventListener('dragstart',e=>{const th=e.target.closest('th');if(!th)return;dragK=th.dataset.k;th.classList.add('dragging');e.dataTransfer.effectAllowed='move';try{e.dataTransfer.setData('text/plain',dragK);}catch(_){}});
+$('#thead').addEventListener('dragover',e=>{if(dragK==null)return;e.preventDefault();e.dataTransfer.dropEffect='move';const th=e.target.closest('th');$('#thead').querySelectorAll('th').forEach(t=>t.classList.remove('dropzone'));if(th&&th.dataset.k!==dragK)th.classList.add('dropzone');});
+$('#thead').addEventListener('drop',e=>{e.preventDefault();const th=e.target.closest('th');if(!th||dragK==null)return;const destK=th.dataset.k;if(destK!==dragK){const from=COLS.findIndex(c=>c.k===dragK);const[m]=COLS.splice(from,1);const to=COLS.findIndex(c=>c.k===destK);COLS.splice(to,0,m);saveCols();renderColPanel();render();}dragK=null;});
+$('#thead').addEventListener('dragend',()=>{dragK=null;$('#thead').querySelectorAll('th').forEach(t=>t.classList.remove('dropzone','dragging'));});
// rail filters
$('#fStatus').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.status,c.dataset.s);c.classList.toggle('active');render();});
$('#fType').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.type,c.dataset.t);c.classList.toggle('active');render();});
@@ -262,7 +276,7 @@ fetch('/data/listings.json').then(r=>r.json()).then(d=>{
META=d.meta||{};
DATA=(d.listings||[]).filter(l=>String(l.status||'').toLowerCase().startsWith('active'));
$('#srcBadge').textContent=`${DATA.length.toLocaleString()} active`;
- $('#prov').innerHTML=`Active listings sourced from broker/portal feeds${META.sourced?` · batch sourced <b>${esc(String(META.sourced).slice(0,10))}</b>`:''}. Click any column header to sort; use <b>⚙ Columns</b> to show/hide fields. This feed carries no per-listing "date listed", so there's no true recency sort — use <b>Cap rate</b>, <b>Price</b>, or <b>Newest built</b>. Verify status against the source before acting.`;
+ $('#prov').innerHTML=`Active listings sourced from broker/portal feeds${META.sourced?` · batch sourced <b>${esc(String(META.sourced).slice(0,10))}</b>`:''}. Click any column header to sort, <b>drag headers</b> to reorder, and use <b>⚙ Columns</b> to show/hide fields. This feed carries no per-listing "date listed", so there's no true recency sort — use <b>Cap rate</b>, <b>Price</b>, or <b>Newest built</b>. Verify status against the source before acting.`;
buildRail();render();
}).catch(()=>{$('#count').textContent='failed to load /data/listings.json';});
</script> <script src="/global-search.js" defer></script>
← ae59100 auto-data-snapshot: 2026-08-12T06:18:56 (5 data files) — dat
·
back to Commercialrealestate
·
auto-data-snapshot: 2026-08-12T07:20:53 (1 data files) — dat 6d53e85 →