[object Object]

← back to Marketing Command Center

IG activity panel: grid/list toggle + sort by all columns (dropdown + clickable headers)

1ec9940a2b5169fc7cf40ce819b9053a70fec2a5 · 2026-08-11 08:28:50 -0700 · Steve Abrams

Files touched

Diff

commit 1ec9940a2b5169fc7cf40ce819b9053a70fec2a5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 11 08:28:50 2026 -0700

    IG activity panel: grid/list toggle + sort by all columns (dropdown + clickable headers)
---
 public/ig-activity.html | 123 +++++++++++++++++++++++++++++++-----------------
 public/ig-activity.json |  96 +++++++++++++++++--------------------
 2 files changed, 123 insertions(+), 96 deletions(-)

diff --git a/public/ig-activity.html b/public/ig-activity.html
index 88ae127..50d7292 100644
--- a/public/ig-activity.html
+++ b/public/ig-activity.html
@@ -20,6 +20,11 @@
   .toolbar label{color:var(--mut);font-size:12px;text-transform:uppercase;letter-spacing:.08em;margin-right:8px}
   select,input[type=range]{accent-color:var(--accent)}
   select{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px}
+  .seg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}
+  .seg button{background:var(--card);color:var(--mut);border:0;padding:7px 14px;font-size:13px;cursor:pointer}
+  .seg button.on{background:var(--accent);color:#000;font-weight:600}
+  #densityWrap.hide{display:none}
+  /* GRID */
   .grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:16px;padding:22px 28px 60px}
   .card{background:var(--card);border:1px solid var(--line);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;transition:border-color .15s,transform .15s}
   .card:hover{border-color:var(--accent);transform:translateY(-2px)}
@@ -32,8 +37,16 @@
   .foot{display:flex;justify-content:space-between;align-items:center;margin-top:9px;padding-top:9px;border-top:1px solid var(--line)}
   .view{font-size:12px;color:var(--accent);text-decoration:none;border:1px solid var(--line);border-radius:6px;padding:4px 9px}
   .view:hover{background:var(--accent);color:#000;border-color:var(--accent)}
+  /* LIST / TABLE */
+  table{width:calc(100% - 56px);margin:22px 28px 60px;border-collapse:collapse;font-size:13px}
+  th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--line);vertical-align:middle}
+  th{color:var(--mut);text-transform:uppercase;letter-spacing:.06em;font-size:11px;cursor:pointer;user-select:none;white-space:nowrap}
+  th:hover{color:var(--accent)}
+  th .arw{color:var(--accent);font-size:10px}
+  td img{width:44px;height:44px;object-fit:cover;border-radius:6px;display:block;background:#111}
+  td a.acct{color:var(--accent)}
+  tr:hover td{background:#161410}
   .empty{color:var(--mut);text-align:center;padding:80px 20px}
-  @media(max-width:640px){ .grid{--cols:2 !important} }
 </style>
 </head>
 <body>
@@ -47,69 +60,95 @@
   </div>
 </header>
 <div class="toolbar">
+  <div><label>View</label>
+    <span class="seg"><button id="v-grid" class="on">Grid</button><button id="v-list">List</button></span>
+  </div>
   <div><label>Sort</label>
     <select id="sort">
-      <option value="newest">Newest</option>
-      <option value="oldest">Oldest</option>
-      <option value="account">Account A→Z</option>
-      <option value="product">Product A→Z</option>
+      <option value="ts|desc">Newest</option>
+      <option value="ts|asc">Oldest</option>
+      <option value="handle|asc">Account A→Z</option>
+      <option value="handle|desc">Account Z→A</option>
+      <option value="product_title|asc">Product A→Z</option>
+      <option value="product_title|desc">Product Z→A</option>
+      <option value="kind|asc">Type A→Z</option>
     </select>
   </div>
-  <div><label>Density</label>
+  <div id="densityWrap"><label>Density</label>
     <input type="range" id="density" min="2" max="7" step="1">
   </div>
   <div class="sub" id="gen" style="margin-left:auto"></div>
 </div>
 <div id="grid" class="grid"></div>
+<table id="list" style="display:none"><thead><tr>
+  <th data-k="image_url">Image</th>
+  <th data-k="handle">Account <span class="arw"></span></th>
+  <th data-k="product_title">Product <span class="arw"></span></th>
+  <th data-k="ts">Posted <span class="arw"></span></th>
+  <th data-k="kind">Type <span class="arw"></span></th>
+  <th>Link</th>
+</tr></thead><tbody id="rows"></tbody></table>
 
 <script>
 const $=s=>document.querySelector(s);
 const fmt=iso=>{try{return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})}catch{return iso||''}};
