← back to Ventura Claw Leads

views/public/neighborhood.ejs

183 lines

<%- include('../partials/head', { title }) %>
<%- include('../partials/header') %>

<%
  // BreadcrumbList JSON-LD: Home › Find › Neighborhood
  var _publicUrl = (typeof publicUrl !== 'undefined' && publicUrl) ? publicUrl.replace(/\/+$/, '') : 'https://leads.venturaclaw.com';
  var _ldCrumbs = {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: [
      { '@type': 'ListItem', position: 1, name: 'Home',  item: _publicUrl + '/' },
      { '@type': 'ListItem', position: 2, name: 'Find a business', item: _publicUrl + '/find' },
      { '@type': 'ListItem', position: 3, name: neighborhood.label, item: _publicUrl + '/neighborhood/' + neighborhood.slug }
    ]
  };
  // ItemList JSON-LD — same shape as /find. Surfaces the neighborhood page
  // as a structured list of LocalBusiness entries to Google. Capped at 30
  // items (Google's recommended depth for ItemList rich results).
  var _ldList = {
    '@context': 'https://schema.org',
    '@type': 'ItemList',
    name: neighborhood.label + ' businesses on Ventura Blvd',
    itemListOrder: 'https://schema.org/ItemListOrderAscending',
    numberOfItems: (typeof totalCount !== 'undefined' && totalCount) ? totalCount : businesses.length,
    itemListElement: businesses.slice(0, 30).map(function(b, i){
      return {
        '@type': 'ListItem',
        position: i + 1,
        url: _publicUrl + '/business/' + b.slug,
        name: b.business_name
      };
    })
  };
%>
<script type="application/ld+json"><%- JSON.stringify(_ldCrumbs) %></script>
<script type="application/ld+json"><%- JSON.stringify(_ldList) %></script>

<nav class="breadcrumb" aria-label="Breadcrumb" style="padding:16px 28px 0;max-width:1440px;margin:0 auto;font-size:12px;color:var(--fg-muted);letter-spacing:0.04em">
  <a href="/" style="border:none;color:var(--fg-muted)">Home</a>
  <span style="margin:0 6px;opacity:0.5">›</span>
  <a href="/find" style="border:none;color:var(--fg-muted)">Find</a>
  <span style="margin:0 6px;opacity:0.5">›</span>
  <span style="color:var(--fg)"><%= neighborhood.label %></span>
</nav>

