← back to Rentv 2026
public/ad-slot.js
66 lines
/* RENTV ad-slot serving include — drop-in, zero deps.
*
* Usage on any page:
* <div data-ad-slot="home-leaderboard"></div>
* <script src="/ad-slot.js" defer></script>
*
* For each [data-ad-slot="<id>"] it GETs /api/ad/slot/<id> and renders:
* • SOLD/RESERVED with a creative → the advertiser's banner (image + click-through, rel=nofollow sponsored)
* • otherwise → a house "Your ad here — $X/mo" promo that links to /advertise?slot=<id>
* So an unsold slot still earns by advertising the slot itself.
*/
(function () {
var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); };
var money = function (n) { return '$' + Number(n || 0).toLocaleString('en-US'); };
function css() {
if (document.getElementById('ad-slot-css')) return;
var s = document.createElement('style'); s.id = 'ad-slot-css';
s.textContent = [
'.adslot{display:block;position:relative;overflow:hidden;border-radius:8px;min-height:60px}',
'.adslot .ad-house{display:flex;align-items:center;justify-content:center;gap:12px;text-align:center;',
' min-height:90px;padding:14px 18px;border:1px dashed rgba(120,130,145,.45);border-radius:8px;',
' background:repeating-linear-gradient(135deg,rgba(200,16,46,.04),rgba(200,16,46,.04) 10px,transparent 10px,transparent 20px);',
' color:#5b636b;font:600 13px/1.35 Inter,system-ui,sans-serif}',
'.adslot .ad-house b{color:#c8102e;font-weight:800}',
'.adslot .ad-house .cta{margin-left:6px;background:#c8102e;color:#fff;font-weight:800;font-size:11px;',
' text-transform:uppercase;letter-spacing:.05em;padding:6px 12px;border-radius:5px;white-space:nowrap}',
'.adslot .ad-live img{display:block;width:100%;height:auto}',
'.adslot .ad-flag{position:absolute;top:6px;right:6px;background:rgba(11,14,18,.8);color:#fff;',
' font:700 9px/1 Inter;letter-spacing:.1em;text-transform:uppercase;padding:3px 6px;border-radius:3px}'
].join('');
document.head.appendChild(s);
}
function render(el, slot) {
el.classList.add('adslot');
var sold = slot && (slot.status === 'sold' || slot.status === 'reserved') && slot.creative_url;
if (sold) {
var click = slot.click_url || '/advertise?slot=' + encodeURIComponent(slot.id);
el.innerHTML = '<a class="ad-live" href="' + esc(click) + '" target="_blank" rel="nofollow sponsored noopener" ' +
'title="Sponsored by ' + esc(slot.advertiser || 'advertiser') + '">' +
'<span class="ad-flag">Sponsored</span>' +
'<img loading="lazy" alt="' + esc(slot.advertiser || 'Sponsor') + '" src="' + esc(slot.creative_url) + '"></a>';
return;
}
// house promo for an available slot — this IS the "slots for sale" pitch, inline
var price = slot ? money(slot.price_monthly) : '';
var name = slot ? esc(slot.name) : 'Ad space';
el.innerHTML = '<a class="ad-house" href="/advertise?slot=' + encodeURIComponent(slot ? slot.id : '') + '">' +
'<span>📣 <b>' + name + '</b> — your brand here' + (price ? ', <b>' + price + '/mo</b>' : '') + '</span>' +
'<span class="cta">Advertise →</span></a>';
}
function fill(el) {
var id = el.getAttribute('data-ad-slot'); if (!id) return;
fetch('/api/ad/slot/' + encodeURIComponent(id), { credentials: 'same-origin' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (j) { render(el, (j && j.slot) || { id: id, name: 'Ad space', status: 'available' }); })
.catch(function () { render(el, { id: id, name: 'Ad space', status: 'available' }); });
}
function boot() { css(); Array.prototype.forEach.call(document.querySelectorAll('[data-ad-slot]'), fill); }
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot();
window.RENTVAdSlot = { refill: boot };
})();