← 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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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 };
})();