<section class="find-page">
  <p class="kicker">On Ventura Blvd</p>
  <h1 class="display-sm">Businesses in <%= neighborhood.label %></h1>
  <%
    // Use !== undefined (not truthiness) so a legitimate 0 doesn't fall
    // back to businesses.length. PROSECUTOR caught the falsy-zero edge in
    // the codex-3way sweep; harmless in practice (both equal 0 for empty
    // neighborhoods) but cleaner this way.
    var _displayTotal = (typeof totalCount !== 'undefined') ? totalCount : businesses.length;
  %>
  <p class="lede"><%= _displayTotal %> business<%= _displayTotal === 1 ? '' : 'es' %> in <%= neighborhood.label %>. Search by name, browse by category, or jump straight to a profile and send a message.</p>

  <%
    // Per-vertical chip row + an inline 'browse by category' paragraph that
    // gives Google crawlable anchor text linking each vertical+city combo
    // page. Hide verticals with 0 matches so we don't ship soft-404 chips.
    var _vMap = (typeof verticalCountMap !== 'undefined') ? verticalCountMap : {};
    var _activeVerticals = verticals.filter(function(v){ return (_vMap[v.key] || 0) > 0; });
  %>
  <div class="grid-controls" role="region" aria-label="Filters" style="margin-top:24px">
    <div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
      <% _activeVerticals.forEach(function(v){
           var n = _vMap[v.key] || 0;
      %>
        <a href="/find?vertical=<%= v.key %>&city=<%= encodeURIComponent(neighborhood.label) %>" class="btn btn-ghost btn-sm" style="font-size:12px">
          <%= v.icon %> <%= v.label %> <span class="muted" style="font-size:11px;font-weight:400">·&nbsp;<%= n %></span>
        </a>
      <% }); %>
    </div>
    <% var _sort = (typeof sort === 'string') ? sort : 'featured'; %>
    <div class="grid-controls-right">
      <label class="grid-control" for="grid-sort"><span>Sort</span>
        <select id="grid-sort" aria-label="Sort businesses by">
          <option value="featured"  <%= _sort === 'featured'  ? 'selected' : '' %>>Featured</option>
          <option value="newest"    <%= _sort === 'newest'    ? 'selected' : '' %>>Newest</option>
          <option value="updated"   <%= _sort === 'updated'   ? 'selected' : '' %>>Recently updated</option>
          <option value="name_asc"  <%= _sort === 'name_asc'  ? 'selected' : '' %>>Name A→Z</option>
          <option value="name_desc" <%= _sort === 'name_desc' ? 'selected' : '' %>>Name Z→A</option>
          <option value="vertical"  <%= _sort === 'vertical'  ? 'selected' : '' %>>By category</option>
        </select>
      </label>
      <label class="grid-control"><span>Density</span>
        <input type="range" id="grid-density" min="220" max="480" step="20" value="300" aria-label="Card width">
      </label>
    </div>
  </div>

  <% if (_activeVerticals.length > 1) { %>
    <p class="muted" style="margin:14px 0 0;font-size:13px;line-height:1.7">
      Jump to a category in <%= neighborhood.label %>:
      <% _activeVerticals.forEach(function(v, i){
           var n = _vMap[v.key] || 0;
      %><% if (i > 0) { %>, <% } %><a href="/find?vertical=<%= v.key %>&city=<%= encodeURIComponent(neighborhood.label) %>" style="font-weight:600"><%= v.label.toLowerCase() %> in <%= neighborhood.label %></a> (<%= n %>)<% }); %>.
    </p>
  <% } %>

  <% if (businesses.length === 0) { %>
    <div style="padding:40px 0;text-align:center">
      <h3>No businesses listed in <%= neighborhood.label %> yet.</h3>
      <p class="muted">We're adding new businesses every week. <a href="/find">Browse all listings</a>.</p>
    </div>
  <% } else { %>
    <div class="business-grid" id="grid" style="margin-top:24px">
      <% businesses.forEach(function(b){
           var vMeta = verticals.find(function(vv){return vv.key===b.vertical});
      %>
        <a class="business-card" href="/business/<%= b.slug %>">
          <% if (b.photo_path) { %>
            <div class="biz-card-hero biz-card-hero-photo">
              <img src="<%= b.photo_path %>" alt="<%= b.business_name %>" loading="lazy" decoding="async">
            </div>
          <% } else { %>
            <div class="biz-card-hero vertical-hero-<%= b.vertical %>" aria-hidden="true">
              <%- include('../partials/vertical-glyph', { verticalKey: b.vertical }) %>
            </div>
          <% } %>
          <p class="biz-vertical"><%= vMeta ? vMeta.label : b.vertical %></p>
          <p class="biz-name"><%= b.business_name %></p>
          <p class="biz-loc"><%= [b.neighborhood, b.city, b.state].filter(Boolean).join(' · ') %></p>
          <% if (b.headline) { %><p class="biz-headline"><%= b.headline %></p><% } %>
          <div class="biz-badges">
            <% if (b.tier === 'premier') { %><span class="biz-badge is-premier">Featured</span>
            <% } else if (b.tier === 'standard') { %><span class="biz-badge is-standard">Standard</span><% } %>
            <% if (b.claim_status === 'self' || b.claim_status === 'claimed') { %><span class="biz-badge is-claimed">Claimed</span><% } %>
            <% if (b.verified) { %><span class="biz-badge">Verified</span><% } %>
          </div>
        </a>
      <% }); %>
    </div>
  <% } %>
</section>

<script>
  // Sort + density: same pattern as /find — persist to localStorage,
  // hydrate the saved value BEFORE the first grid render, sort change
  // reloads with ?sort=, density updates a CSS var live (no reload).
  // Reuses the 'vcl-grid-density' / 'vcl-grid-sort' keys so preferences
  // carry between /find and /neighborhood/:slug.
  (function(){
    var grid = document.getElementById('grid');
    var sortSel = document.getElementById('grid-sort');
    var dens = document.getElementById('grid-density');
    if (!grid || !sortSel || !dens) return;

    function applyDensity(px){
      grid.style.setProperty('--vcl-card-min', px + 'px');
      try { localStorage.setItem('vcl-grid-density', String(px)); } catch(e){}
    }
    var savedDens = null;
    try { savedDens = localStorage.getItem('vcl-grid-density'); } catch(e){}
    if (savedDens && Number(savedDens) >= 220 && Number(savedDens) <= 480) {
      dens.value = savedDens;
      applyDensity(Number(savedDens));
    } else {
      applyDensity(Number(dens.value));
    }
    dens.addEventListener('input', function(){ applyDensity(Number(this.value)); });

    var savedSort = null;
    try { savedSort = localStorage.getItem('vcl-grid-sort'); } catch(e){}
    var urlSort = new URLSearchParams(window.location.search).get('sort');
    if (!urlSort && savedSort && savedSort !== 'featured') {
      var p = new URLSearchParams(window.location.search);
      p.set('sort', savedSort);
      window.location.search = p.toString();
      return;
    }
    sortSel.addEventListener('change', function(){
      try { localStorage.setItem('vcl-grid-sort', this.value); } catch(e){}
      var p = new URLSearchParams(window.location.search);
      if (this.value === 'featured') p.delete('sort'); else p.set('sort', this.value);
      window.location.search = p.toString();
    });
  })();
</script>

<%- include('../partials/footer') %>