diff --git a/public/assets/my.js b/public/assets/my.js index 7bd0373..69bb540 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -225,6 +225,43 @@ } // ── overview v3 charts: hand-rolled SVG/CSS, real data only ── const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); + + // Paged lists (Marty, 2026-09-19): 25 rows, then a clearly marked next page, on every list + // that grows with the membership. Works on what is already rendered: rows past the page are + // hidden and a control bar goes under the list, so a row's buttons and bindings never change. + // key remembers the page across re-renders of the same list + // host the element the bar is appended to (a wrapper, never a ) + // rowSel the rows, queried inside host + // opts per (default 25); tail: a selector for a drawer that follows its row and pages + // with it; focus: a row predicate, opens on whichever page holds that row + const PAGE_SIZE = 25; + const PAGE_AT = {}; + function pageList(key, host, rowSel, opts) { + if (!host) return; opts = opts || {}; const per = opts.per || PAGE_SIZE; + const rows = [...host.querySelectorAll(rowSel)]; + const old = host.querySelector(':scope > .pgr'); if (old) old.remove(); + if (rows.length <= per) { rows.forEach(r => { r.hidden = false; }); return; } + const pages = Math.ceil(rows.length / per); + let p = PAGE_AT[key] || 1; + if (opts.focus) { const i = rows.findIndex(opts.focus); if (i >= 0) p = Math.floor(i / per) + 1; } + p = Math.min(Math.max(1, p), pages); PAGE_AT[key] = p; + const start = (p - 1) * per; + rows.forEach((r, i) => { + const on = i >= start && i < start + per; r.hidden = !on; + if (opts.tail) { const t = r.nextElementSibling; if (t && t.matches(opts.tail) && !on && !t.hidden) { t.hidden = true; const b = r.querySelector('[aria-expanded]'); if (b) { b.setAttribute('aria-expanded', 'false'); b.textContent = 'Activity \u25BE'; } } } + }); + const bar = document.createElement('div'); bar.className = 'pgr'; + bar.innerHTML = 'Showing ' + (start + 1) + '\u2013' + Math.min(start + per, rows.length) + ' of ' + rows.length + '' + + '' + + 'Page ' + p + ' of ' + pages + '' + + ''; + bar.querySelectorAll('[data-go]').forEach(b => b.addEventListener('click', () => { + PAGE_AT[key] = Number(b.dataset.go); + pageList(key, host, rowSel, Object.assign({}, opts, { focus: null })); + host.scrollIntoView({ behavior: 'smooth', block: 'start' }); + })); + host.appendChild(bar); + } const CH = { mint: '#43e8c3', cyan: '#54ccff', violet: '#9d7dff', amber: '#ffb238', track: 'rgba(139,166,156,.18)' }; function bars(el, xel, data) { // data: [{v,label,tip,alt}] if (!el) return; @@ -564,6 +601,7 @@ + '' + '').join(''); wrap.appendChild(t); + pageList('roster', wrap, 'table.roster tr'); } // overview recent-activity widget: chain events + line joins, newest first try { @@ -831,6 +869,7 @@ + r.items.map(i => '').join('') + '
' + new Date(r.joined).toLocaleDateString() + '' + r.status + '
' + when(i.ts) + '' + (i.delta > 0 ? '+' : '') + i.delta.toLocaleString() + '' + esc(i.note) + '
'; el.appendChild(box); + pageList('credits', box, 'tbody tr'); } catch (e) {} } async function loadCampaigns() { @@ -883,6 +922,7 @@ : ' ') + '').join('') + ''; el.appendChild(tbl); + pageList('camps', tbl, 'tbody tr'); const note = document.createElement('p'); note.className = 'muted small'; note.textContent = 'Credits you put into a campaign are set aside the moment you start it, so your Available number drops right away and the campaign spends from its own budget. Featured links and verified-visit packs are paid in full up front; a featured run is sold by the day, so use Extend run to keep it going. Whatever a campaign has not spent comes back to Available when it ends.'; el.appendChild(note); @@ -1483,6 +1523,7 @@ $('pipeDue').innerHTML = r.due.length ? '
' + r.due.map(c => pipeCardHtml(c, true)).join('') + '
' : '

Nothing due. Set a follow-up date on any card and it shows up here.

