[cabo-voting-app] Multi-provider map search: Yelp/OSM/All tabs + quick-book aggregators + legend update
This commit is contained in:
@@ -109,54 +109,91 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Yelp search button */
|
/* Yelp search button */
|
||||||
|
/* Multi-provider search bar */
|
||||||
#map-search-wrap {
|
#map-search-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 0;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
background: rgba(19,22,31,0.95);
|
background: rgba(19,22,31,0.95);
|
||||||
border: 1px solid #252a38;
|
border: 1px solid #252a38;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
max-width: 360px;
|
max-width: 480px;
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
#map-search-wrap:focus-within {
|
#map-search-wrap:focus-within { border-color: var(--accent); }
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
#map-search-input {
|
#map-search-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: #e0e6f0;
|
color: #e0e6f0;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
outline: none;
|
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
outline: none;
|
||||||
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
#map-search-input::placeholder { color: #7a8499; }
|
#map-search-input::placeholder { color: #7a8499; }
|
||||||
.yelp-search-btn {
|
.provider-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: rgba(255,107,53,0.15);
|
|
||||||
border: none;
|
|
||||||
border-left: 1px solid #252a38;
|
border-left: 1px solid #252a38;
|
||||||
color: #ff6b35;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.provider-tab {
|
||||||
|
padding: 5px 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #555;
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
transition: background 0.15s;
|
transition: all 0.15s;
|
||||||
|
border-right: 1px solid #252a38;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
}
|
}
|
||||||
.yelp-search-btn:hover { background: rgba(255,107,53,0.25); }
|
.provider-tab:last-child { border-right: none; }
|
||||||
.yelp-search-btn.searching {
|
.provider-tab:hover { color: #ccc; background: rgba(255,255,255,0.04); }
|
||||||
opacity: 0.7;
|
.provider-tab.active-yelp { color: #ff6b35; background: rgba(255,107,53,0.12); }
|
||||||
cursor: wait;
|
.provider-tab.active-osm { color: #fbbf24; background: rgba(251,191,36,0.10); }
|
||||||
|
.provider-tab.active-all { color: #00d4ff; background: rgba(0,212,255,0.08); }
|
||||||
|
#map-search-btn {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #00d4ff;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-left: 1px solid #252a38;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
#map-search-btn:hover { background: rgba(0,212,255,0.06); }
|
||||||
|
/* Quick-book mini bar */
|
||||||
|
.map-quick-book {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.qb-label { color: #444; font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin-right: 2px; }
|
||||||
|
.qb-btn {
|
||||||
|
display: flex; align-items: center; gap: 2px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border: 1px solid #252a38;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
color: #555;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.6rem; font-weight: 600;
|
||||||
|
cursor: pointer; white-space: nowrap; text-decoration: none;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.qb-btn:hover { border-color: #444; color: #ccc; background: rgba(255,255,255,0.07); }
|
||||||
|
/* Legend: multi-provider dots */
|
||||||
|
.legend-dot-osm { background: #fbbf24; width: 9px; height: 9px; border-radius: 50%; display: inline-block; margin-right: 4px; border: 1px solid rgba(0,0,0,0.3); }
|
||||||
|
.legend-dot-all { background: conic-gradient(#ff6b35 33%, #fbbf24 33%, #fbbf24 66%, #00d4ff 66%); }
|
||||||
|
|
||||||
/* Filter bar layout */
|
/* Filter bar layout */
|
||||||
.map-overlay {
|
.map-overlay {
|
||||||
@@ -972,16 +1009,18 @@
|
|||||||
<div id="map-view" style="display:none;">
|
<div id="map-view" style="display:none;">
|
||||||
<div id="cabo-map"></div>
|
<div id="cabo-map"></div>
|
||||||
<div class="map-overlay">
|
<div class="map-overlay">
|
||||||
<!-- Row 1: Search + Yelp -->
|
<!-- Row 1: Multi-provider search -->
|
||||||
<div id="map-search-wrap">
|
<div id="map-search-wrap">
|
||||||
<span style="color:#7a8499;font-size:0.8rem;padding-left:8px;">🔍</span>
|
<span style="color:#7a8499;font-size:0.8rem;padding-left:8px;flex-shrink:0;">🔍</span>
|
||||||
<input type="text" id="map-search-input" placeholder="Search venues in Los Cabos…" autocomplete="off" />
|
<input type="text" id="map-search-input" placeholder="Search Los Cabos…" autocomplete="off" />
|
||||||
<div id="map-search-results"></div>
|
<div class="provider-tabs">
|
||||||
<button class="yelp-search-btn" id="yelp-search-btn" onclick="mapYelpSearch()">
|
<button class="provider-tab active-yelp" id="tab-yelp" onclick="setProvider('yelp')">🍴 Yelp</button>
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="#ff6b35"><path d="M12 2C8.13 2 5 5.13 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h1v1c0 .55.45 1 1 1h1.5c.28 0 .5-.22.5-.5v-1h1c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.87-3.13-7-7-7zm0 2c2.76 0 5 2.24 5 5 0 1.65-.8 3.1-2 3.92V14h-1v1.5c0 .28-.22.5-.5.5H13v1h-.5c-.28 0-.5-.22-.5-.5V13H11v-.08C9.8 10.1 9 8.65 9 7c0-2.76 2.24-5 5-5z"/></svg>
|
<button class="provider-tab" id="tab-osm" onclick="setProvider('osm')">📍 OSM</button>
|
||||||
Yelp
|
<button class="provider-tab" id="tab-all" onclick="setProvider('all')">⚡ All</button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button id="map-search-btn" onclick="mapDoSearch()">→</button>
|
||||||
|
</div>
|
||||||
|
<div id="map-search-results"></div>
|
||||||
<!-- Row 2: Category filters -->
|
<!-- Row 2: Category filters -->
|
||||||
<div class="map-filter-row">
|
<div class="map-filter-row">
|
||||||
<span class="row-label">Show</span>
|
<span class="row-label">Show</span>
|
||||||
@@ -1000,7 +1039,9 @@
|
|||||||
<div class="legend-item"><div class="legend-dot" style="background:#06b6d4"></div> Excursion</div>
|
<div class="legend-item"><div class="legend-dot" style="background:#06b6d4"></div> Excursion</div>
|
||||||
<div class="legend-item"><div class="legend-dot" style="background:#a855f7"></div> Nightlife</div>
|
<div class="legend-item"><div class="legend-dot" style="background:#a855f7"></div> Nightlife</div>
|
||||||
<div class="legend-item"><div class="legend-dot" style="background:#fbbf24"></div> Itinerary</div>
|
<div class="legend-item"><div class="legend-dot" style="background:#fbbf24"></div> Itinerary</div>
|
||||||
<div class="legend-item"><div class="legend-dot" style="background:#ff6b35;opacity:0.8"></div> Yelp results</div>
|
<div class="legend-item"><div class="legend-dot" style="background:#ff6b35;opacity:0.8"></div> Yelp</div>
|
||||||
|
<div class="legend-item"><div class="legend-dot legend-dot-osm"></div> OSM</div>
|
||||||
|
<div class="legend-item"><div class="legend-dot" style="background:conic-gradient(#ff6b35 33%, #fbbf24 33%, #fbbf24 66%, #00d4ff 66%);width:9px;height:9px;border-radius:50%;display:inline-block;border:1px solid rgba(0,0,0,0.3)"></div> All sources</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1441,12 +1482,12 @@
|
|||||||
const q = searchInput.value.trim();
|
const q = searchInput.value.trim();
|
||||||
if (q.length < 2) { searchResults.classList.remove('show'); return; }
|
if (q.length < 2) { searchResults.classList.remove('show'); return; }
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
searchTimeout = setTimeout(() => mapDoSearch(q), 400);
|
searchTimeout = setTimeout(() => mapDoSearch(), 400);
|
||||||
});
|
});
|
||||||
searchInput.addEventListener('keydown', e => {
|
searchInput.addEventListener('keydown', e => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
const q = searchInput.value.trim();
|
const q = searchInput.value.trim();
|
||||||
if (q) { mapYelpSearch(q); searchResults.classList.remove('show'); }
|
if (q) { mapDoSearch(); searchResults.classList.remove('show'); }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.addEventListener('click', e => {
|
document.addEventListener('click', e => {
|
||||||
@@ -1591,7 +1632,31 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── External / Yelp search ────────────────────────────────
|
// ── Multi-provider search ───────────────────────────────
|
||||||
|
let currentProvider = 'yelp'; // 'yelp' | 'osm' | 'all'
|
||||||
|
|
||||||
|
function setProvider(p) {
|
||||||
|
currentProvider = p;
|
||||||
|
document.getElementById('tab-yelp').className = 'provider-tab' + (p === 'yelp' ? ' active-yelp' : '');
|
||||||
|
document.getElementById('tab-osm').className = 'provider-tab' + (p === 'osm' ? ' active-osm' : '');
|
||||||
|
document.getElementById('tab-all').className = 'provider-tab' + (p === 'all' ? ' active-all' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickBook(type) {
|
||||||
|
const q = (document.getElementById('map-search-input').value || '').trim() || 'Los Cabos Mexico';
|
||||||
|
let url;
|
||||||
|
switch(type) {
|
||||||
|
case 'gmaps': url = `https://www.google.com/maps/search/${encodeURIComponent(q)}`; break;
|
||||||
|
case 'flights': url = `https://www.google.com/travel/flights/search?q=${encodeURIComponent(q)}&tfpla=on`; break;
|
||||||
|
case 'hotels': url = `https://www.kayak.com/hotels/${encodeURIComponent(q)}/2admins`; break;
|
||||||
|
case 'viator': url = `https://www.viator.com/search/${encodeURIComponent(q)}`; break;
|
||||||
|
case 'expedia': url = `https://www.expedia.com/Thotel-Search?destination=${encodeURIComponent(q)}`; break;
|
||||||
|
case 'tripadvisor': url = `https://www.tripadvisor.com/Search?q=${encodeURIComponent(q)}`; break;
|
||||||
|
default: return;
|
||||||
|
}
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
|
}
|
||||||
|
|
||||||
function mapClearExtMarkers() {
|
function mapClearExtMarkers() {
|
||||||
mapExtMarkers.forEach(m => mapInstance.removeLayer(m));
|
mapExtMarkers.forEach(m => mapInstance.removeLayer(m));
|
||||||
mapExtMarkers = [];
|
mapExtMarkers = [];
|
||||||
@@ -1599,61 +1664,48 @@
|
|||||||
|
|
||||||
async function mapYelpSearch(q) {
|
async function mapYelpSearch(q) {
|
||||||
const input = document.getElementById('map-search-input');
|
const input = document.getElementById('map-search-input');
|
||||||
const btn = document.getElementById('yelp-search-btn');
|
|
||||||
const results = document.getElementById('map-search-results');
|
const results = document.getElementById('map-search-results');
|
||||||
q = (q || input.value || '').trim();
|
q = (q || input.value || '').trim();
|
||||||
if (!q) return;
|
if (!q) return;
|
||||||
|
|
||||||
btn.classList.add('searching');
|
results.innerHTML = '<div class="msr-item" style="color:#ff6b35;">Searching Yelp…</div>';
|
||||||
btn.textContent = '...';
|
|
||||||
results.innerHTML = '<div class="msr-item" style="color:#ff6b35;">Searching Yelp...</div>';
|
|
||||||
results.classList.add('show');
|
results.classList.add('show');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call our backend proxy (keeps API key server-side)
|
|
||||||
const res = await fetch(`/api/yelp?term=${encodeURIComponent(q)}&location=Los+Cabos+Mexico`);
|
const res = await fetch(`/api/yelp?term=${encodeURIComponent(q)}&location=Los+Cabos+Mexico`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.error) throw new Error(data.error);
|
if (data.error) throw new Error(data.error);
|
||||||
|
|
||||||
mapClearExtMarkers();
|
mapClearExtMarkers();
|
||||||
|
|
||||||
if (!data.businesses || data.businesses.length === 0) {
|
if (!data.businesses || data.businesses.length === 0) {
|
||||||
results.innerHTML = '<div class="msr-item" style="color:#7a8499;">No Yelp results found. Try a different search.</div>';
|
results.innerHTML = '<div class="msr-item" style="color:#7a8499;">No Yelp results. Try OSM or All.</div>';
|
||||||
} else {
|
} else {
|
||||||
// Build search results dropdown
|
|
||||||
results.innerHTML = data.businesses.slice(0, 8).map((b, i) => {
|
results.innerHTML = data.businesses.slice(0, 8).map((b, i) => {
|
||||||
const cat = b.categories?.[0]?.title || 'Place';
|
const cat = b.categories?.[0]?.title || 'Place';
|
||||||
const rating = b.rating ? `⭐ ${b.rating}` : '';
|
const rating = b.rating ? `⭐ ${b.rating}` : '';
|
||||||
const price = b.price || '';
|
const price = b.price || '';
|
||||||
return `<div class="msr-item" onclick="mapShowExtResult(${i})">
|
return `<div class="msr-item" onclick="mapShowExtResult(${i})">
|
||||||
<span style="font-size:0.7rem;">🍴</span>
|
<span style="font-size:0.7rem;">🍴</span>
|
||||||
<span>
|
<span><strong>${b.name}</strong> <span style="color:#7a8499;font-size:0.65rem;">· ${cat} · ${rating} ${price}</span></span>
|
||||||
<strong>${b.name}</strong>
|
|
||||||
<span style="color:#7a8499;font-size:0.65rem;"> · ${cat} · ${rating} ${price}</span>
|
|
||||||
</span>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
// Store for later reference
|
|
||||||
window._mapYelpResults = data.businesses;
|
window._mapYelpResults = data.businesses;
|
||||||
|
|
||||||
// Show all results as dashed markers on map
|
|
||||||
data.businesses.forEach((b, i) => {
|
data.businesses.forEach((b, i) => {
|
||||||
if (!b.coordinates || !b.coordinates.latitude || !b.coordinates.longitude) return;
|
if (!b.coordinates?.latitude || !b.coordinates?.longitude) return;
|
||||||
if (!b.location?.city?.toLowerCase().includes('cabo') &&
|
const city = b.location?.city?.toLowerCase() || '';
|
||||||
!b.location?.state?.includes('BS') &&
|
const state = b.location?.state || '';
|
||||||
!b.location?.state?.includes('Baja')) return; // Only show results near Los Cabos
|
if (!city.includes('cabo') && !state.includes('BS') && !state.includes('Baja')) return;
|
||||||
|
|
||||||
const emoji = '🍴';
|
const icon = mapMakeExtIcon('#ff6b35', '🍴', 30);
|
||||||
const icon = mapMakeExtIcon('#ff6b35', emoji, 30);
|
|
||||||
const marker = L.marker([b.coordinates.latitude, b.coordinates.longitude], { icon })
|
const marker = L.marker([b.coordinates.latitude, b.coordinates.longitude], { icon })
|
||||||
.addTo(mapInstance)
|
.addTo(mapInstance)
|
||||||
.bindPopup(mapMakeExtPopup(b), { className: 'map-popup-wrapper' });
|
.bindPopup(mapMakeExtPopup(b), { className: 'map-popup-wrapper' });
|
||||||
mapExtMarkers.push(marker);
|
mapExtMarkers.push(marker);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fit map to show all results
|
|
||||||
if (mapExtMarkers.length > 0) {
|
if (mapExtMarkers.length > 0) {
|
||||||
const group = L.featureGroup(mapExtMarkers);
|
const group = L.featureGroup(mapExtMarkers);
|
||||||
mapInstance.fitBounds(group.getBounds(), { padding: [50, 50], maxZoom: 13 });
|
mapInstance.fitBounds(group.getBounds(), { padding: [50, 50], maxZoom: 13 });
|
||||||
@@ -1661,92 +1713,159 @@
|
|||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error('Yelp search error:', err);
|
console.error('Yelp search error:', err);
|
||||||
results.innerHTML = '<div class="msr-item" style="color:#f87171;">Yelp search failed. Try again.</div>';
|
results.innerHTML = '<div class="msr-item" style="color:#f87171;">Yelp failed. Try OSM or All.</div>';
|
||||||
} finally {
|
}
|
||||||
btn.classList.remove('searching');
|
}
|
||||||
btn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="#ff6b35"><path d="M12 2C8.13 2 5 5.13 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h1v1c0 .55.45 1 1 1h1.5c.28 0 .5-.22.5-.5v-1h1c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.87-3.13-7-7-7zm0 2c2.76 0 5 2.24 5 5 0 1.65-.8 3.1-2 3.92V14h-1v1.5c0 .28-.22.5-.5.5H13v1h-.5c-.28 0-.5-.22-.5-.5V13H11v-.08C9.8 10.1 9 8.65 9 7c0-2.76 2.24-5 5-5z"/></svg> Yelp`;
|
|
||||||
|
// ── OSM search ──────────────────────────────────────────
|
||||||
|
async function mapOSMSearch(q) {
|
||||||
|
const input = document.getElementById('map-search-input');
|
||||||
|
const results = document.getElementById('map-search-results');
|
||||||
|
q = (q || input.value || '').trim();
|
||||||
|
if (!q) return;
|
||||||
|
|
||||||
|
results.innerHTML = '<div class="msr-item" style="color:#fbbf24;">Searching OSM…</div>';
|
||||||
|
results.classList.add('show');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(q + ', Los Cabos, Mexico')}&limit=8&viewbox=${CABOS_BBOX}&bounded=1`,
|
||||||
|
{ headers: { 'Accept': 'application/json' } }
|
||||||
|
).then(r => r.json()).catch(() => []);
|
||||||
|
|
||||||
|
mapClearExtMarkers();
|
||||||
|
|
||||||
|
if (!res || res.length === 0) {
|
||||||
|
results.innerHTML = '<div class="msr-item" style="color:#7a8499;">No OSM results. Try Yelp or All.</div>';
|
||||||
|
} else {
|
||||||
|
results.innerHTML = res.map((r, i) => {
|
||||||
|
const type = (r.type || 'place');
|
||||||
|
const addr = r.display_name.split(',').slice(1, 4).join(',').trim();
|
||||||
|
return `<div class="msr-item" onclick="mapShowOSMResult(${i},${r.lat},${r.lon},'${r.display_name.replace(/'/g,"\\'")}')">
|
||||||
|
<span style="font-size:0.7rem;">📍</span>
|
||||||
|
<span><strong>${r.display_name.split(',')[0]}</strong> <span style="color:#7a8499;font-size:0.65rem;">· ${type}</span></span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
window._mapOSMResults = res;
|
||||||
|
|
||||||
|
res.forEach(r => {
|
||||||
|
const icon = mapMakeExtIcon('#fbbf24', '📍', 28);
|
||||||
|
const marker = L.marker([parseFloat(r.lat), parseFloat(r.lon)], { icon })
|
||||||
|
.addTo(mapInstance)
|
||||||
|
.bindPopup(`<div class="map-popup"><h4>📍 ${r.display_name.split(',')[0]}</h4><p style="color:#7a8499;font-size:0.7rem;">${r.display_name.split(',').slice(1,3).join(',').trim()}</p><a href="https://www.google.com/maps?q=${r.lat},${r.lon}" target="_blank" rel="noopener noreferrer" class="mp-link">📍 Maps →</a></div>`);
|
||||||
|
mapExtMarkers.push(marker);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mapExtMarkers.length > 0) {
|
||||||
|
const group = L.featureGroup(mapExtMarkers);
|
||||||
|
mapInstance.fitBounds(group.getBounds(), { padding: [50, 50], maxZoom: 13 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
results.innerHTML = '<div class="msr-item" style="color:#f87171;">OSM failed. Try Yelp or All.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── All providers at once ───────────────────────────────
|
||||||
|
async function mapSearchAll(q) {
|
||||||
|
const input = document.getElementById('map-search-input');
|
||||||
|
const results = document.getElementById('map-search-results');
|
||||||
|
q = (q || input.value || '').trim();
|
||||||
|
if (!q) return;
|
||||||
|
|
||||||
|
results.innerHTML = '<div class="msr-item" style="color:#00d4ff;">Searching all sources…</div>';
|
||||||
|
results.classList.add('show');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [osmRes, yelpData] = await Promise.all([
|
||||||
|
fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(q + ', Los Cabos, Mexico')}&limit=5&viewbox=${CABOS_BBOX}&bounded=1`, { headers: { 'Accept': 'application/json' } }).then(r => r.json()).catch(() => []),
|
||||||
|
fetch(`/api/yelp?term=${encodeURIComponent(q)}&location=Los+Cabos+Mexico`).then(r => r.json()).catch(() => ({}))
|
||||||
|
]);
|
||||||
|
|
||||||
|
mapClearExtMarkers();
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Yelp results
|
||||||
|
const businesses = yelpData.businesses || [];
|
||||||
|
if (businesses.length > 0) {
|
||||||
|
window._mapYelpResults = businesses;
|
||||||
|
html += '<div class="msr-item" style="color:#7a8499;font-size:0.65rem;padding:4px 10px;">🍴 Yelp</div>';
|
||||||
|
html += businesses.slice(0, 5).map((b, i) => {
|
||||||
|
const rating = b.rating ? `⭐ ${b.rating}` : '';
|
||||||
|
return `<div class="msr-item" onclick="mapShowExtResult(${i})">🍴 ${b.name} <span style="color:#7a8499;font-size:0.65rem;">${rating}</span></div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
businesses.forEach(b => {
|
||||||
|
if (!b.coordinates?.latitude || !b.coordinates?.longitude) return;
|
||||||
|
const icon = mapMakeExtIcon('#ff6b35', '🍴', 30);
|
||||||
|
const marker = L.marker([b.coordinates.latitude, b.coordinates.longitude], { icon })
|
||||||
|
.addTo(mapInstance)
|
||||||
|
.bindPopup(mapMakeExtPopup(b), { className: 'map-popup-wrapper' });
|
||||||
|
mapExtMarkers.push(marker);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSM results
|
||||||
|
if (osmRes && osmRes.length > 0) {
|
||||||
|
window._mapOSMResults = osmRes;
|
||||||
|
html += '<div class="msr-item" style="color:#7a8499;font-size:0.65rem;padding:4px 10px;">📍 OSM</div>';
|
||||||
|
html += osmRes.slice(0, 5).map((r, i) => {
|
||||||
|
return `<div class="msr-item" onclick="mapShowOSMResult(${i},${r.lat},${r.lon},'${r.display_name.replace(/'/g,"\\'")}')">📍 ${r.display_name.split(',')[0]}</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
osmRes.forEach(r => {
|
||||||
|
const icon = mapMakeExtIcon('#fbbf24', '📍', 28);
|
||||||
|
const marker = L.marker([parseFloat(r.lat), parseFloat(r.lon)], { icon })
|
||||||
|
.addTo(mapInstance)
|
||||||
|
.bindPopup(`<div class="map-popup"><h4>📍 ${r.display_name.split(',')[0]}</h4><a href="https://www.google.com/maps?q=${r.lat},${r.lon}" target="_blank" rel="noopener noreferrer" class="mp-link">📍 Maps →</a></div>`);
|
||||||
|
mapExtMarkers.push(marker);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!html) html = '<div class="msr-item" style="color:#7a8499;">No results found anywhere.</div>';
|
||||||
|
results.innerHTML = html;
|
||||||
|
|
||||||
|
if (mapExtMarkers.length > 0) {
|
||||||
|
const group = L.featureGroup(mapExtMarkers);
|
||||||
|
mapInstance.fitBounds(group.getBounds(), { padding: [50, 50], maxZoom: 13 });
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
results.innerHTML = '<div class="msr-item" style="color:#f87171;">Search failed. Try again.</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapShowExtResult(index) {
|
function mapShowExtResult(index) {
|
||||||
const businesses = window._mapYelpResults || [];
|
const businesses = window._mapYelpResults || [];
|
||||||
const b = businesses[index];
|
const b = businesses[index];
|
||||||
if (!b || !b.coordinates) return;
|
if (!b?.coordinates) return;
|
||||||
|
|
||||||
document.getElementById('map-search-results').classList.remove('show');
|
document.getElementById('map-search-results').classList.remove('show');
|
||||||
document.getElementById('map-search-input').value = b.name;
|
document.getElementById('map-search-input').value = b.name;
|
||||||
mapInstance.flyTo([b.coordinates.latitude, b.coordinates.longitude], 15, { duration: 1.0 });
|
mapInstance.flyTo([b.coordinates.latitude, b.coordinates.longitude], 15, { duration: 1.0 });
|
||||||
|
if (mapExtMarkers[index]) mapExtMarkers[index].openPopup();
|
||||||
// Find and open the right marker
|
|
||||||
if (mapExtMarkers[index]) {
|
|
||||||
mapExtMarkers[index].openPopup();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Local venue search (dropdown) ────────────────────────
|
function mapShowOSMResult(index, lat, lon, name) {
|
||||||
async function mapDoSearch(q) {
|
|
||||||
const results = document.getElementById('map-search-results');
|
|
||||||
results.innerHTML = '<div class="msr-item" style="color:#7a8499;">Searching…</div>';
|
|
||||||
results.classList.add('show');
|
|
||||||
|
|
||||||
// Local in-app venue match
|
|
||||||
const localMatches = state.options.filter(o =>
|
|
||||||
o.approved && o.lat && o.lng &&
|
|
||||||
!mapHiddenCats.has(o.categoryId) &&
|
|
||||||
(o.name.toLowerCase().includes(q.toLowerCase()) ||
|
|
||||||
(o.desc && o.desc.toLowerCase().includes(q.toLowerCase())))
|
|
||||||
).slice(0, 5);
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
if (localMatches.length > 0) {
|
|
||||||
const catColors = { hotel: '#3b82f6', golf: '#22c55e', nightlife: '#a855f7', excursion: '#06b6d4', itinerary: '#fbbf24' };
|
|
||||||
html += '<div class="msr-item" style="color:#7a8499;font-size:0.65rem;padding:4px 10px;">Our venues</div>';
|
|
||||||
html += localMatches.map(opt => {
|
|
||||||
const color = opt.categoryColor || catColors[opt.categoryId] || '#888';
|
|
||||||
return `<div class="msr-item" onclick="mapFlyTo('${opt.id}')">
|
|
||||||
<span class="msr-cat" style="background:${color}20;color:${color}">${opt.categoryId}</span>
|
|
||||||
${opt.name} <span style="color:#7a8499;font-size:0.65rem;">(${opt.votes.length} votes)</span>
|
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// OSM search
|
|
||||||
try {
|
|
||||||
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(q + ', Los Cabos, Mexico')}&limit=5&viewbox=${CABOS_BBOX}&bounded=1`, { headers: { 'Accept': 'application/json' } }).then(r => r.json()).catch(() => []);
|
|
||||||
if (res && res.length > 0) {
|
|
||||||
html += '<div class="msr-item" style="color:#7a8499;font-size:0.65rem;padding:4px 10px;">Web results</div>';
|
|
||||||
res.forEach(r => {
|
|
||||||
html += `<div class="msr-item" onclick="mapFlyToOSM(${r.lat},${r.lon},'${r.display_name.replace(/'/g,"\\'")}')">
|
|
||||||
📍 ${r.display_name.split(',')[0]}
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch(e) {}
|
|
||||||
|
|
||||||
if (!html) html = '<div class="msr-item" style="color:#7a8499;">No results — try the Yelp button for full search</div>';
|
|
||||||
results.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapFlyTo(optionId) {
|
|
||||||
const opt = state.options.find(o => o.id === optionId);
|
|
||||||
if (!opt || !mapMarkers[optionId]) return;
|
|
||||||
document.getElementById('map-search-results').classList.remove('show');
|
|
||||||
document.getElementById('map-search-input').value = opt.name;
|
|
||||||
mapMarkers[optionId].openPopup();
|
|
||||||
mapInstance.flyTo([opt.lat, opt.lng], 15, { duration: 1.0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapFlyToOSM(lat, lon, name) {
|
|
||||||
document.getElementById('map-search-results').classList.remove('show');
|
document.getElementById('map-search-results').classList.remove('show');
|
||||||
document.getElementById('map-search-input').value = name.split(',')[0];
|
document.getElementById('map-search-input').value = name.split(',')[0];
|
||||||
mapInstance.flyTo([lat, lon], 15, { duration: 1.0 });
|
mapInstance.flyTo([parseFloat(lat), parseFloat(lon)], 15, { duration: 1.0 });
|
||||||
const osmMarker = L.marker([lat, lon], {
|
if (mapExtMarkers[index]) mapExtMarkers[index].openPopup();
|
||||||
icon: mapMakeExtIcon('#fbbf24', '📍', 28)
|
|
||||||
}).addTo(mapInstance).bindPopup(`<div class="map-popup"><h4 style="color:#fbbf24;">📍 ${name.split(',')[0]}</h4><p style="color:#7a8499;font-size:0.7rem;">${name.split(',').slice(1,3).join(',').trim()}</p><a href="https://www.google.com/maps?q=${lat},${lon}" target="_blank" rel="noopener noreferrer" class="mp-link">📍 Open in Google Maps →</a></div>`).openPopup();
|
|
||||||
// Track as external so it doesn't clutter venue markers
|
|
||||||
mapExtMarkers.push(osmMarker);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Unified search dispatcher ──────────────────────────
|
||||||
|
function mapDoSearch() {
|
||||||
|
const q = document.getElementById('map-search-input').value.trim();
|
||||||
|
if (!q) return;
|
||||||
|
if (currentProvider === 'yelp') mapYelpSearch(q);
|
||||||
|
else if (currentProvider === 'osm') mapOSMSearch(q);
|
||||||
|
else mapSearchAll(q);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Enter key to use the dispatcher
|
||||||
|
document.getElementById('map-search-input').onkeydown = function(e) {
|
||||||
|
if (e.key === 'Enter') { mapDoSearch(); document.getElementById('map-search-results').classList.remove('show'); }
|
||||||
|
};
|
||||||
|
|
||||||
// ── WebSocket vote update — refresh map markers ──────────────
|
// ── WebSocket vote update — refresh map markers ──────────────
|
||||||
|
|
||||||
// ── Kick off ──────────────────────────────────────────────
|
// ── Kick off ──────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user