-let DATA=[];
+let DATA=[], SORT=localStorage.getItem('ig_sort')||'ts|desc', MODE=localStorage.getItem('ig_mode')||'grid';
+function sorted(){
+  const [k,dir]=SORT.split('|'); const s=[...DATA];
+  s.sort((a,b)=>{ let x=a[k],y=b[k];
+    if(k==='ts'){x=new Date(x);y=new Date(y);}
+    else {x=(x||'').toString().toLowerCase();y=(y||'').toString().toLowerCase();}
+    return (x<y?-1:x>y?1:0)*(dir==='desc'?-1:1); });
+  return s;
+}
 function render(){
-  const sort=$('#sort').value;
-  const p=[...DATA];
-  if(sort==='newest')p.sort((a,b)=>new Date(b.ts)-new Date(a.ts));
-  if(sort==='oldest')p.sort((a,b)=>new Date(a.ts)-new Date(b.ts));
-  if(sort==='account')p.sort((a,b)=>(a.handle||'').localeCompare(b.handle||''));
-  if(sort==='product')p.sort((a,b)=>(a.product_title||'').localeCompare(b.product_title||''));
-  const g=$('#grid');
-  if(!p.length){g.innerHTML='<div class="empty">No posts logged yet. As the network posts, they appear here.</div>';return;}
-  g.innerHTML=p.map(x=>`
-    <div class="card">
-      <a href="${x.permalink||'#'}" target="_blank" rel="noopener noreferrer">
-        <span class="thumb" style="background-image:url('${(x.image_url||'').replace(/'/g,'')}')"></span></a>
-      <div class="body">
-        <a class="acct" href="https://www.instagram.com/${x.handle}/" target="_blank" rel="noopener noreferrer">@${x.handle}</a>
-        <div class="title" title="${(x.product_title||'').replace(/"/g,'&quot;')}">${x.product_title||'—'}</div>
-        <div class="when" title="${x.ts||''}">🕓 ${fmt(x.ts)}</div>
-        <div class="foot">
-          <span class="when">${x.kind||'IMAGE'}</span>
-          <a class="view" href="${x.permalink||'#'}" target="_blank" rel="noopener noreferrer">View post ↗</a>
+  const p=sorted();
+  const grid=$('#grid'), list=$('#list');
+  if(MODE==='grid'){ grid.style.display='grid'; list.style.display='none'; $('#densityWrap').classList.remove('hide');
+    grid.innerHTML = p.length? p.map(x=>`
+      <div class="card">
+        <a href="${x.permalink||'#'}" target="_blank" rel="noopener noreferrer"><span class="thumb" style="background-image:url('${(x.image_url||'').replace(/'/g,'')}')"></span></a>
+        <div class="body">
+          <a class="acct" href="https://www.instagram.com/${x.handle}/" target="_blank" rel="noopener noreferrer">@${x.handle}</a>
+          <div class="title" title="${(x.product_title||'').replace(/"/g,'&quot;')}">${x.product_title||'—'}</div>
+          <div class="when" title="${x.ts||''}">🕓 ${fmt(x.ts)}</div>
+          <div class="foot"><span class="when">${x.kind||'IMAGE'}</span>
+            <a class="view" href="${x.permalink||'#'}" target="_blank" rel="noopener noreferrer">View post ↗</a></div>
         </div>
