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