← back to Rentv
contrib/contractors/widget.html
139 lines
<!-- =========================================================================
RENTV contrib — "Licensed contractors in {market}" widget (TK-10488)
-------------------------------------------------------------------------
A SELF-CONTAINED, drop-in widget for a CRE deal news story. Paste this
whole block into a story template (or include the <script> from a partial).
It calls the contrib router's /api/contractors-for-market endpoint and
renders a TEXT-ONLY list (name + trade + phone). No external links, no
re-hosted assets, honoring the RENTV no-rentv.com-link-out rule.
USAGE (two ways):
1) Auto-init from data attributes — drop the markup on the page:
<div class="rentv-contractors"
data-county="Los Angeles" data-city="Los Angeles"
data-limit="6"></div>
...then include this file once (or its <script>) anywhere after it.
2) Programmatic — call the global:
RentvContractors.render(document.getElementById('box'),
{ county: 'Los Angeles', city: 'Los Angeles', limit: 6 });
The endpoint base defaults to same-origin '/contrib/contractors'; override
with data-endpoint="..." or the { endpoint } option.
========================================================================= -->
<style>
.rentv-contractors { font: 14px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: #1a1a1a; }
.rentv-contractors__title { font-size: 15px; font-weight: 700; margin: 0 0 .5em; letter-spacing: .01em; }
.rentv-contractors__list { list-style: none; margin: 0; padding: 0; }
.rentv-contractors__item { padding: .55em 0; border-top: 1px solid #e6e6e6; }
.rentv-contractors__item:first-child { border-top: none; }
.rentv-contractors__name { font-weight: 600; }
.rentv-contractors__trade { color: #555; }
.rentv-contractors__phone { color: #333; }
.rentv-contractors__meta { color: #888; font-size: 12px; }
.rentv-contractors__asof { color: #999; font-size: 11px; margin-top: .2em; font-style: italic; }
.rentv-contractors__attr { color: #999; font-size: 11px; margin-top: .6em; }
.rentv-contractors[hidden] { display: none; }
</style>
<script>
(function () {
'use strict';
if (window.RentvContractors) return; // idempotent — safe to include more than once
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
});
}
function marketLabel(m) {
if (!m) return 'this market';
return m.city || m.county || 'this market';
}
function renderList(el, data) {
var contractors = (data && data.contractors) || [];
if (!contractors.length) { el.hidden = true; return; } // nothing to show -> hide, story stays clean
var label = marketLabel(data.market);
// DATE EVERYTHING — every entry carries the CSLB freshness date + a text-only
// "verify at cslb.ca.gov" (no link-out, honoring the RENTV no-link-out rule).
var asOf = (data && data.source_as_of) ? String(data.source_as_of) : 'unknown';
var verify = (data && data.verify_url) ? String(data.verify_url) : 'cslb.ca.gov';
var asOfLine = 'CSLB data as of ' + esc(asOf) + ' — verify at ' + esc(verify);
var rows = contractors.map(function (c) {
var bits = [];
bits.push('<span class="rentv-contractors__name">' + esc(c.name) + '</span>');
if (c.trade) bits.push('<span class="rentv-contractors__trade"> · ' + esc(c.trade) + '</span>');
var phone = c.phone
? '<div class="rentv-contractors__phone">' + esc(c.phone) + '</div>'
: '';
var meta = [];
if (c.city) meta.push(esc(c.city));
if (c.license_no) meta.push('Lic. ' + esc(c.license_no));
if (c.license_status) meta.push(esc(c.license_status));
var metaLine = meta.length
? '<div class="rentv-contractors__meta">' + meta.join(' · ') + '</div>'
: '';
// per-entry dated attribution — required by TK-10488
var asOfPerEntry = '<div class="rentv-contractors__asof">' + asOfLine + '</div>';
return '<li class="rentv-contractors__item">' + bits.join('') + phone + metaLine + asOfPerEntry + '</li>';
}).join('');
el.innerHTML =
'<div class="rentv-contractors__title">Licensed contractors in ' + esc(label) + '</div>' +
'<ul class="rentv-contractors__list">' + rows + '</ul>' +
'<div class="rentv-contractors__attr">' +
esc((data && data.attribution) || 'CA CSLB licensed contractors') +
'</div>';
el.hidden = false;
}
function render(el, opts) {
if (!el) return;
opts = opts || {};
var endpoint = (opts.endpoint || el.getAttribute('data-endpoint') || '/contrib/contractors')
.replace(/\/+$/, '');
var county = opts.county || el.getAttribute('data-county') || '';
var city = opts.city || el.getAttribute('data-city') || '';
var limit = opts.limit || el.getAttribute('data-limit') || '';
var mode = opts.mode || el.getAttribute('data-mode') || 'deal';
if (!county && !city) { el.hidden = true; return; }
var qs = new URLSearchParams({ mode: mode });
if (county) qs.set('county', county);
if (city) qs.set('city', city);
if (limit) qs.set('limit', limit);
fetch(endpoint + '/api/contractors-for-market?' + qs.toString(), {
headers: { accept: 'application/json' },
credentials: 'same-origin'
})
.then(function (r) { return r.ok ? r.json() : { contractors: [] }; })
.then(function (data) { renderList(el, data); })
.catch(function () { el.hidden = true; }); // fail soft — never break the story
}
function autoInit(root) {
var scope = root || document;
var nodes = scope.querySelectorAll('.rentv-contractors:not([data-rendered])');
Array.prototype.forEach.call(nodes, function (el) {
el.setAttribute('data-rendered', '1');
render(el);
});
}
window.RentvContractors = { render: render, autoInit: autoInit };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { autoInit(); });
} else {
autoInit();
}
})();
</script>