-      </div>
-    </div>`).join('');
+      </div>`).join('') : '<div class="empty">No posts logged yet.</div>';
+  } else { grid.style.display='none'; list.style.display='table'; $('#densityWrap').classList.add('hide');
+    $('#rows').innerHTML = p.length? p.map(x=>`
+      <tr>
+        <td><a href="${x.permalink||'#'}" target="_blank" rel="noopener noreferrer"><img src="${x.image_url||''}" alt=""></a></td>
+        <td><a class="acct" href="https://www.instagram.com/${x.handle}/" target="_blank" rel="noopener noreferrer">@${x.handle}</a></td>
+        <td>${x.product_title||'—'}</td>
+        <td title="${x.ts||''}">🕓 ${fmt(x.ts)}</td>
+        <td>${x.kind||'IMAGE'}</td>
+        <td><a class="view" href="${x.permalink||'#'}" target="_blank" rel="noopener noreferrer">View ↗</a></td>
+      </tr>`).join('') : '<tr><td colspan="6" class="empty">No posts logged yet.</td></tr>';
+  }
+  // header arrows
+  const [ck,cd]=SORT.split('|');
+  document.querySelectorAll('th[data-k]').forEach(th=>{const a=th.querySelector('.arw'); if(a) a.textContent = th.dataset.k===ck?(cd==='desc'?'▼':'▲'):'';});
+  $('#sort').value=SORT;
 }
