Admin: wall fallback ads editor (House ads pane) + /api/admin/wall-ads
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+11
-1
@@ -199,6 +199,16 @@
|
||||
<div class="card-head"><h3>House ads</h3><span class="sub" id="houseSub">running free</span></div>
|
||||
<div class="tablewrap"><table class="adm-table" id="houseTable"></table></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Wall fallback ads</h3><span class="sub" id="wallAdsSub">shown on member walls in positions they have not earned or filled, when no upline banner exists</span></div>
|
||||
<p class="small muted">A member's wall has three positions. Position 1 is theirs. Positions 2 and 3 show an upline's banner until the member earns them (2 and 5 qualifying buyers); when there is no upline banner, one of these ads shows instead. They rotate in order across walls. Leave the list empty to fall back to a plain InstantAdPay card.</p>
|
||||
<div id="wallAdsList" class="drip-steps"></div>
|
||||
<p style="display:flex;gap:10px;flex-wrap:wrap;margin:12px 0 0">
|
||||
<button class="btn small" id="wallAdsSave" type="button">Save wall ads</button>
|
||||
<button class="btn small sec" id="wallAdsAdd" type="button">+ Add a wall ad</button>
|
||||
</p>
|
||||
<p id="wallAdsErr" class="small" style="color:var(--bad)" hidden></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pane" id="pane-campaigns" hidden>
|
||||
@@ -271,6 +281,6 @@
|
||||
</div>
|
||||
|
||||
<script src="/assets/common.js?v=20260909a"></script>
|
||||
<script src="/assets/admin.js?v=20260909c"></script>
|
||||
<script src="/assets/admin.js?v=20260909d"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -192,10 +192,54 @@
|
||||
if (!$('hWatchSecs').options.length) $('hWatchSecs').innerHTML = (rates.videoTiers || []).map(t => '<option value="' + t.secs + '">Watch ' + t.secs + 's (viewer earns ' + t.reward + ')</option>').join('');
|
||||
if (!$('hFeatDays').options.length) $('hFeatDays').innerHTML = (rates.featuredDurations || [1, 2, 7]).map(d => '<option value="' + d + '">' + d + ' day' + (d > 1 ? 's' : '') + '</option>').join('');
|
||||
showHouseRows();
|
||||
loadWallAds();
|
||||
const house = (r.campaigns || []).filter(c => c.house);
|
||||
$('houseSub').textContent = house.filter(c => c.status === 'active').length + ' active · ' + house.length + ' total';
|
||||
$('houseTable').innerHTML = house.length ? campHead(false) + house.map(c => campRow(c, false)).join('') : '<tr><td class="muted">No house ads yet. Place one above.</td></tr>';
|
||||
}
|
||||
// wall fallback ads editor
|
||||
let wallAds = [];
|
||||
function drawWallAds() {
|
||||
const w = $('wallAdsList');
|
||||
w.innerHTML = wallAds.map((a, i) => '<div class="drip-step" data-i="' + i + '"><div class="ds-head"><span class="ds-n">WALL AD ' + (i + 1) + '</span>'
|
||||
+ '<span class="ds-tools"><button type="button" class="btn small sec" data-wact="up" ' + (i === 0 ? 'disabled' : '') + '>↑</button><button type="button" class="btn small sec" data-wact="down" ' + (i === wallAds.length - 1 ? 'disabled' : '') + '>↓</button><button type="button" class="btn small sec" data-wact="remove">Remove</button></span></div>'
|
||||
+ '<div class="grid c3"><p><input class="wa-name" maxlength="60" placeholder="Label shown under the ad" value="' + esc(a.name || '') + '"></p>'
|
||||
+ '<p><input class="wa-target" placeholder="Link (https://…)" value="' + esc(a.targetUrl || '') + '"></p>'
|
||||
+ '<p><input class="wa-banner" placeholder="Banner image URL or upload" value="' + esc(a.bannerUrl || '') + '"> <button type="button" class="btn small sec wa-upload">Upload</button><input type="file" class="wa-file" accept="image/png,image/jpeg,image/webp,image/gif" hidden></p></div>'
|
||||
+ (a.bannerUrl ? '<img src="' + esc(a.bannerUrl) + '" alt="" style="max-height:60px;border-radius:6px">' : '')
|
||||
+ '</div>').join('') || '<p class="muted small">No wall ads set. Walls fall back to a plain InstantAdPay card.</p>';
|
||||
}
|
||||
function readWallAds() {
|
||||
return [...document.querySelectorAll('#wallAdsList .drip-step')].map(c => ({ name: c.querySelector('.wa-name').value.trim(), targetUrl: c.querySelector('.wa-target').value.trim(), bannerUrl: c.querySelector('.wa-banner').value.trim() }));
|
||||
}
|
||||
async function loadWallAds() {
|
||||
try { const r = await api('/api/admin/wall-ads'); wallAds = r.ads || []; $('wallAdsSub').textContent = r.usingDefaults ? 'none set: walls show the default InstantAdPay card' : wallAds.length + ' in rotation'; drawWallAds(); } catch (e) {}
|
||||
}
|
||||
$('wallAdsList').addEventListener('click', async e => {
|
||||
const up = e.target.closest('.wa-upload');
|
||||
if (up) { up.parentElement.querySelector('.wa-file').click(); return; }
|
||||
const b = e.target.closest('[data-wact]'); if (!b) return;
|
||||
const i = Number(b.closest('.drip-step').dataset.i); wallAds = readWallAds();
|
||||
if (b.dataset.wact === 'remove') wallAds.splice(i, 1);
|
||||
if (b.dataset.wact === 'up' && i > 0) [wallAds[i - 1], wallAds[i]] = [wallAds[i], wallAds[i - 1]];
|
||||
if (b.dataset.wact === 'down' && i < wallAds.length - 1) [wallAds[i + 1], wallAds[i]] = [wallAds[i], wallAds[i + 1]];
|
||||
drawWallAds();
|
||||
});
|
||||
$('wallAdsList').addEventListener('change', async e => {
|
||||
const f = e.target.closest('.wa-file'); if (!f || !f.files[0]) return;
|
||||
const file = f.files[0]; const card = f.closest('.drip-step');
|
||||
try {
|
||||
const r = await (await fetch('/api/admin/upload', { method: 'POST', headers: { 'Content-Type': file.type }, body: file })).json();
|
||||
if (r.error) IAP.status(r.error, 'bad'); else { card.querySelector('.wa-banner').value = r.url; IAP.status('Uploaded.', 'ok'); }
|
||||
} catch (err) { IAP.status('Upload failed.', 'bad'); }
|
||||
f.value = '';
|
||||
});
|
||||
$('wallAdsAdd').addEventListener('click', () => { wallAds = readWallAds(); wallAds.push({ name: '', targetUrl: '', bannerUrl: '' }); drawWallAds(); });
|
||||
$('wallAdsSave').addEventListener('click', busy($('wallAdsSave'), async () => {
|
||||
$('wallAdsErr').hidden = true;
|
||||
try { const r = await api('/api/admin/wall-ads', { ads: readWallAds() }, 'PATCH'); wallAds = r.ads || []; drawWallAds(); IAP.status('Wall ads saved.', 'ok'); await loadWallAds(); }
|
||||
catch (e) { $('wallAdsErr').textContent = e.message; $('wallAdsErr').hidden = false; }
|
||||
}));
|
||||
document.addEventListener('click', async e => {
|
||||
const b = e.target.closest('[data-act][data-id]'); if (!b) return;
|
||||
b.disabled = true;
|
||||
|
||||
Reference in New Issue
Block a user