← 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
}());