← back to Ventura Corridor
public/pitches-map.html
171 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Pitches map · DW · Ventura Corridor</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<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>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
<style>
:root {
--noir: #0a0a0c; --noir-rise: #131316; --ink: #f0ece2;
--ink-mute: #888475; --metal: #b89968; --metal-glow: #d4b683;
--rule: #2a2622;
--serif: 'Cormorant Garamond', 'Times New Roman', serif;
--sans: 'Inter', -apple-system, system-ui, sans-serif;
--mono: 'JetBrains Mono', ui-monospace, monospace;
}
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;1,300;1,400&family=Inter:wght@200;300;400;600&display=swap');
html, body { margin: 0; height: 100%; background: var(--noir); color: var(--ink); font-family: var(--sans); font-weight: 300; overflow: hidden; }
* { box-sizing: border-box; }
header { position: absolute; top: 0; left: 0; right: 0; z-index: 1000;
background: linear-gradient(180deg, rgba(10,10,12,.97) 70%, rgba(10,10,12,0));
padding: 18px 32px 14px; border-bottom: 1px solid var(--rule);
display: flex; justify-content: space-between; align-items: baseline; gap: 24px; flex-wrap: wrap; pointer-events: none; }
header > * { pointer-events: auto; }
header h1 { font-family: var(--serif); font-weight: 400; font-size: 22px; margin: 0; }
header h1 em { font-style: italic; color: var(--metal); }
.sub { font-size: 10px; letter-spacing: .26em; text-transform: uppercase; color: var(--ink-mute); margin-top: 2px; }
nav.tabs { display: flex; gap: 4px; flex-wrap: wrap; }
nav.tabs a { font-size: 9px; letter-spacing: .26em; text-transform: uppercase;
color: var(--ink-mute); text-decoration: none; padding: 5px 10px; border: 1px solid var(--rule); background: rgba(10,10,12,0.85); }
nav.tabs a.active, nav.tabs a:hover { color: var(--metal); border-color: var(--metal); }
#map { position: absolute; inset: 0; }
/* Invert OSM tiles for noir */
.leaflet-tile-pane { filter: invert(1) hue-rotate(180deg) saturate(0.6) brightness(0.92) contrast(0.92); }
.leaflet-popup-content-wrapper { background: var(--noir-rise); color: var(--ink); border-radius: 0; border: 1px solid var(--rule); }
.leaflet-popup-tip { background: var(--noir-rise); border: 1px solid var(--rule); }
.leaflet-popup-content { margin: 12px 16px; font-family: var(--sans); font-size: 12px; line-height: 1.55; }
.leaflet-popup-content b { font-family: var(--serif); font-weight: 400; font-size: 16px; color: var(--metal-glow); display: block; margin-bottom: 4px; }
.leaflet-popup-content small { color: var(--ink-mute); display: block; font-family: var(--mono); font-size: 9px; letter-spacing: .1em; margin-top: 4px; }
.leaflet-popup-content a { color: var(--metal); text-decoration: none; font-size: 10px; letter-spacing: .15em; text-transform: uppercase; padding: 3px 8px; border: 1px solid var(--metal); display: inline-block; margin-top: 8px; margin-right: 4px; }
.leaflet-popup-content a:hover { background: rgba(184,153,104,0.06); }
/* Custom markers */
.pin-dw {
width: 24px; height: 24px; background: var(--metal-glow); border: 2px solid var(--noir);
border-radius: 50%; box-shadow: 0 0 0 3px var(--metal-glow), 0 0 12px rgba(212,182,131,0.6);
}
.pin-pitch {
width: 12px; height: 12px; border-radius: 50%; border: 1.5px solid var(--noir);
box-shadow: 0 1px 2px rgba(0,0,0,0.6);
}
.pin-pitch.s-draft { background: #6e6a5e; }
.pin-pitch.s-researched { background: var(--metal); }
.pin-pitch.s-scrubbed { background: #c4a05c; }
.pin-pitch.s-approved { background: #6a9b73; }
.pin-pitch.s-sent { background: var(--metal-glow); border-color: var(--metal-glow); box-shadow: 0 0 8px rgba(212,182,131,0.6); }
.pin-pitch.s-replied { background: #6a9b73; box-shadow: 0 0 8px rgba(106,155,115,0.6); }
.pin-pitch.s-won { background: #d4b683; box-shadow: 0 0 12px rgba(212,182,131,0.9); width: 16px; height: 16px; }
.pin-pitch.s-lost { background: #b66565; opacity: 0.4; }
.pin-pitch.s-skip { background: #444; opacity: 0.3; }
/* Side legend + counts */
.legend {
position: absolute; bottom: 20px; left: 20px; z-index: 1000;
background: rgba(10,10,12,0.92); border: 1px solid var(--rule);
padding: 12px 16px; min-width: 200px;
}
.legend h3 { font-family: var(--serif); font-style: italic; font-weight: 400; font-size: 14px; margin: 0 0 8px; color: var(--metal); }
.legend .row { display: flex; align-items: center; gap: 8px; padding: 3px 0; font-size: 10px; letter-spacing: .12em; color: var(--ink-mute); text-transform: uppercase; }
.legend .row .dot { width: 10px; height: 10px; border-radius: 50%; border: 1px solid var(--noir); flex-shrink: 0; }
.legend .row .ct { color: var(--metal); font-family: var(--serif); font-style: italic; font-size: 13px; margin-left: auto; }
</style>
</head>
<body>
<header>
<div>
<h1>Pitches <em>map</em> · walking distance</h1>
<div class="sub">DW + 200 corridor neighbors · click any pin</div>
</div>
<nav class="tabs">
<a href="/">map</a>
<a href="/today.html">today</a>
<a href="/pitches.html">pitches</a>
<a href="/linkedin.html">linkedin</a>
<a href="/pitches-map.html" class="active">map</a>
<span class="theme-toggle-host" style="display:inline-flex;align-items:center;margin-left:6px"></span>
</nav>
</header>
<div id="map"></div>
<div class="legend">
<h3>Status</h3>
<div id="legend-rows"></div>
</div>
<script>
function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
const STATUS_ORDER = ['won','sent','replied','approved','scrubbed','researched','draft','skip','lost'];
const STATUS_LABEL = { draft:'draft', researched:'researched', scrubbed:'scrubbed', approved:'approved', sent:'sent', replied:'replied', won:'won', lost:'lost', skip:'skip' };
(async () => {
const data = await fetch('/api/pitches').then(r => r.json());
// Filter to walking-distance + has lat/lng
// Need to JOIN with businesses for lat/lng — let's also fetch businesses
const bizData = await fetch('/api/businesses?limit=20000').then(r => r.json());
const bizById = {};
for (const b of (bizData.rows || [])) bizById[String(b.id)] = b;
const walking = (data.rows || []).filter(p =>
['same_building','same_block','walk_2min','walk_5min','walk_10min','walk_15min'].includes(p.dw_proximity)
);
// Attach lat/lng from businesses
const targets = walking.map(p => {
const b = bizById[String(p.business_id)];
return b && b.lat && b.lng ? { ...p, lat: b.lat, lng: b.lng, biz_address: b.address } : null;
}).filter(Boolean);
// Map setup centered on DW (15442 Ventura)
const map = L.map('map', { zoomControl: true }).setView([34.1542, -118.4703], 15);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19, attribution: '© OpenStreetMap'
}).addTo(map);
// DW marker
L.marker([34.1542, -118.4703], {
icon: L.divIcon({ html: '<div class="pin-dw" title="Designer Wallcoverings"></div>', className: '', iconSize: [24, 24], iconAnchor: [12, 12] })
}).bindPopup('<b>Designer Wallcoverings</b>15442 Ventura Blvd #201<small>YOU ARE HERE</small>').addTo(map);
// Status counts
const counts = {};
for (const t of targets) counts[t.status] = (counts[t.status] || 0) + 1;
document.getElementById('legend-rows').innerHTML = STATUS_ORDER
.filter(s => counts[s])
.map(s => `
<div class="row">
<div class="dot pin-pitch s-${s}" style="position:relative"></div>
<span>${STATUS_LABEL[s]}</span>
<span class="ct">${counts[s]}</span>
</div>`).join('');
// Pitch markers
for (const t of targets) {
const marker = L.marker([t.lat, t.lng], {
icon: L.divIcon({
html: `<div class="pin-pitch s-${t.status}"></div>`,
className: '', iconSize: [12, 12], iconAnchor: [6, 6]
})
}).addTo(map);
const proxLabel = { same_building:'⚡ same building', same_block:'★ same block', walk_2min:'2 min walk', walk_5min:'5 min walk', walk_10min:'10 min walk', walk_15min:'15 min walk' }[t.dw_proximity] || '';
const popup = `
<b>${escapeHtml(t.name)}</b>
${escapeHtml(t.biz_address || '')}<br>
<small>${escapeHtml(t.pitch_type || '')} · ${proxLabel} · ${t.status}</small>
<small>${escapeHtml(t.naics || '')}</small>
<a href="${t.research_links?.gmaps || '#'}" target="_blank">Maps ↗</a>
<a href="/pitches.html?id=${t.id}">Detail ↗</a>`;
marker.bindPopup(popup);
}
})();
</script>
</body>
</html>