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:
martbost
2026-09-09 15:45:49 -05:00
parent 2ac6549171
commit b86dbfd9e9
3 changed files with 79 additions and 1 deletions
+44
View File
@@ -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;