Ad reporting + featured rotates one link + clear campaign form on submit

- Ad reporting (auto-approved ads need a safety valve): new reports.js store
  (dual-mode) + ad_reports table + POST /api/report-ad; a "⚠ report" control on
  served ad slots opens a reason prompt and notifies the admin by email
  (siteConfig.adminEmail / ADMIN_EMAIL).
- Featured links now show ONE link at a time and cycle (true rotation), instead
  of listing all links at once.
- Campaign form now clears EVERY field on submit (target/creative/title/etc.).
  A leftover target URL was carrying into the next campaign — the same reason a
  video short was saved with the wrong (massifly) CTA target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-07 07:57:12 -05:00
parent ea1b2ea0ac
commit 4991fe96e8
11 changed files with 133 additions and 18 deletions
+19 -3
View File
@@ -96,6 +96,21 @@ window.IAP = (function () {
return div;
}
// Render one served ad into #<elId>. Silent if no inventory.
// report an ad (auto-approved ads need a member-facing flag → admin notified)
function reportAd(campaignId) {
if (!campaignId) return;
const reason = (prompt('Report this ad. Reason: broken, inappropriate, spam, scam, or other', 'broken') || '').trim().toLowerCase();
if (!reason) return;
const note = prompt('Anything to add? (optional)') || '';
fetch('/api/report-ad', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ campaignId, reason, note }) })
.then(() => status('Thanks — this ad was reported to the admin for review.', 'ok'))
.catch(() => status('Could not send the report. Try again.', 'bad'));
}
const reportTag = ad => ' <a class="ad-report small muted" href="#" data-cid="' + ad.id + '" style="margin-left:8px">⚠ report</a>';
function wireReport(el) {
const rl = el.querySelector('.ad-report');
if (rl) rl.addEventListener('click', e => { e.preventDefault(); reportAd(Number(rl.dataset.cid)); });
}
async function adSlot(type, elId) {
try {
const { ad } = await (await fetch('/api/ads/slot?type=' + type)).json();
@@ -104,13 +119,14 @@ window.IAP = (function () {
if (ad.imageUrl) {
el.innerHTML = '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow">'
+ '<img src="' + ad.imageUrl + '" alt="advertisement" style="max-width:100%;border-radius:8px"></a>'
+ '<div class="small muted">member ad</div>';
+ '<div class="small muted">member ad' + reportTag(ad) + '</div>';
} else {
el.innerHTML = '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow"><b>' + ad.title + '</b>'
+ (ad.body ? ' · ' + ad.body : '') + '</a> <span class="small muted">member ad</span>';
+ (ad.body ? ' · ' + ad.body : '') + '</a> <span class="small muted">member ad' + reportTag(ad) + '</span>';
}
wireReport(el);
el.hidden = false;
} catch (e) {}
}
return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, $ };
return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, reportAd, $ };
})();
+18 -7
View File
@@ -28,16 +28,24 @@
return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.';
}
// featured rotation strip on the overview
let featItems = [], featIdx = 0, featTimer = null;
function renderOneFeatured() {
if (!featItems.length) return;
const i = featItems[featIdx % featItems.length];
$('featStrip').innerHTML = '<a href="' + i.url + '" target="_blank" rel="noopener nofollow"><span>' + esc(i.title) + '</span>'
+ (i.by ? '<span class="by">' + esc(i.by) + '</span>' : '') + '</a>';
}
async function loadFeatured() {
try {
const r = await (await fetch('/api/featured')).json();
const card = $('featuredCard'); if (!card) return;
if (!r.items || !r.items.length) { card.hidden = true; return; }
card.hidden = false;
featItems = r.items; featIdx = Math.floor(Math.random() * featItems.length);
$('featSub').textContent = r.items.length + ' link' + (r.items.length === 1 ? '' : 's') + ' in rotation';
$('featStrip').innerHTML = r.items.map(i =>
'<a href="' + i.url + '" target="_blank" rel="noopener nofollow"><span>' + esc(i.title) + '</span>'
+ (i.by ? '<span class="by">' + esc(i.by) + '</span>' : '') + '</a>').join('');
renderOneFeatured(); // show ONE at a time (true rotation), cycle if more than one
clearInterval(featTimer);
if (featItems.length > 1) featTimer = setInterval(() => { featIdx++; renderOneFeatured(); }, 6000);
} catch (e) {}
}
@@ -665,11 +673,14 @@
ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value,
budget: isFeat ? (Number($('cFeatDays').value) * (lastRates.featuredPerDay || 40)) : Number($('cBudget').value) });
IAP.status('Campaign is live. It starts serving right away.', 'ok');
$('cName').value = ''; $('cBudget').value = '';
$('cSoloEd').innerHTML = ''; $('cSoloRaw').value = ''; $('cCtaLabel').value = '';
$('cVideoUrl').value = ''; $('cVideoTitle').value = ''; $('cVideoCta').value = '';
// clear EVERY field so no target/creative carries into the next campaign
['cName', 'cBudget', 'cTarget', 'cImage', 'cTitle', 'cBody', 'cCtaLabel',
'cVideoUrl', 'cVideoTitle', 'cVideoCta', 'cVisitTitle', 'cVisitCount', 'cFeatTitle']
.forEach(id => { if ($(id)) $(id).value = ''; });
$('cSoloEd').innerHTML = ''; if ($('cSoloRaw')) $('cSoloRaw').value = '';
$('cVideoInfo').textContent = ''; $('cVideoPrev').hidden = true; $('cVideoPrev').innerHTML = '';
$('edMediaInfo').textContent = '';
if ($('edMediaInfo')) $('edMediaInfo').textContent = '';
cVidDims = null;
await loadCampaigns();
}));
// defers the busy() lookup to click time (busy is declared below)