Wall ownership ladder: positions 2 and 3 become the member's own at 2 / 5 qualifying buyers

- accounts.wall_offers (JSON, up to 2 offers: label, https link, banner) set from
  Profile > Your wall; upload supported; locks show the buyer threshold.
- /api/wall assembles: position 1 = own line banner; positions 2-3 = own offer
  when unlocked and set, else upline banners (only uplines with a live banner),
  else house ads. Response carries unlocked + buyerCount; wall labels show
  "this wall" / "their line" / "InstantAdPay".
- Chatbot canned answer + facts, follow-up email 7 mention the ladder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-09 15:31:45 -05:00
parent 8b25361eef
commit 2ac6549171
21 changed files with 192 additions and 69 deletions
+38
View File
@@ -1532,8 +1532,46 @@
const a = await (await fetch('/api/me')).json();
fillLineBanner(a);
fillProfileDetails(a);
// buyerCount + wallUnlocked live on the dashboard payload (chain read), not on /api/me
let d = {}; try { d = await (await fetch('/api/my/dashboard')).json(); } catch (e) {}
fillWallOffers(Object.assign({}, a, { buyerCount: d.buyerCount || 0, wallUnlocked: d.wallUnlocked || 1 }));
} catch (e) {}
}
// ── wall positions 2 & 3: the member's own offers, unlocked by qualifying buyers ──
function fillWallOffers(a) {
if (!a || !$('wallOffersCard')) return;
const unlocked = a.wallUnlocked || 1, bc = a.buyerCount || 0;
const offers = Array.isArray(a.wallOffers) ? a.wallOffers : [];
const NEED = [2, 5];
for (let i = 0; i < 2; i++) {
const o = offers[i] || {};
const open = unlocked >= i + 2;
$('woTitle' + i).value = o.title || ''; $('woTarget' + i).value = o.targetUrl || ''; $('woBanner' + i).value = o.bannerUrl || '';
$('woPrev' + i).hidden = !o.bannerUrl; $('woPrev' + i).innerHTML = o.bannerUrl ? '<img src="' + o.bannerUrl + '" alt="">' : '';
$('woLock' + i).textContent = open ? 'yours' : 'unlocks at ' + NEED[i] + ' qualifying buyers (' + bc + '/' + NEED[i] + ')';
$('woSlot' + i).classList.toggle('locked', !open);
}
$('woStatus').textContent = unlocked >= 3 ? 'Fully qualified: all three wall positions are yours.'
: unlocked === 2 ? 'Position 2 is yours. ' + (5 - bc) + ' more qualifying buyer' + (5 - bc === 1 ? '' : 's') + ' and position 3 is too.'
: (2 - bc) + ' more qualifying buyer' + (2 - bc === 1 ? '' : 's') + ' ($20 or more) opens position 2. You can set your links now; they go live the moment a slot unlocks.';
}
document.querySelectorAll('.wo-upload').forEach(b => b.addEventListener('click', () => { const f = document.querySelector('.wo-file[data-slot="' + b.dataset.slot + '"]'); if (f) f.click(); }));
document.querySelectorAll('.wo-file').forEach(inp => inp.addEventListener('change', async () => {
const i = inp.dataset.slot, f = inp.files[0]; if (!f) return;
$('woInfo' + i).textContent = 'Uploading…';
try {
const r = await (await fetch('/api/my/upload', { method: 'POST', headers: { 'Content-Type': f.type }, body: f })).json();
if (r.error) { $('woInfo' + i).textContent = r.error; }
else { $('woBanner' + i).value = r.url; $('woInfo' + i).textContent = 'Uploaded'; $('woPrev' + i).hidden = false; $('woPrev' + i).innerHTML = '<img src="' + r.url + '" alt="">'; }
} catch (e) { $('woInfo' + i).textContent = 'Upload failed. Try again.'; }
inp.value = '';
}));
if ($('woSaveBtn')) $('woSaveBtn').addEventListener('click', busy2($('woSaveBtn'), async () => {
const offers = [0, 1].map(i => ({ title: $('woTitle' + i).value, targetUrl: $('woTarget' + i).value, bannerUrl: $('woBanner' + i).value }));
const r = await api('/api/my/wall-offers', { offers });
IAP.status('Wall positions saved.', 'ok');
await loadLineBanner();
}));
const SOCIALS = ['facebook', 'twitter', 'youtube', 'instagram', 'tiktok', 'telegram', 'linkedin', 'website'];
function fillProfileDetails(a) {
if (!a) return;
+5
View File
@@ -610,3 +610,8 @@ img{max-width:100%}
.lin-row .earn{font-family:var(--mono);font-size:12px;color:var(--muted);white-space:nowrap;font-variant-numeric:tabular-nums}
.lin-row .earn.on{color:var(--mint);font-weight:700}
.wo-slot{border:1px solid var(--line);border-radius:12px;padding:12px 14px}
.wo-slot.locked{opacity:.6}
.wo-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}
.wo-slot img{max-width:100%;border-radius:8px}
+1 -1
View File
@@ -64,7 +64,7 @@
const creative = m.bannerUrl
? '<img src="' + m.bannerUrl + '" alt="' + safe + ' banner">'
: '<b>' + safe + '</b><br><span class="muted small">' + (m.targetUrl ? 'visit their site' : 'banner slot open') + '</span>';
d.innerHTML = '<div class="wall-pos">Position ' + (i + 1) + (i === 0 ? ' · this wall' : '') + '</div>'
d.innerHTML = '<div class="wall-pos">Position ' + (i + 1) + (m.own ? ' · this wall' : m.admin ? ' · InstantAdPay' : ' · their line') + '</div>'
+ '<div class="wc-creative">' + creative + '</div>'
+ '<div class="wc-action"></div>'
+ '<div class="small muted" style="margin-top:8px">' + safe + '</div>';