← 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: '&copy; <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 {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>