← back to NationalPaperHangers
views/public/map.ejs
193 lines
<%- include('../partials/head', { title }) %>
<link rel="stylesheet" href="/vendor/leaflet/leaflet.css">
<style>
.near-me-page { max-width: 1200px; margin: 0 auto; padding: 32px 24px; }
.near-me-head h1 { font-size: 32px; letter-spacing: 0.005em; margin: 0 0 8px; }
.near-me-head p.lede { margin: 0 0 20px; max-width: 560px; line-height: 1.55; font-size: 15px; }
.near-me-form {
display: flex; gap: 10px; flex-wrap: wrap; margin: 0 0 24px;
padding: 16px; border: 1px solid var(--border, #ddd); border-radius: 8px;
background: var(--bg-subtle, #faf9f7);
}
.near-me-form label { flex: 1; min-width: 280px; font-size: 13px; color: #666; }
.near-me-form input[type="text"] {
width: 100%; padding: 12px 14px; font-size: 15px;
border: 1px solid var(--border, #ddd); border-radius: 6px;
margin-top: 4px;
}
.near-me-form button { padding: 12px 22px; font-size: 15px; }
.near-me-geo-btn {
background: transparent; border: 1px solid var(--border, #ddd);
color: inherit; cursor: pointer; padding: 12px 18px;
border-radius: 6px; font-size: 14px;
margin-top: 19px;
}
.near-me-layout { display: grid; grid-template-columns: 1fr 1.2fr; gap: 24px; align-items: start; }
@media (max-width: 800px) { .near-me-layout { grid-template-columns: 1fr; } }
#near-me-map { height: 520px; border-radius: 8px; overflow: hidden; border: 1px solid var(--border, #ddd); }
.results-pane h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; color: #999; margin: 0 0 12px; }
.results-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 12px; max-height: 520px; overflow-y: auto; }
.result-card { padding: 14px 16px; border: 1px solid var(--border, #ddd); border-radius: 6px; transition: border-color 0.15s; }
.result-card:hover { border-color: #0e0e0e; }
.result-card .name { font-weight: 600; font-size: 15px; }
.result-card .meta { font-size: 13px; color: #666; margin-top: 4px; }
.result-card .reasons { font-size: 12px; color: #15803d; margin-top: 6px; }
.tier-badge { display: inline-block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; padding: 2px 8px; border-radius: 10px; vertical-align: middle; margin-left: 6px; }
.tier-signature, .tier-enterprise { background: #0e0e0e; color: #fff; }
.tier-pro { background: #f3f4f6; color: #374151; }
.empty-state { padding: 32px; border: 1px dashed var(--border, #ddd); border-radius: 8px; text-align: center; color: #666; }
</style>
<%- include('../partials/header') %>
<section class="near-me-page">
<header class="near-me-head">
<h1>Who services my area?</h1>
<p class="lede">Enter your address and we'll show every wallcovering installer who services your location — sorted by distance. <%= stats.pinned %>+ studios are mapped.</p>
</header>
<form method="get" action="/map" class="near-me-form" autocomplete="off">
<label>
Your address, city, or ZIP
<input type="text" name="address" value="<%= address || '' %>" required
placeholder="e.g. 90210, or 123 Main St, Brooklyn NY"
autofocus>
</label>
<button type="submit" class="btn btn-primary">Show installers near me →</button>
<button type="button" class="near-me-geo-btn" id="useLocBtn" title="Use my browser-detected location">📍 Use my location</button>
</form>
<% if (geocodeFailed) { %>
<div class="callout" style="padding:14px 18px;border:1px solid #fca5a5;background:#fef2f2;border-radius:6px;margin-bottom:24px">
<strong>Couldn't find that address.</strong> Try adding the city + state (e.g. "Brooklyn, NY") or a ZIP code.
</div>
<% } %>
<% if (location) { %>
<div class="near-me-layout">
<div>
<h2 style="margin:0 0 8px;font-size:14px;text-transform:uppercase;letter-spacing:0.08em;color:#999">Your area</h2>
<p style="margin:0 0 12px;font-size:14px"><%= location.display_name %></p>
<div id="near-me-map" data-lat="<%= location.lat %>" data-lng="<%= location.lng %>"
data-installers='<%- JSON.stringify(results.map(r => ({ id:r.id, slug:r.slug, name:r.business_name, lat:r.lat, lng:r.lng, distance:r.distance_miles, tier:r.tier }))) %>'></div>
</div>
<div class="results-pane">
<h2><%= results.length %> studio<%= results.length === 1 ? '' : 's' %> service this area</h2>
<% if (!results.length) { %>
<div class="empty-state">
<p style="margin:0 0 6px"><strong>No installer-marked service coverage for this location.</strong></p>
<p style="margin:0;font-size:13px">Try a nearby ZIP, or <a href="/find">browse all studios</a> and reach out directly — many will travel for the right project.</p>
</div>
<% } else { %>
<ul class="results-list">
<% results.forEach(function(r){ %>
<li class="result-card">
<a href="/installer/<%= r.slug %>" style="text-decoration:none;color:inherit;display:block">
<div>
<span class="name"><%= r.business_name %></span>
<% if (r.tier === 'signature' || r.tier === 'enterprise') { %>
<span class="tier-badge tier-<%= r.tier %>"><%= r.tier %></span>
<% } else if (r.tier === 'pro') { %>
<span class="tier-badge tier-pro">pro</span>
<% } %>
<% if (r.verified) { %><span style="font-size:12px;color:#888"> · verified</span><% } %>
</div>
<div class="meta">
<%= r.distance_miles %> mi · <%= r.city %><% if (r.state) { %>, <%= r.state %><% } %>
<% if (r.response_time_hours && r.claim_status !== 'unclaimed') { %>
· replies in < <%= r.response_time_hours %>h
<% } %>
<% if (r.verified_brands_count) { %>
· <%= r.verified_brands_count %> brand-trained
<% } %>
</div>
<% if (r.match_reasons && r.match_reasons.length) { %>
<div class="reasons">✓ <%= r.match_reasons.join(' · ') %></div>
<% } %>
</a>
</li>
<% }); %>
</ul>
<% } %>
</div>
</div>
<% } %>
</section>
<% if (location) { %>
<script src="/vendor/leaflet/leaflet.js"></script>
<script>
(function(){
var mapEl = document.getElementById('near-me-map');
if (!mapEl || !window.L) return;
var ulat = parseFloat(mapEl.dataset.lat);
var ulng = parseFloat(mapEl.dataset.lng);
var insts = [];
try { insts = JSON.parse(mapEl.dataset.installers || '[]'); } catch(e) {}
var maxDist = insts.length ? Math.max.apply(null, insts.map(function(i){return i.distance;})) : 5;
var zoom = maxDist < 5 ? 12 : maxDist < 20 ? 10 : maxDist < 60 ? 9 : 8;
var map = L.map(mapEl).setView([ulat, ulng], zoom);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '© OpenStreetMap'
}).addTo(map);
L.circleMarker([ulat, ulng], {
radius: 9, color: '#dc2626', weight: 3, fillColor: '#fff', fillOpacity: 1
}).addTo(map).bindPopup('You');
insts.forEach(function(i){
var tierColor = i.tier === 'signature' || i.tier === 'enterprise' ? '#0e0e0e'
: i.tier === 'pro' ? '#374151'
: '#9ca3af';
L.circleMarker([i.lat, i.lng], {
radius: 7, color: tierColor, weight: 2, fillColor: '#fff', fillOpacity: 1
}).addTo(map).bindPopup(
'<a href="/installer/' + i.slug + '" style="color:inherit;font-weight:600">' + i.name + '</a><br>' +
'<span style="font-size:12px;color:#666">' + i.distance + ' mi away</span>'
);
});
if (insts.length) {
var pts = insts.map(function(i){ return [i.lat, i.lng]; }).concat([[ulat, ulng]]);
map.fitBounds(L.latLngBounds(pts), { padding: [40, 40], maxZoom: 12 });
}
})();
</script>
<% } %>
<script>
(function(){
var btn = document.getElementById('useLocBtn');
var input = document.querySelector('input[name="address"]');
if (!btn || !navigator.geolocation) { if (btn) btn.style.display = 'none'; return; }
btn.addEventListener('click', function(){
btn.textContent = '📍 Finding…';
btn.disabled = true;
navigator.geolocation.getCurrentPosition(function(pos){
var url = 'https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=' +
pos.coords.latitude + '&lon=' + pos.coords.longitude;
fetch(url, { headers: { 'Accept': 'application/json' } })
.then(function(r){ return r.json(); })
.then(function(d){
input.value = (d && d.display_name) || (pos.coords.latitude.toFixed(5) + ', ' + pos.coords.longitude.toFixed(5));
input.form.submit();
})
.catch(function(){
input.value = pos.coords.latitude.toFixed(5) + ', ' + pos.coords.longitude.toFixed(5);
input.form.submit();
});
}, function(err){
btn.textContent = '📍 Use my location';
btn.disabled = false;
alert('Could not get your location: ' + err.message);
}, { timeout: 8000 });
});
})();
</script>
<%- include('../partials/footer') %>