← back to Ventura Corridor
public/dw-radar.html
305 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>DW radar · Ventura Blvd · interior + furniture + design</title>
<script>(function(){try{var t=localStorage.getItem('vc-theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(e){document.documentElement.dataset.theme='dark';}})();</script>
<link rel="stylesheet" href="/theme.css">
<script src="/theme.js" defer></script>
<style>
:root {
--noir: #0a0a0c; --noir-rise: #131316; --noir-deep: #07070a;
--rule: #2a2724; --rule-hot: #3a342c;
--ink: #f4f1ea; --ink-soft: #d8d2c5; --ink-mute: #8b857a;
--metal: #b89968; --metal-glow: #d4b683;
--hot: #d96a3a; --you: #4f7eff;
--serif: "Cormorant Garamond", Georgia, serif;
--sans: -apple-system, "SF Pro Text", system-ui, sans-serif;
--mono: "SFMono-Regular", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
html, body { margin: 0; background: var(--noir); color: var(--ink); font-family: var(--sans); font-weight: 300; }
header {
padding: 32px 40px 22px; border-bottom: 1px solid var(--rule);
display: grid; grid-template-columns: 1fr auto; gap: 20px; align-items: end;
position: sticky; top: 0; z-index: 50;
background: linear-gradient(180deg, rgba(10,10,12,.97) 70%, rgba(10,10,12,0));
backdrop-filter: blur(10px);
}
header h1 { font-family: var(--serif); font-weight: 300; font-size: clamp(30px, 3.6vw, 48px); margin: 0; letter-spacing: -0.012em; line-height: 1; }
header h1 em { font-style: italic; color: var(--metal); }
header .sub { font-size: 11px; letter-spacing: .22em; text-transform: uppercase; color: var(--ink-mute); margin-top: 8px; }
header .you-pin { color: var(--you); font-style: italic; font-family: var(--serif); font-size: 14px; letter-spacing: 0; text-transform: none; }
nav.tabs { display: flex; gap: 0; align-self: end; }
nav.tabs a { padding: 10px 18px; border: 1px solid var(--rule); color: var(--ink-mute); text-decoration: none; font-size: 10px; letter-spacing: .24em; text-transform: uppercase; border-right: 0; }
nav.tabs a:last-child { border-right: 1px solid var(--rule); }
nav.tabs a.active { color: var(--metal); border-color: var(--metal); background: rgba(184,153,104,0.05); }
nav.tabs a:hover { color: var(--metal-glow); }
/* You-are-here strip */
.you-strip {
padding: 16px 40px; border-bottom: 1px solid var(--rule);
background: var(--noir-deep);
display: flex; gap: 32px; align-items: center; flex-wrap: wrap;
}
.you-strip .label { font-size: 10px; letter-spacing: .24em; text-transform: uppercase; color: var(--you); font-weight: 500; }
.you-strip .name { font-family: var(--serif); font-style: italic; color: var(--metal); font-size: 22px; font-weight: 400; }
.you-strip .addr { font-family: var(--mono); font-size: 11px; color: var(--ink-mute); }
/* Filters */
.rail {
padding: 22px 40px; border-bottom: 1px solid var(--rule);
display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 32px; align-items: start;
}
.ctrl { display: flex; flex-direction: column; gap: 10px; }
.ctrl-label { font-size: 10px; letter-spacing: .26em; text-transform: uppercase; color: var(--ink-mute); display: flex; justify-content: space-between; }
.ctrl-label b { color: var(--metal); font-family: var(--serif); font-style: italic; font-weight: 400; font-size: 14px; letter-spacing: 0; font-variant-numeric: tabular-nums; }
input[type="range"] { -webkit-appearance: none; width: 100%; background: transparent; cursor: pointer; margin: 0; }
input[type="range"]::-webkit-slider-runnable-track { height: 2px; background: var(--rule-hot); }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 0; background: var(--metal); margin-top: -7px; border: 1px solid var(--ink); }
input[type="range"]::-webkit-slider-thumb:hover { background: var(--metal-glow); }
input[type="search"] { padding: 10px 14px; background: var(--noir-rise); border: 1px solid var(--rule); color: var(--ink); font-family: var(--sans); font-size: 13px; outline: none; width: 100%; }
input[type="search"]::placeholder { color: var(--ink-mute); font-style: italic; font-family: var(--serif); }
input[type="search"]:focus { border-color: var(--metal); }
select { padding: 10px 14px; background: var(--noir-rise); border: 1px solid var(--rule); color: var(--ink-soft); font-family: var(--sans); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; outline: none; cursor: pointer; }
/* Stats */
.stats { padding: 16px 40px; border-bottom: 1px solid var(--rule); display: flex; gap: 40px; flex-wrap: wrap; font-size: 11px; letter-spacing: .12em; color: var(--ink-mute); text-transform: uppercase; }
.stats b { color: var(--metal); font-family: var(--serif); font-style: italic; font-size: 18px; font-weight: 400; letter-spacing: 0; margin-right: 8px; font-variant-numeric: tabular-nums; }
/* Grid */
main { padding: 28px 40px 80px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1px; background: var(--rule); border: 1px solid var(--rule); }
.card { background: var(--noir-rise); padding: 22px 24px 22px; min-height: 220px; display: flex; flex-direction: column; gap: 10px; position: relative; transition: background 180ms ease; }
.card:hover { background: var(--noir); }
.card.you { background: linear-gradient(135deg, rgba(79,126,255,0.08), var(--noir-rise)); border-left: 3px solid var(--you); padding-left: 21px; }
.card .you-tag { position: absolute; top: 16px; right: 18px; font-size: 9px; letter-spacing: .26em; text-transform: uppercase; color: var(--you); font-weight: 600; }
.card .naics { font-size: 9px; letter-spacing: .22em; text-transform: uppercase; color: var(--ink-mute); }
.card .name { font-family: var(--serif); font-weight: 400; font-size: 22px; line-height: 1.1; color: var(--ink); margin: 0; letter-spacing: -0.01em; }
.card.you .name em { color: var(--you); font-style: italic; }
.card .addr { font-size: 11px; color: var(--ink-mute); line-height: 1.4; font-family: var(--mono); }
.card .pixels { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.card .px { font-size: 9px; letter-spacing: .14em; text-transform: uppercase; padding: 3px 8px; border: 1px solid var(--rule); color: var(--ink-mute); font-family: var(--mono); }
.card .px.hot { color: var(--hot); border-color: var(--hot); }
.card .px.metal { color: var(--metal); border-color: var(--metal); }
.card .px.warn { color: var(--ink-mute); border-color: rgba(216,210,197,0.18); border-style: dashed; }
.card .socials { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.card .soc { font-size: 9px; letter-spacing: .14em; text-transform: uppercase; padding: 3px 8px; border: 1px solid rgba(216,210,197,0.18); color: var(--ink-soft); text-decoration: none; font-family: var(--mono); font-weight: 500; }
.card .soc:hover { color: var(--metal-glow); border-color: var(--metal-glow); }
.card .stat-row { display: flex; gap: 14px; font-size: 11px; color: var(--ink-mute); margin-top: 4px; }
.card .stat-row span b { color: var(--metal); font-family: var(--serif); font-style: italic; font-weight: 400; font-size: 14px; margin-right: 4px; font-variant-numeric: tabular-nums; }
.card .footer-row { margin-top: auto; padding-top: 12px; border-top: 1px solid var(--rule); display: flex; justify-content: space-between; align-items: baseline; font-size: 11px; color: var(--ink-mute); }
.card .score { color: var(--metal); font-family: var(--serif); font-style: italic; font-size: 16px; font-weight: 400; }
.card .score em { font-style: normal; font-size: 9px; letter-spacing: .2em; margin-left: 6px; color: var(--ink-mute); text-transform: uppercase; }
.card a.dom { color: var(--metal); font-family: var(--mono); font-size: 10px; text-decoration: none; word-break: break-all; }
.card a.dom:hover { color: var(--metal-glow); }
.empty { padding: 80px 32px; text-align: center; font-family: var(--serif); font-style: italic; color: var(--ink-mute); font-size: 18px; }
footer { padding: 24px 40px; border-top: 1px solid var(--rule); font-family: var(--serif); font-style: italic; color: var(--ink-mute); font-size: 12px; text-align: center; }
</style>
</head>
<body>
<header>
<div>
<h1>DW <em>radar</em> · the corridor</h1>
<div class="sub">interior · furniture · design · wallcover · paint · floor · lighting · upholstery</div>
</div>
<nav class="tabs">
<a href="/">2D map</a>
<a href="/3d.html">3D</a>
<a href="/wall.html">Wall</a>
<a href="/signals.html">Signals</a>
<a href="/dw-radar.html" class="active" aria-current="page">DW radar</a>
<a href="/api/export.csv?vertical=furniture" download style="border-color:var(--metal);color:var(--metal)">Furniture CSV ↓</a>
<a href="/api/export.csv?vertical=interior" download style="border-color:var(--metal);color:var(--metal)">Interior CSV ↓</a>
<span class="theme-toggle-host" style="display:inline-flex;align-items:center;margin-left:6px"></span>
</nav>
</header>
<div class="you-strip">
<span class="label">you are here</span>
<span class="name">Designer Wallcoverings</span>
<span class="addr">15442 Ventura Blvd #201 · Sherman Oaks · 91403</span>
</div>
<div class="stats" id="stats">
<span><b id="s-total">—</b> on radar</span>
<span><b id="s-paid">—</b> paying for ads</span>
<span><b id="s-google">—</b> Google Ads</span>
<span><b id="s-meta">—</b> Meta Pixel</span>
<span><b id="s-contacts">—</b> with contacts</span>
</div>
<section class="rail">
<div class="ctrl">
<div class="ctrl-label">Search <b id="lbl-q">all</b></div>
<input type="search" id="q" placeholder="name, address, NAICS…" autocomplete="off">
</div>
<div class="ctrl">
<div class="ctrl-label">Min paid platforms <b id="lbl-paid">0</b></div>
<input type="range" id="r-paid" min="0" max="8" value="0" step="1">
</div>
<div class="ctrl">
<div class="ctrl-label">Min contacts <b id="lbl-contacts">0</b></div>
<input type="range" id="r-contacts" min="0" max="10" value="0" step="1">
</div>
<div class="ctrl">
<div class="ctrl-label">NAICS</div>
<select id="f-naics">
<option value="">All categories</option>
<option value="furniture">Furniture stores</option>
<option value="interior">Interior design</option>
<option value="home furnish">Home furnishings</option>
<option value="design service">Specialized design</option>
<option value="paint">Paint</option>
<option value="lighting">Lighting</option>
<option value="floor">Flooring</option>
<option value="upholst">Upholstery</option>
<option value="window treat">Window treatments</option>
<option value="wallcover">Wallcoverings</option>
</select>
</div>
<div class="ctrl">
<div class="ctrl-label">Sort</div>
<select id="sort">
<option value="paid">Most paid platforms first</option>
<option value="contacts">Most contacts first</option>
<option value="seo">Highest SEO score</option>
<option value="distance">Closest to DW</option>
<option value="name">Name A–Z</option>
</select>
</div>
</section>
<main>
<div class="grid" id="grid"></div>
<div class="empty" id="empty" style="display:none">No matches.</div>
</main>
<footer>Directory only. Public signals from the businesses' own front-page HTML and LA City Business Tax Registrants. No ad-spend dollars (Google does not publish that data).</footer>
<script>
let DATA = [];
const DW_LAT = 34.1542, DW_LNG = -118.4703;
function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function hostOnly(url) { try { return new URL(/^https?:/.test(url) ? url : 'https://' + url).hostname.replace(/^www\./, ''); } catch { return ''; } }
function distance(lat1, lng1, lat2, lng2) {
if (lat1 == null || lng1 == null || lat2 == null || lng2 == null) return Infinity;
const R = 3958.8, toR = (d) => d * Math.PI / 180;
const dLat = toR(lat2 - lat1), dLng = toR(lng2 - lng1);
const a = Math.sin(dLat/2)**2 + Math.cos(toR(lat1)) * Math.cos(toR(lat2)) * Math.sin(dLng/2)**2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
const PIXEL_LABELS = {
google_ads:'Google Ads', meta_pixel:'Meta', tiktok_pixel:'TikTok',
pinterest_tag:'Pinterest', linkedin_insight:'LinkedIn', twitter_pixel:'Twitter',
reddit_pixel:'Reddit', bing_uet:'Bing', snap_pixel:'Snap',
gtm_container:'GTM ⚠',
};
const PAID = ['google_ads','meta_pixel','tiktok_pixel','pinterest_tag','twitter_pixel','reddit_pixel','bing_uet','snap_pixel'];
function render() {
const q = (document.getElementById('q').value || '').trim().toLowerCase();
const minPaid = +document.getElementById('r-paid').value;
const minContacts = +document.getElementById('r-contacts').value;
const naicsFilter = document.getElementById('f-naics').value;
const sort = document.getElementById('sort').value;
document.getElementById('lbl-q').textContent = q || 'all';
document.getElementById('lbl-paid').textContent = minPaid;
document.getElementById('lbl-contacts').textContent = minContacts;
let rows = DATA.filter(r => {
const sig = r.ad_signals || {};
if ((sig.paid_ads_count || 0) < minPaid) return false;
if ((r.contact_count || 0) < minContacts) return false;
if (naicsFilter && !(r.naics_desc || '').toLowerCase().includes(naicsFilter)) return false;
if (q) {
const hay = [r.name, r.address, r.city, r.naics_desc].filter(Boolean).join(' ').toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
});
rows.forEach(r => { r._dist = distance(DW_LAT, DW_LNG, r.lat, r.lng); });
rows.sort((a, b) => {
if (+a.id === 17463) return -1; // DW always first
if (+b.id === 17463) return 1;
if (sort === 'paid') return ((b.ad_signals?.paid_ads_count || 0) - (a.ad_signals?.paid_ads_count || 0)) || ((b.contact_count||0) - (a.contact_count||0));
if (sort === 'contacts') return (b.contact_count || 0) - (a.contact_count || 0);
if (sort === 'seo') return (b.seo_score || 0) - (a.seo_score || 0);
if (sort === 'distance') return a._dist - b._dist;
if (sort === 'name') return (a.name || '').localeCompare(b.name || '');
return 0;
});
const grid = document.getElementById('grid');
const empty = document.getElementById('empty');
if (!rows.length) { grid.innerHTML = ''; empty.style.display = 'block'; return; }
empty.style.display = 'none';
grid.innerHTML = rows.slice(0, 600).map(r => {
const isYou = +r.id === 17463;
const sig = r.ad_signals || {};
const px = Object.keys(PIXEL_LABELS).filter(k => sig[k]).map(k => {
const cls = k === 'google_ads' ? 'hot' : (PAID.includes(k) ? 'metal' : (k === 'gtm_container' ? 'warn' : ''));
const title = k === 'gtm_container' ? 'title="GTM detected — additional pixels may load indirectly"' : '';
return `<span class="px ${cls}" ${title}>${PIXEL_LABELS[k]}</span>`;
}).join('');
const dom = r.website ? hostOnly(r.website) : '';
const distMi = isFinite(r._dist) ? r._dist.toFixed(1) + ' mi' : '—';
const contactList = r.contacts || [];
const SOC_LABEL = { instagram: 'IG', facebook: 'FB', twitter: 'X', tiktok: 'TT', bluesky: 'BSKY' };
const seenSoc = new Set();
const socials = contactList
.filter(c => c.role === 'social_handle' && c.notes && SOC_LABEL[c.notes])
.filter(c => { const k = c.notes; if (seenSoc.has(k)) return false; seenSoc.add(k); return true; })
.map(c => `<a class="soc" href="${c.source_url}" target="_blank" rel="noopener">${SOC_LABEL[c.notes]} ${escapeHtml(c.person_name || '')}</a>`)
.join('');
const liUrl = contactList.find(c => c.role === 'linkedin_company')?.person_linkedin;
const liChip = liUrl ? `<a class="soc" href="${liUrl}" target="_blank" rel="noopener">LinkedIn</a>` : '';
return `
<article class="card${isYou ? ' you' : ''}">
${isYou ? '<div class="you-tag">You</div>' : ''}
<div class="naics">${escapeHtml(r.naics_desc || '—')}</div>
<h3 class="name">${isYou ? `<em>${escapeHtml(r.name)}</em>` : escapeHtml(r.name)}</h3>
<div class="addr">${escapeHtml(r.address || '')}${r.city ? ' · ' + escapeHtml(r.city) : ''}</div>
<div class="pixels">${px || '<span class="px">no signals on file</span>'}</div>
${(socials || liChip) ? `<div class="socials">${socials}${liChip}</div>` : ''}
<div class="stat-row">
<span><b>${distMi}</b>from DW</span>
<span><b>${r.contact_count || 0}</b>contacts</span>
</div>
<div class="footer-row">
<span class="score">${r.seo_score || '—'}<em>${r.seo_tier || ''}</em></span>
${dom ? `<a class="dom" href="${r.website}" target="_blank" rel="noopener">${escapeHtml(dom)}</a>` : '<span></span>'}
</div>
</article>`;
}).join('');
}
function updateStats() {
document.getElementById('s-total').textContent = DATA.length;
document.getElementById('s-paid').textContent = DATA.filter(r => (r.ad_signals?.paid_ads_count || 0) > 0).length;
document.getElementById('s-google').textContent = DATA.filter(r => r.ad_signals?.google_ads).length;
document.getElementById('s-meta').textContent = DATA.filter(r => r.ad_signals?.meta_pixel).length;
document.getElementById('s-contacts').textContent = DATA.filter(r => (r.contact_count || 0) > 0).length;
}
async function load() {
const r = await fetch('/api/dw-radar').then(r => r.json());
DATA = r.rows || [];
updateStats();
render();
}
document.getElementById('q').addEventListener('input', () => { clearTimeout(window._t); window._t = setTimeout(render, 120); });
document.getElementById('r-paid').addEventListener('input', render);
document.getElementById('r-contacts').addEventListener('input', render);
document.getElementById('f-naics').addEventListener('change', render);
document.getElementById('sort').addEventListener('change', render);
load();
</script>
</body>
</html>