← back to Interiordesignershowroom

public/js/adminbar.js

78 lines

// Front-end admin "see-more" bar. Shown ONLY when the server has set the
// `ids_admin_ui` hint cookie (after a successful /admin Basic-auth). This cookie
// is a UI hint, NOT a security boundary — every mutation below still POSTs to the
// Basic-auth-gated /admin route, so a forged cookie reveals only the low-value
// metadata the storefront already carries and can change nothing.
(function () {
  function cookie(n) { var m = document.cookie.match('(?:^|; )' + n + '=([^;]*)'); return m ? m[1] : ''; }
  if (cookie('ids_admin_ui') !== '1') return;

  var KEY = 'ids_adminview';
  var on = localStorage.getItem(KEY) === '1';

  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }

  var bar = document.createElement('div');
  bar.className = 'admin-bar';
  bar.innerHTML =
    '<span class="ab-tag">🔧 Admin</span>'
    + '<label class="ab-toggle"><input type="checkbox" id="ab-on"' + (on ? ' checked' : '') + '><span>Admin view</span></label>'
    + '<a class="ab-link" href="/admin">Dashboard ↗</a>'
    + '<a class="ab-link" href="/admin/affiliates">Affiliates ↗</a>'
    + '<button type="button" class="ab-exit" id="ab-exit" title="Leave admin view on this device">Exit</button>';
  document.body.appendChild(bar);
  apply(on);

  document.getElementById('ab-on').addEventListener('change', function (e) {
    on = e.target.checked; localStorage.setItem(KEY, on ? '1' : '0'); apply(on);
  });
  document.getElementById('ab-exit').addEventListener('click', function () {
    document.cookie = 'ids_admin_ui=; Max-Age=0; path=/';
    document.body.classList.remove('admin-view');
    Array.prototype.forEach.call(document.querySelectorAll('.admin-chip'), function (c) { c.remove(); });
    bar.remove();
  });

  function apply(show) {
    document.body.classList.toggle('admin-view', show);
    if (show) decorate();
  }

  // Build a per-card admin chip from the data-* the server already emits — no extra
  // request. Idempotent: skips a card that already has one.
  function decorate() {
    Array.prototype.forEach.call(document.querySelectorAll('.card[data-id]'), function (card) {
      if (card.querySelector('.admin-chip')) return;
      var id = card.getAttribute('data-id');
      var net = card.getAttribute('data-network') || '';
      var adv = card.getAttribute('data-advertiser') || '';
      var feat = card.getAttribute('data-featured') === '1';
      var inStock = card.getAttribute('data-instock') !== '0';
      var chip = document.createElement('div');
      chip.className = 'admin-chip';
      chip.innerHTML =
        '<div class="ac-src">' + esc(net) + (adv ? ' · ' + esc(adv) : '') + ' <span class="ac-id">#' + esc(id) + '</span></div>'
        + '<div class="ac-flags">' + (inStock ? '' : '<span class="ac-out">out of stock</span> ')
        + '<button type="button" class="ac-feat' + (feat ? ' is-on' : '') + '" data-id="' + esc(id) + '">' + (feat ? '★ Featured' : '☆ Feature') + '</button></div>';
      card.appendChild(chip);
      var btn = chip.querySelector('.ac-feat');
      btn.addEventListener('click', function () {
        btn.disabled = true;
        // Browser auto-attaches the cached /admin Basic-auth creds; same-origin POST
        // satisfies the admin CSRF guard.
        fetch('/admin/products/' + id + '/featured', {
          method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: ''
        }).then(function (r) {
          if (!r.ok) throw new Error(r.status);
          var nowOn = !btn.classList.contains('is-on');
          btn.classList.toggle('is-on', nowOn);
          btn.textContent = nowOn ? '★ Featured' : '☆ Feature';
          card.setAttribute('data-featured', nowOn ? '1' : '0');
        }).catch(function () {
          alert('Feature toggle failed — make sure you are signed in to /admin first.');
        }).finally(function () { btn.disabled = false; });
      });
    });
  }
})();