'; $('pipeSummary').textContent = r.counts.total ? r.counts.total + ' people on your board' + (r.counts.stalled ? ', ' + r.counts.stalled + ' stalled' : '') + '. Cards move on their own when something happens on the ledger; open one to add a note, a follow-up date or a tag.' : 'Nobody on your board yet. Add prospects on My line, and everyone who joins through your link appears here on their own.'; $('pipeBoard').innerHTML = r.columns.map(col => '

' + esc(col.label) + '' + col.cards.length + '

' + esc(col.hint) + '

' + col.cards.map(c => pipeCardHtml(c, false)).join('') + '
').join(''); + $('pipeBoard').querySelectorAll('.pipe-col').forEach((col, i) => pageList('pipe-' + i, col, ':scope > [data-pk]')); $('pipeLive').querySelectorAll('[data-pk]').forEach(el => { el.addEventListener('click', () => openPipeCard(el.dataset.pk)); el.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openPipeCard(el.dataset.pk); } }); @@ -1599,6 +1640,8 @@ el.querySelectorAll('[data-cemail]').forEach(b => b.addEventListener('click', () => openConvo(b.dataset.cemail, b.dataset.cname))); el.querySelectorAll('[data-act]').forEach(b => b.addEventListener('click', () => toggleLineActivity(b))); + const focusRef = LINE_FOCUS_REF ? LINE_FOCUS_REF.replace(/"/g, '') : null; + el.querySelectorAll('.lin-lvl').forEach((lv, i) => pageList('line-' + i, lv, ':scope > .lin-row', { tail: '.lin-act', focus: focusRef ? row => !!row.querySelector('[data-act="' + focusRef + '"]') : null })); if (LINE_FOCUS_REF) { const ref = LINE_FOCUS_REF; LINE_FOCUS_REF = null; const b = el.querySelector('[data-act="' + ref.replace(/"/g, '') + '"]'); @@ -2657,6 +2700,7 @@ + (t.unread ? '' + t.unread + '' : '') + '').join(''); $('chatThreads').querySelectorAll('.chat-thread').forEach(el => el.addEventListener('click', () => openConvo(el.dataset.email, el.dataset.name))); + pageList('chat', $('chatThreads'), '.chat-thread'); } catch (e) { $('chatThreads').innerHTML = '
Could not load messages.
'; } } diff --git a/public/assets/site.css b/public/assets/site.css index bb527c6..f772af3 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -751,3 +751,12 @@ img{max-width:100%} /* Overview chips drill down to My line (Marty, 2026-09-15) */ .lt-node.lt-go{cursor:pointer;transition:border-color .15s,transform .15s} .lt-node.lt-go:hover,.lt-node.lt-go:focus-visible{border-color:var(--mint);transform:translateY(-1px);outline:none} .lin-row.lin-focus{box-shadow:0 0 0 2px var(--mint);border-radius:10px} +/* paged lists: 25 rows, then a control bar that reads as a control, not more list (Marty, 2026-09-19) */ +.pgr{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin:12px 0 4px;padding:10px 12px;border:1px solid var(--line-strong);border-radius:12px;background:rgba(67,232,195,.07)} +.pgr-n{color:var(--ink);font-size:13px} .pgr-n b{color:var(--mint);font-variant-numeric:tabular-nums} +.pgr-b{display:flex;align-items:center;gap:8px;flex-wrap:wrap} +.pgr-pg{font-size:11px;color:var(--muted);letter-spacing:.06em;text-transform:uppercase;white-space:nowrap} +.pgr-btn{background:var(--mint);color:var(--mint-ink);border:0;border-radius:999px;padding:8px 14px;font-weight:800;font-size:13px;cursor:pointer;white-space:nowrap} +.pgr-btn:hover{background:var(--mint-hi)} .pgr-btn[disabled]{opacity:.35;cursor:default} +.pipe-col .pgr{padding:8px;gap:6px;margin-top:8px} .pipe-col .pgr-btn{padding:6px 10px;font-size:12px} +#chatThreads .pgr{margin:8px 10px} diff --git a/public/my.html b/public/my.html index 8274de8..dae27ac 100644 --- a/public/my.html +++ b/public/my.html @@ -5,7 +5,7 @@ Member area | InstantAdPay - + @@ -1037,7 +1037,7 @@ - +