← back to Rentv Adintel

public/js/app.js

265 lines

/* app.js — Global: CA/AZ/All + Verified-Only switches, Show-Me-Why drawer,
   Download Everything button. Loaded on every page.
   No framework, no build step, plain ES5-compatible vanilla JS. */

'use strict';

(function () {

  /* ── Market + Verified switches (persist to localStorage) ─────────────────
     The switches on the header update a query-param-based reload so the
     server-rendered pages stay in sync. */

  var MARKET_KEY   = 'rentv:market';    // 'CA' | 'AZ' | 'ALL'
  var VERIFIED_KEY = 'rentv:verified';  // '1' | '0'

  function getMarket()   { return localStorage.getItem(MARKET_KEY)   || 'ALL'; }
  function getVerified() { return localStorage.getItem(VERIFIED_KEY) || '0'; }

  function setMarket(v) {
    localStorage.setItem(MARKET_KEY, v);
    reloadWithParams();
  }
  function setVerified(v) {
    localStorage.setItem(VERIFIED_KEY, v);
    reloadWithParams();
  }

  function reloadWithParams() {
    var url = new URL(location.href);
    var m = getMarket(), v = getVerified();
    if (m && m !== 'ALL') url.searchParams.set('market', m);
    else url.searchParams.delete('market');
    if (v === '1') url.searchParams.set('verifiedOnly', '1');
    else url.searchParams.delete('verifiedOnly');
    location.href = url.toString();
  }

  // Sync switch UI state on load
  function syncSwitches() {
    var m = getMarket(), v = getVerified();

    // Market buttons
    document.querySelectorAll('.market-switch button[data-market]').forEach(function (btn) {
      btn.classList.toggle('active', btn.dataset.market === m);
    });

    // Verified checkbox/label
    var verEl = document.querySelector('.verified-switch');
    var verCb = document.querySelector('.verified-switch input[type="checkbox"]');
    if (verEl) verEl.classList.toggle('active', v === '1');
    if (verCb) verCb.checked = (v === '1');

    // Apply URL params from persisted prefs on first load (only if URL is default)
    var url = new URL(location.href);
    var urlM = url.searchParams.get('market');
    var urlV = url.searchParams.get('verifiedOnly');
    // If the URL already has params, trust them (user navigated with link)
    // If not, apply prefs (fresh page load from navigation)
    var needsRedirect = false;
    if (m !== 'ALL' && !urlM) needsRedirect = true;
    if (v === '1' && urlV !== '1') needsRedirect = true;
    if (needsRedirect && !window._skipRedirect) {
      window._skipRedirect = true;
      reloadWithParams();
    }
  }

  document.addEventListener('DOMContentLoaded', function () {
    syncSwitches();

    // Market switch buttons
    document.querySelectorAll('.market-switch button[data-market]').forEach(function (btn) {
      btn.addEventListener('click', function () { setMarket(btn.dataset.market); });
    });

    // Verified-only toggle
    var verSwitch = document.querySelector('.verified-switch');
    if (verSwitch) {
      verSwitch.addEventListener('click', function (e) {
        if (e.target.tagName === 'INPUT') return; // let checkbox handle itself
        setVerified(getVerified() === '1' ? '0' : '1');
      });
    }
    var verCb = document.querySelector('.verified-switch input[type="checkbox"]');
    if (verCb) {
      verCb.addEventListener('change', function () {
        setVerified(verCb.checked ? '1' : '0');
      });
    }

    // Global header search → go to /search?q=…
    var globalSearch = document.getElementById('global-search');
    if (globalSearch) {
      globalSearch.addEventListener('keydown', function (e) {
        if (e.key === 'Enter' && globalSearch.value.trim()) {
          location.href = '/advertisers?q=' + encodeURIComponent(globalSearch.value.trim());
        }
      });
    }

    // ── Download Everything button ─────────────────────────────────────────
    var dlBtn = document.getElementById('download-everything-btn');
    if (dlBtn) {
      dlBtn.addEventListener('click', function (e) {
        e.preventDefault();
        startExport();
      });
    }

    // ── Show-Me-Why drawer ─────────────────────────────────────────────────
    // Opened by any element with data-smw-org="<org-id>"
    document.addEventListener('click', function (e) {
      var trigger = e.target.closest('[data-smw-org]');
      if (!trigger) return;
      e.preventDefault();
      openScoreDrawer(trigger.dataset.smwOrg, trigger.dataset.smwName || '');
    });

    // Close drawer on overlay click or close button
    var overlay = document.getElementById('score-drawer-overlay');
    if (overlay) {
      overlay.addEventListener('click', function (e) {
        if (e.target === overlay) closeScoreDrawer();
      });
    }
    document.addEventListener('keydown', function (e) {
      if (e.key === 'Escape') closeScoreDrawer();
    });
  });

  /* ── Download Everything ─────────────────────────────────────────────────── */
  function startExport() {
    var btn = document.getElementById('download-everything-btn');
    if (btn) { btn.textContent = 'Preparing…'; btn.disabled = true; }

    fetch('/api/v1/exports', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind: 'DOWNLOAD_EVERYTHING' }) })
      .then(function (r) { return r.json(); })
      .then(function (d) {
        if (d.error) throw new Error(d.message || d.error);
        var exportId = d.id;
        if (!exportId) {
          // stub response — show msg
          showExportMsg('Export queued. Refresh in a moment to download.');
          return;
        }
        pollExport(exportId, 0);
      })
      .catch(function (err) {
        showExportMsg('Export error: ' + (err.message || err));
        if (btn) { btn.textContent = 'Download Everything'; btn.disabled = false; }
      });
  }

  function pollExport(id, attempt) {
    if (attempt > 30) {
      showExportMsg('Export is taking a while — check /exports for status.');
      resetDlBtn();
      return;
    }
    fetch('/api/v1/exports/' + encodeURIComponent(id))
      .then(function (r) { return r.json(); })
      .then(function (d) {
        if (d.status === 'DONE' && d.download_url) {
          window.location = d.download_url;
          resetDlBtn();
        } else if (d.status === 'ERROR') {
          showExportMsg('Export failed: ' + (d.error || 'unknown error'));
          resetDlBtn();
        } else {
          var pct = d.progress_pct != null ? ' (' + d.progress_pct + '%)' : '';
          var btn = document.getElementById('download-everything-btn');
          if (btn) btn.textContent = 'Building export…' + pct;
          setTimeout(function () { pollExport(id, attempt + 1); }, 2000);
        }
      })
      .catch(function () {
        setTimeout(function () { pollExport(id, attempt + 1); }, 3000);
      });
  }

  function resetDlBtn() {
    var btn = document.getElementById('download-everything-btn');
    if (btn) { btn.textContent = 'Download Everything'; btn.disabled = false; }
  }

  function showExportMsg(msg) {
    var el = document.getElementById('export-msg');
    if (el) { el.textContent = msg; el.style.display = 'block'; }
    else alert(msg);
  }

  /* ── Show-Me-Why score drawer ─────────────────────────────────────────────── */
  function openScoreDrawer(orgId, orgName) {
    var overlay = document.getElementById('score-drawer-overlay');
    var body    = document.getElementById('score-drawer-body');
    if (!overlay || !body) return;

    body.innerHTML = '<p>Loading score for ' + tblEsc(orgName || orgId) + '…</p>';
    overlay.classList.add('open');
    document.body.style.overflow = 'hidden';

    fetch('/api/v1/advertisers/' + encodeURIComponent(orgId))
      .then(function (r) { return r.json(); })
      .then(function (d) {
        if (d.error) { body.innerHTML = '<p class="miss">' + tblEsc(d.message || d.error) + '</p>'; return; }
        renderScoreDrawer(body, d);
      })
      .catch(function (err) {
        body.innerHTML = '<p class="miss">Failed to load: ' + tblEsc(err.message) + '</p>';
      });
  }

  function closeScoreDrawer() {
    var overlay = document.getElementById('score-drawer-overlay');
    if (overlay) overlay.classList.remove('open');
    document.body.style.overflow = '';
  }
  window.closeScoreDrawer = closeScoreDrawer;

  var FACTOR_LABELS = {
    verifiedAdvertising: 'Verified advertising (22%)',
    verifiedConferenceSpendSignal: 'Conference spend signal (15%)',
    recency: 'Recency (12%)',
    repeatActivity: 'Repeat activity (10%)',
    californiaFit: 'California fit (10%)',
    arizonaFit: 'Arizona fit (5%)',
    categoryFit: 'Category fit (8%)',
    rentvAudienceFit: 'RENTV audience fit (8%)',
    contactCompleteness: 'Contact completeness (4%)',
    evidenceQuality: 'Evidence quality (6%)',
  };

  function renderScoreDrawer(el, org) {
    var score = (org.score || {});
    var factors = score.factors || [];
    var totalScore = score.score != null ? score.score : '—';
    var html = '<h2>Score: ' + totalScore + ' / 100</h2>';
    html += '<p style="color:var(--ink-muted);font-size:var(--font-size-sm)">RENTV sales opportunity score for <strong>' + tblEsc(org.display_name || '') + '</strong>. This ranks sales opportunity, not ad spend.</p>';

    if (!factors.length) {
      html += '<p class="miss">No score factors available. Run the scoring job to compute.</p>';
    } else {
      html += factors.map(function (f) {
        var label = FACTOR_LABELS[f.factor] || f.factor;
        var pct   = Math.round((f.value / 100) * 100);
        return '<div class="factor-row">' +
          '<span class="factor-name">' + tblEsc(label) + '</span>' +
          '<div class="factor-bar-wrap"><div class="factor-bar" style="width:' + pct + '%"></div></div>' +
          '<span class="factor-contrib">' + f.value + '</span>' +
          '</div>';
      }).join('');
    }

    // Evidence link
    html += '<p style="margin-top:20px"><a href="/advertisers/' + tblEsc(String(org.id || '')) + '#evidence" class="btn btn-outline">View full evidence</a></p>';
    el.innerHTML = html;
  }

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

}());