← back to Ventura Claw Leads
views/public/map.ejs
185 lines
<%- include('../partials/head', { title }) %>
<%- include('../partials/header') %>
<link rel="stylesheet" href="/static/vendor/leaflet/leaflet.css">
<link rel="stylesheet" href="/static/vendor/markercluster/MarkerCluster.css">
<link rel="stylesheet" href="/static/vendor/markercluster/MarkerCluster.Default.css">
<style>
.map-page { display: grid; grid-template-rows: auto 1fr; min-height: calc(100vh - 64px); }
.map-head {
display: flex; align-items: center; gap: 1rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
background: var(--bg-alt);
flex-wrap: wrap;
}
.map-head h1 { margin: 0; font-family: var(--serif); font-weight: 500; font-size: 1.5rem; }
.map-head .map-stats { color: var(--fg-muted); font-size: 0.85rem; }
.map-head .map-controls { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-left: auto; align-items: center; }
#map { width: 100%; height: 100%; min-height: 540px; background: var(--bg-alt); }
.pin-marker { width: 16px; height: 16px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.4); }
.pin-marker.tier-premier { background: var(--brass); }
.pin-marker.tier-standard { background: #6366f1; }
.pin-marker.tier-starter { background: #0ea5e9; }
.pin-marker.tier-free { background: #94a3b8; }
.pin-popup { font-family: var(--sans); min-width: 220px; }
.pin-popup h3 { margin: 0 0 4px; font-family: var(--serif); font-weight: 500; font-size: 1.05rem; color: #0e0e0e; }
.pin-popup .pin-loc { color: #555; font-size: 0.82rem; margin: 0 0 6px; }
.pin-popup .pin-vert { color: var(--brass); font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; margin: 0 0 6px; }
.pin-popup .pin-actions { display: flex; gap: 0.4rem; margin-top: 0.6rem; }
.pin-popup .pin-actions a { flex: 1; text-align: center; padding: 0.4rem 0.6rem; font-size: 0.78rem; font-weight: 500; border-radius: 4px; text-decoration: none; }
.pin-popup .pin-actions a.primary { background: #0e0e0e; color: #fff; }
.pin-popup .pin-actions a.ghost { background: transparent; color: #0e0e0e; border: 1px solid #ccc; }
.pin-popup .pin-headline { font-size: 0.78rem; color: #444; margin: 0 0 4px; line-height: 1.4; }
.leaflet-tooltip.pin-hover {
background: var(--bg, #fff); color: var(--fg, #0e0e0e);
border: 1px solid var(--border, #d8d2c0); border-radius: 4px;
padding: 6px 10px; font-family: var(--sans, -apple-system, sans-serif);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
font-size: 13px; line-height: 1.35;
}
.leaflet-tooltip.pin-hover::before { display: none; }
</style>
<section class="map-page">
<div class="map-head">
<div>
<h1>Map of Ventura Blvd businesses</h1>
<p class="map-stats"><%= stats.pinned %> of <%= stats.total %> pinned · click a pin for the profile</p>
</div>
<div class="map-controls">
<input type="text" id="map-search" placeholder="Search by name, neighborhood, or category" autocomplete="off">
<select id="map-vertical">
<option value="">All categories</option>
<% verticals.forEach(function(v){ %>
<option value="<%= v.key %>"><%= v.icon %> <%= v.label %></option>
<% }); %>
</select>
<a id="map-list-link" href="/find" class="btn btn-ghost btn-sm" style="text-decoration:none;align-self:center">View as list</a>
</div>
</div>
<div id="map" role="application" aria-label="Map of Ventura Blvd businesses"></div>
</section>
<%- include('../partials/footer') %>
<script src="/static/vendor/leaflet/leaflet.js"></script>
<script src="/static/vendor/markercluster/leaflet.markercluster.js"></script>
<script>
(function(){
var VENTURA_CENTER = [34.1597, -118.5012];
var map = L.map('map').setView(VENTURA_CENTER, 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
var cluster = L.markerClusterGroup({
showCoverageOnHover: false,
spiderfyOnMaxZoom: true,
chunkedLoading: true,
maxClusterRadius: 50
});
map.addLayer(cluster);
var allMarkers = [];
function pinClass(b){ return 'tier-' + (b.tier || 'free'); }
function pinIcon(b){
return L.divIcon({
className: '',
html: '<div class="pin-marker '+pinClass(b)+'"></div>',
iconSize: [16,16], iconAnchor: [8,8]
});
}
function escHtml(s){ return String(s||'').replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); }
function popupHtml(b){
var loc = [b.neighborhood, b.city, b.state].filter(Boolean).join(', ');
var verticalLabel = (b.vertical || '').replace(/_/g, ' ');
return '<div class="pin-popup">' +
'<p class="pin-vert">'+escHtml(verticalLabel)+'</p>' +
'<h3>'+escHtml(b.business_name)+'</h3>' +
'<p class="pin-loc">'+escHtml(loc)+'</p>' +
(b.headline ? '<p class="pin-headline">'+escHtml(b.headline)+'</p>' : '') +
'<div class="pin-actions">' +
'<a class="primary" href="/business/'+encodeURIComponent(b.slug)+'">View profile</a>' +
'<a class="ghost" href="/business/'+encodeURIComponent(b.slug)+'#contact">Message</a>' +
'</div>' +
'</div>';
}
function applyFilters(){
var q = (document.getElementById('map-search').value || '').toLowerCase().trim();
var vert = document.getElementById('map-vertical').value;
cluster.clearLayers();
var n = 0;
allMarkers.forEach(function(rec){
var b = rec.data;
if (q) {
var hay = (b.business_name + ' ' + (b.headline||'') + ' ' + (b.neighborhood||'') + ' ' + (b.city||'') + ' ' + b.vertical).toLowerCase();
if (hay.indexOf(q) === -1) return;
}
if (vert && b.vertical !== vert) return;
cluster.addLayer(rec.marker);
n++;
});
var statsEl = document.querySelector('.map-stats');
if (statsEl) statsEl.textContent = n + ' pinned · click a pin for the profile';
}
fetch('/api/businesses.geo')
.then(function(r){ return r.json(); })
.then(function(data){
(data.businesses || []).forEach(function(b){
if (typeof b.latitude !== 'number' || typeof b.longitude !== 'number') return;
var m = L.marker([b.latitude, b.longitude], { icon: pinIcon(b) });
m.bindPopup(popupHtml(b));
// Hover preview — quick name + vertical without forcing a click.
// 'sticky' keeps it pinned to the cursor on the marker; auto-hide on leave.
var verticalLabel = (b.vertical || '').replace(/_/g, ' ');
m.bindTooltip(
'<strong>' + escHtml(b.business_name) + '</strong>'
+ '<br><span style="font-size:11px;color:#b8860b;letter-spacing:0.06em;text-transform:uppercase">' + escHtml(verticalLabel) + '</span>'
+ (b.neighborhood ? '<br><span style="font-size:11px;color:#666">' + escHtml(b.neighborhood) + '</span>' : ''),
{ direction: 'top', offset: [0, -8], opacity: 0.95, sticky: false, className: 'pin-hover' }
);
cluster.addLayer(m);
allMarkers.push({ data: b, marker: m });
});
if (allMarkers.length) {
var bounds = L.latLngBounds(allMarkers.map(function(r){ return r.marker.getLatLng(); }));
map.fitBounds(bounds, { padding: [40,40], maxZoom: 13 });
}
// Pre-fill from URL query so /find ↔ /map hand-off keeps state.
var u = new URLSearchParams(window.location.search);
var qp = u.get('q'); if (qp) document.getElementById('map-search').value = qp;
var vp = u.get('vertical'); if (vp) document.getElementById('map-vertical').value = vp;
function syncListLink(){
var p = new URLSearchParams();
var qq = document.getElementById('map-search').value.trim();
var vv = document.getElementById('map-vertical').value;
if (qq) p.set('q', qq);
if (vv) p.set('vertical', vv);
var link = document.getElementById('map-list-link');
if (link) link.href = '/find' + (p.toString() ? '?' + p.toString() : '');
}
['map-search','map-vertical'].forEach(function(id){
var el = document.getElementById(id);
if (!el) return;
el.addEventListener('input', applyFilters);
el.addEventListener('input', syncListLink);
el.addEventListener('change', applyFilters);
el.addEventListener('change', syncListLink);
});
if (qp || vp) applyFilters();
syncListLink();
})
.catch(function(e){ console.error('[map] load failed', e); });
})();
</script>