diff --git a/public/index.html b/public/index.html
index a99ec28..ddeffb7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -109,54 +109,91 @@
}
/* Yelp search button */
+ /* Multi-provider search bar */
#map-search-wrap {
display: flex;
align-items: center;
- gap: 6px;
+ gap: 0;
pointer-events: all;
background: rgba(19,22,31,0.95);
border: 1px solid #252a38;
border-radius: 8px;
- display: flex;
- align-items: center;
padding: 0;
- max-width: 360px;
+ max-width: 480px;
backdrop-filter: blur(8px);
overflow: hidden;
+ flex-shrink: 0;
}
- #map-search-wrap:focus-within {
- border-color: var(--accent);
- }
+ #map-search-wrap:focus-within { border-color: var(--accent); }
#map-search-input {
flex: 1;
background: transparent;
border: none;
color: #e0e6f0;
font-size: 0.82rem;
- outline: none;
padding: 8px 10px;
+ outline: none;
+ min-width: 120px;
}
#map-search-input::placeholder { color: #7a8499; }
- .yelp-search-btn {
+ .provider-tabs {
display: flex;
align-items: center;
- gap: 4px;
- padding: 6px 10px;
- background: rgba(255,107,53,0.15);
- border: none;
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-weight: 700;
cursor: pointer;
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); }
- .yelp-search-btn.searching {
- opacity: 0.7;
- cursor: wait;
+ .provider-tab:last-child { border-right: none; }
+ .provider-tab:hover { color: #ccc; background: rgba(255,255,255,0.04); }
+ .provider-tab.active-yelp { color: #ff6b35; background: rgba(255,107,53,0.12); }
+ .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 */
.map-overlay {
@@ -972,16 +1009,18 @@
-
+
-
π
-
-
-
+
π
+
+
+
+
+
+
+
+
Show
@@ -1000,7 +1039,9 @@
-
+
+
+
@@ -1441,12 +1482,12 @@
const q = searchInput.value.trim();
if (q.length < 2) { searchResults.classList.remove('show'); return; }
clearTimeout(searchTimeout);
- searchTimeout = setTimeout(() => mapDoSearch(q), 400);
+ searchTimeout = setTimeout(() => mapDoSearch(), 400);
});
searchInput.addEventListener('keydown', e => {
if (e.key === 'Enter') {
const q = searchInput.value.trim();
- if (q) { mapYelpSearch(q); searchResults.classList.remove('show'); }
+ if (q) { mapDoSearch(); searchResults.classList.remove('show'); }
}
});
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() {
mapExtMarkers.forEach(m => mapInstance.removeLayer(m));
mapExtMarkers = [];
@@ -1599,61 +1664,48 @@
async function mapYelpSearch(q) {
const input = document.getElementById('map-search-input');
- const btn = document.getElementById('yelp-search-btn');
const results = document.getElementById('map-search-results');
q = (q || input.value || '').trim();
if (!q) return;
- btn.classList.add('searching');
- btn.textContent = '...';
- results.innerHTML = '
Searching Yelp...
';
+ results.innerHTML = '
Searching Yelpβ¦
';
results.classList.add('show');
try {
- // Call our backend proxy (keeps API key server-side)
const res = await fetch(`/api/yelp?term=${encodeURIComponent(q)}&location=Los+Cabos+Mexico`);
const data = await res.json();
-
if (data.error) throw new Error(data.error);
mapClearExtMarkers();
if (!data.businesses || data.businesses.length === 0) {
- results.innerHTML = '
No Yelp results found. Try a different search.
';
+ results.innerHTML = '
No Yelp results. Try OSM or All.
';
} else {
- // Build search results dropdown
results.innerHTML = data.businesses.slice(0, 8).map((b, i) => {
const cat = b.categories?.[0]?.title || 'Place';
const rating = b.rating ? `β ${b.rating}` : '';
const price = b.price || '';
return `
π΄
-
- ${b.name}
- Β· ${cat} Β· ${rating} ${price}
-
+ ${b.name} Β· ${cat} Β· ${rating} ${price}
`;
}).join('');
- // Store for later reference
window._mapYelpResults = data.businesses;
- // Show all results as dashed markers on map
data.businesses.forEach((b, i) => {
- if (!b.coordinates || !b.coordinates.latitude || !b.coordinates.longitude) return;
- if (!b.location?.city?.toLowerCase().includes('cabo') &&
- !b.location?.state?.includes('BS') &&
- !b.location?.state?.includes('Baja')) return; // Only show results near Los Cabos
+ if (!b.coordinates?.latitude || !b.coordinates?.longitude) return;
+ const city = b.location?.city?.toLowerCase() || '';
+ const state = b.location?.state || '';
+ if (!city.includes('cabo') && !state.includes('BS') && !state.includes('Baja')) return;
- const emoji = 'π΄';
- const icon = mapMakeExtIcon('#ff6b35', emoji, 30);
+ 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);
});
- // Fit map to show all results
if (mapExtMarkers.length > 0) {
const group = L.featureGroup(mapExtMarkers);
mapInstance.fitBounds(group.getBounds(), { padding: [50, 50], maxZoom: 13 });
@@ -1661,92 +1713,159 @@
}
} catch(err) {
console.error('Yelp search error:', err);
- results.innerHTML = '
Yelp search failed. Try again.
';
- } finally {
- btn.classList.remove('searching');
- btn.innerHTML = `
Yelp`;
+ results.innerHTML = '
Yelp failed. Try OSM or All.
';
+ }
+ }
+
+ // ββ 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 = '
Searching OSMβ¦
';
+ 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 = '
No OSM results. Try Yelp or All.
';
+ } else {
+ results.innerHTML = res.map((r, i) => {
+ const type = (r.type || 'place');
+ const addr = r.display_name.split(',').slice(1, 4).join(',').trim();
+ return `
+ π
+ ${r.display_name.split(',')[0]} Β· ${type}
+
`;
+ }).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(``);
+ 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 = '
OSM failed. Try Yelp or All.
';
+ }
+ }
+
+ // ββ 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 = '
Searching all sourcesβ¦
';
+ 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 += '
π΄ Yelp
';
+ html += businesses.slice(0, 5).map((b, i) => {
+ const rating = b.rating ? `β ${b.rating}` : '';
+ return `
π΄ ${b.name} ${rating}
`;
+ }).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 += '
π OSM
';
+ html += osmRes.slice(0, 5).map((r, i) => {
+ return `
π ${r.display_name.split(',')[0]}
`;
+ }).join('');
+
+ osmRes.forEach(r => {
+ const icon = mapMakeExtIcon('#fbbf24', 'π', 28);
+ const marker = L.marker([parseFloat(r.lat), parseFloat(r.lon)], { icon })
+ .addTo(mapInstance)
+ .bindPopup(``);
+ mapExtMarkers.push(marker);
+ });
+ }
+
+ if (!html) html = '
No results found anywhere.
';
+ 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 = '
Search failed. Try again.
';
}
}
function mapShowExtResult(index) {
const businesses = window._mapYelpResults || [];
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-input').value = b.name;
mapInstance.flyTo([b.coordinates.latitude, b.coordinates.longitude], 15, { duration: 1.0 });
-
- // Find and open the right marker
- if (mapExtMarkers[index]) {
- mapExtMarkers[index].openPopup();
- }
+ if (mapExtMarkers[index]) mapExtMarkers[index].openPopup();
}
- // ββ Local venue search (dropdown) ββββββββββββββββββββββββ
- async function mapDoSearch(q) {
- const results = document.getElementById('map-search-results');
- results.innerHTML = '
Searchingβ¦
';
- 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 += '
Our venues
';
- html += localMatches.map(opt => {
- const color = opt.categoryColor || catColors[opt.categoryId] || '#888';
- return `
- ${opt.categoryId}
- ${opt.name} (${opt.votes.length} votes)
-
`;
- }).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 += '
Web results
';
- res.forEach(r => {
- html += `
- π ${r.display_name.split(',')[0]}
-
`;
- });
- }
- } catch(e) {}
-
- if (!html) html = '
No results β try the Yelp button for full search
';
- 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) {
+ function mapShowOSMResult(index, lat, lon, name) {
document.getElementById('map-search-results').classList.remove('show');
document.getElementById('map-search-input').value = name.split(',')[0];
- mapInstance.flyTo([lat, lon], 15, { duration: 1.0 });
- const osmMarker = L.marker([lat, lon], {
- icon: mapMakeExtIcon('#fbbf24', 'π', 28)
- }).addTo(mapInstance).bindPopup(``).openPopup();
- // Track as external so it doesn't clutter venue markers
- mapExtMarkers.push(osmMarker);
+ mapInstance.flyTo([parseFloat(lat), parseFloat(lon)], 15, { duration: 1.0 });
+ if (mapExtMarkers[index]) mapExtMarkers[index].openPopup();
}
+ // ββ 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 ββββββββββββββ
// ββ Kick off ββββββββββββββββββββββββββββββββββββββββββββββ