-function applyDensity(v){document.documentElement.style.setProperty('--cols',v);}
+function setMode(m){MODE=m;localStorage.setItem('ig_mode',m);$('#v-grid').classList.toggle('on',m==='grid');$('#v-list').classList.toggle('on',m==='list');render();}
+function setSort(s){SORT=s;localStorage.setItem('ig_sort',s);render();}
+$('#v-grid').onclick=()=>setMode('grid'); $('#v-list').onclick=()=>setMode('list');
+$('#sort').onchange=()=>setSort($('#sort').value);
+document.querySelectorAll('th[data-k]').forEach(th=>th.onclick=()=>{const k=th.dataset.k;const [ck,cd]=SORT.split('|');setSort(k+'|'+(ck===k&&cd==='asc'?'desc':'asc'));});
+const sDen=localStorage.getItem('ig_density')||'4'; $('#density').value=sDen; document.documentElement.style.setProperty('--cols',sDen);
+$('#density').oninput=()=>{const v=$('#density').value;localStorage.setItem('ig_density',v);document.documentElement.style.setProperty('--cols',v);};
 async function load(){
-  try{
-    const r=await fetch('ig-activity.json?t='+Date.now());
-    const j=await r.json();
-    DATA=j.posts||[];
-    $('#s-posts').textContent=j.total_posts??DATA.length;
+  try{ const r=await fetch('ig-activity.json?t='+Date.now()); const j=await r.json();
+    DATA=j.posts||[]; $('#s-posts').textContent=j.total_posts??DATA.length;
     $('#s-accts').textContent=j.accounts_touched??new Set(DATA.map(x=>x.handle)).size;
     $('#s-last').textContent=j.last_post_at?fmt(j.last_post_at):'–';
     $('#gen').textContent='Updated '+(j.generated_at?fmt(j.generated_at):'');
   }catch(e){DATA=[];}
-  render();
+  setMode(MODE); // applies mode + renders
 }
-// persisted controls
-const sSort=localStorage.getItem('ig_sort')||'newest';
-const sDen=localStorage.getItem('ig_density')||'4';
-$('#sort').value=sSort;$('#density').value=sDen;applyDensity(sDen);
-$('#sort').onchange=()=>{localStorage.setItem('ig_sort',$('#sort').value);render();};
-$('#density').oninput=()=>{const v=$('#density').value;localStorage.setItem('ig_density',v);applyDensity(v);};
-load(); setInterval(load,60000); // auto-refresh each minute
+load(); setInterval(load,60000);
 </script>
 </body>
 </html>
diff --git a/public/ig-activity.json b/public/ig-activity.json
index 92f8dbd..80e3301 100644
--- a/public/ig-activity.json
+++ b/public/ig-activity.json
@@ -1,105 +1,93 @@
 {
-  "generated_at": "2026-08-11T15:18:04.900Z",
-  "total_posts": 8,
-  "accounts_touched": 8,
-  "last_post_at": "2026-08-11T08:13:00-07:00",
+  "generated_at": "2026-08-11T15:26:27.480Z",
+  "total_posts": 7,
+  "accounts_touched": 7,
+  "last_post_at": "2026-08-11T15:26:27.447Z",
   "by_account": {
     "screenprintedwallpaper": 1,
     "metallicwallpaper": 1,
-    "goldleafwallpaper": 1,
-    "linenwallpaper": 1,
     "grassclothwallpaper": 1,
-    "velvetwallpaper": 1,
+    "linenwallpaper": 1,
     "suedewallpaper": 1,
+    "velvetwallpaper": 1,
     "textilewallpaper": 1
   },
   "posts": [
     {
-      "ts": "2026-08-11T08:13:00-07:00",
+      "ts": "2026-08-11T15:26:27.447Z",
       "handle": "screenprintedwallpaper",
       "page_name": "Screen Printed Wallpaper",
       "product_handle": "regal-lattice-screen-printed-wallpaper-tre-12907",
       "product_title": "Regal Lattice - Screen Printed Wallcovering",
-      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/3547fe79068d43fd86725b501b7c7f01.jpg",
-      "permalink": "https://www.instagram.com/p/Db54ExfmxI5/",
-      "media_id": null,
+      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/3547fe79068d43fd86725b501b7c7f01.jpg?v=1572309178",
+      "permalink": "https://www.instagram.com/p/Db56DKZG-Ur/",
+      "media_id": "18023951597891610",
       "kind": "IMAGE"
     },
     {
-      "ts": "2026-08-11T08:12:00-07:00",
+      "ts": "2026-08-11T15:26:14.155Z",
       "handle": "metallicwallpaper",
       "page_name": "Metallic Wallpaper",
       "product_handle": "showgirls-wallpaper",
       "product_title": "Showgirls Metallic Gold & Black Wallcovering | Graduate Collection",
-      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/ScreenShot2023-09-11at1.53.19PM.png",
-      "permalink": "https://www.instagram.com/p/Db54BKHG8Sv/",
-      "media_id": null,
+      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/ScreenShot2023-09-11at1.53.19PM.png?v=1694465646",
+      "permalink": "https://www.instagram.com/p/Db56Bn6m_al/",
+      "media_id": "17891509140656955",
       "kind": "IMAGE"
     },
     {
-      "ts": "2026-08-11T08:12:00-07:00",
-      "handle": "goldleafwallpaper",
-      "page_name": "Gold Leaf Wallpaper",
-      "product_handle": "dwtt-80525-designer-wallcoverings-los-angeles",
-      "product_title": "Metal Leaf Metallic Gold - Gold Wallcovering | Thibaut",
-      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T41011_e0cd5049-136c-456e-b07d-a26bbb77e65b.jpg",
-      "permalink": "https://www.instagram.com/p/Db54C7Cm-G_/",
-      "media_id": null,
+      "ts": "2026-08-11T15:26:01.248Z",
+      "handle": "grassclothwallpaper",
+      "page_name": "Wallpaper Weekly",
+      "product_handle": "papalo-paper-grasscloth-prg-71096",
+      "product_title": "Papalo Paper Grasscloth | Phillipe Romano",
+      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/cf484d7a1d1bd1eeba8b00c589a2453d.jpg?v=1743032177",
+      "permalink": "https://www.instagram.com/p/Db55_9RG1nJ/",
+      "media_id": "17904217761531860",
       "kind": "IMAGE"
     },
     {
-      "ts": "2026-08-11T07:56:00-07:00",
+      "ts": "2026-08-11T15:25:48.307Z",
       "handle": "linenwallpaper",
       "page_name": "Linen Wallpaper",
       "product_handle": "pure-elements-sisal-linen",
       "product_title": "Pure Elements Sisal Linen Wallcoverings",
-      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/0075624_sisal_650.jpg",
-      "permalink": "https://www.instagram.com/p/Db50HyfG93k/",
-      "media_id": null,
+      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/0075624_sisal_650.jpg?v=1783366838",
+      "permalink": "https://www.instagram.com/p/Db55-UAGzTZ/",
+      "media_id": "18333064030260099",
       "kind": "IMAGE"
     },
     {
-      "ts": "2026-08-11T07:56:00-07:00",
-      "handle": "grassclothwallpaper",
-      "page_name": "Wallpaper Weekly",
-      "product_handle": "papalo-paper-grasscloth-prg-71096",
-      "product_title": "Papalo Paper Grasscloth | Phillipe Romano",
-      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/cf484d7a1d1bd1eeba8b00c589a2453d.jpg",
-      "permalink": "https://www.instagram.com/p/Db50Jmnm-7m/",
-      "media_id": null,
+      "ts": "2026-08-11T15:25:34.527Z",
+      "handle": "suedewallpaper",
+      "page_name": "Suede Wallpaper",
+      "product_handle": "prw-920205-cts-07",
+      "product_title": "Ciudad Suede Natural | Phillipe Romano",
+      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/prw-920205-cts-07.jpg?v=1781716927",
+      "permalink": "https://www.instagram.com/p/Db558rwm6s0/",
+      "media_id": "18141986509562672",
       "kind": "IMAGE"
     },
     {
-      "ts": "2026-08-11T07:55:00-07:00",
+      "ts": "2026-08-11T15:25:20.929Z",
       "handle": "velvetwallpaper",
       "page_name": "Velvet Wallpaper",
       "product_handle": "veronicas-madison-flocked-velvet-wallpaper-4",
       "product_title": "Veronica's Madison Flocked Velvet Wallcovering",
-      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/VV218_madison_MAROON_1056x_8d36f6e3-f1df-410f-a0a6-f8c80c8ac535.webp",
-      "permalink": "https://www.instagram.com/p/Db50Endm9JZ/",
-      "media_id": null,
-      "kind": "IMAGE"
-    },
-    {
-      "ts": "2026-08-11T07:55:00-07:00",
-      "handle": "suedewallpaper",
-      "page_name": "Suede Wallpaper",
-      "product_handle": "prw-920205-cts-07",
-      "product_title": "Ciudad Suede Natural | Phillipe Romano",
-      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/prw-920205-cts-07.jpg",
-      "permalink": "https://www.instagram.com/p/Db50GKmG_k8/",
-      "media_id": null,
+      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/VV218_madison_MAROON_1056x_8d36f6e3-f1df-410f-a0a6-f8c80c8ac535.webp?v=1734414936",
+      "permalink": "https://www.instagram.com/p/Db556-amys3/",
+      "media_id": "18136476730524343",
       "kind": "IMAGE"
     },
     {
-      "ts": "2026-08-11T07:52:00-07:00",
+      "ts": "2026-08-11T15:25:07.634Z",
       "handle": "textilewallpaper",
       "page_name": "Textile Wallpaper",
       "product_handle": "wick-submerge-maharam",
       "product_title": "Wick Submerge | Maharam",
-      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/MH-WICK-021_0.jpg",
-      "permalink": "https://www.instagram.com/p/Db5zlrhG1xF/",
-      "media_id": null,
+      "image_url": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/MH-WICK-021_0.jpg?v=1786455296",
+      "permalink": "https://www.instagram.com/p/Db555ZTG9KG/",
+      "media_id": "17978408325081647",
       "kind": "IMAGE"
     }
   ]

← 1ccbe05 Add IG Network Activity panel (what's posted + where, per-ca  ·  back to Marketing Command Center  ·  auto-data-snapshot: 2026-08-11T09:10:52 (1 data files) — pub fb98d9a →