diff --git a/public/index.html b/public/index.html
index 4b4c983..a99ec28 100644
--- a/public/index.html
+++ b/public/index.html
@@ -68,9 +68,132 @@
backdrop-filter: blur(8px);
transition: all 0.15s;
white-space: nowrap;
+ user-select: none;
+ }
+ .map-cat-btn.active {
+ border-color: var(--accent);
+ color: var(--accent);
+ background: rgba(0,212,255,0.12);
+ }
+ .map-cat-btn.hidden-cat {
+ opacity: 0.35;
+ border-color: #1a1e2a;
}
- .map-cat-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(0,212,255,0.1); }
.map-cat-btn:hover { border-color: #7a8499; color: #e0e6f0; }
+ .map-cat-btn.hidden-cat:hover { opacity: 0.7; }
+
+ /* External / Yelp marker icon */
+ .map-ext-icon {
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ border: 2.5px dashed #ff6b35;
+ background: rgba(255,107,53,0.15);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.4);
+ }
+ .map-ext-icon-new {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ border: 2px dashed #fbbf24;
+ background: rgba(251,191,36,0.15);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 11px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.4);
+ }
+
+ /* Yelp search button */
+ #map-search-wrap {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ 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;
+ backdrop-filter: blur(8px);
+ overflow: hidden;
+ }
+ #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;
+ }
+ #map-search-input::placeholder { color: #7a8499; }
+ .yelp-search-btn {
+ 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;
+ font-size: 0.68rem;
+ font-weight: 700;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background 0.15s;
+ }
+ .yelp-search-btn:hover { background: rgba(255,107,53,0.25); }
+ .yelp-search-btn.searching {
+ opacity: 0.7;
+ cursor: wait;
+ }
+
+ /* Filter bar layout */
+ .map-overlay {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ right: 10px;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ pointer-events: none;
+ }
+ .map-filter-row {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-wrap: wrap;
+ pointer-events: all;
+ }
+ .map-filter-row .row-label {
+ color: #7a8499;
+ font-size: 0.62rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-right: 2px;
+ }
+ #map-search-wrap,
+ .map-filter-row {
+ background: rgba(19,22,31,0.93);
+ border: 1px solid #252a38;
+ border-radius: 8px;
+ padding: 6px 8px;
+ backdrop-filter: blur(8px);
+ }
+
.map-legend {
position: absolute;
bottom: 24px;
@@ -849,18 +972,25 @@
-
-
π
+
+
-
-
-
-
-
-
-
+
+
+ Show
+
+
+
+
+
+
@@ -1287,12 +1418,14 @@
let mapInstance = null;
let mapInitialized = false;
let mapMarkers = {};
- let mapActiveCategory = null;
+ let mapHiddenCats = new Set(); // categories currently hidden
+ let mapExtMarkers = []; // temporary external / Yelp markers
const CABOS_BBOX = '-109.85,22.85,-109.55,23.25';
+ const CABOS_CENTER = { lat: 23.065, lng: -109.698 };
function initMap() {
mapInitialized = true;
- mapInstance = L.map('cabo-map', { zoomControl: true }).setView([23.065, -109.698], 12);
+ mapInstance = L.map('cabo-map', { zoomControl: true }).setView([CABOS_CENTER.lat, CABOS_CENTER.lng], 12);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap, © CARTO',
maxZoom: 18
@@ -1313,11 +1446,11 @@
searchInput.addEventListener('keydown', e => {
if (e.key === 'Enter') {
const q = searchInput.value.trim();
- if (q) { mapDoOSMSearch(q); searchResults.classList.remove('show'); }
+ if (q) { mapYelpSearch(q); searchResults.classList.remove('show'); }
}
});
document.addEventListener('click', e => {
- if (!e.target.closest('.map-search-wrap')) searchResults.classList.remove('show');
+ if (!e.target.closest('#map-search-wrap')) searchResults.classList.remove('show');
});
mapRefreshMarkers();
@@ -1332,10 +1465,19 @@
});
}
+ // Dashed-border icon for external / Yelp results
+ function mapMakeExtIcon(borderColor, emoji, size = 30) {
+ return L.divIcon({
+ html: `
${emoji}
`,
+ className: '',
+ iconSize: [size, size],
+ iconAnchor: [size/2, size/2]
+ });
+ }
+
function mapMakePopup(opt) {
const catColors = { hotel: '#3b82f6', golf: '#22c55e', nightlife: '#a855f7', excursion: '#06b6d4', itinerary: '#fbbf24' };
const catEmoji = { hotel: 'π¨', golf: 'β³', nightlife: 'π§', excursion: 'π€', itinerary: 'πΊοΈ' };
- const catNames = { hotel: 'Hotel', golf: 'Golf', nightlife: 'Nightlife', excursion: 'Excursion', itinerary: 'Itinerary' };
const color = opt.categoryColor || catColors[opt.categoryId] || '#888';
const emoji = catEmoji[opt.categoryId] || 'π';
const hasVoted = state.voterName && opt.votes.some(v => v.name === state.voterName);
@@ -1355,24 +1497,44 @@
`;
}
+ function mapMakeExtPopup(item) {
+ const rating = item.rating ? `β ${item.rating}` : '';
+ const price = item.price ? `
${item.price}` : '';
+ const image = item.image_url ? `

` : '';
+ const yelpUrl = item.url ? `
π΄ View on Yelp β` : '';
+ const gmapsUrl = item.coordinates ? `
π Directions β` : '';
+ return ``;
+ }
+
function toggleVoteFromMap(optionId) {
if (activeTab !== 'map') setTab('map');
toggleVote(optionId);
}
+ // ββ Render venue markers (curated app data) βββββββββββββββ
function mapRefreshMarkers() {
if (!mapInstance) return;
- // Remove existing markers
+
+ // Remove existing venue markers
Object.values(mapMarkers).forEach(m => mapInstance.removeLayer(m));
mapMarkers = {};
- const filtered = mapActiveCategory
- ? state.options.filter(o => o.approved && o.categoryId === mapActiveCategory && o.lat && o.lng)
- : state.options.filter(o => o.approved && o.lat && o.lng);
+ // Show all approved venues whose category is NOT hidden
+ const visible = state.options.filter(o =>
+ o.approved && o.lat && o.lng && !mapHiddenCats.has(o.categoryId)
+ );
- filtered.forEach(opt => {
+ const catEmoji = { hotel: 'π¨', golf: 'β³', nightlife: 'π§', excursion: 'π€', itinerary: 'πΊοΈ' };
+
+ visible.forEach(opt => {
const color = opt.categoryColor || '#888';
- const catEmoji = { hotel: 'π¨', golf: 'β³', nightlife: 'π§', excursion: 'π€', itinerary: 'πΊοΈ' };
const emoji = catEmoji[opt.categoryId] || 'π';
const icon = mapMakeIcon(color, emoji, 34);
const marker = L.marker([opt.lat, opt.lng], { icon })
@@ -1381,25 +1543,147 @@
mapMarkers[opt.id] = marker;
});
- // Fit bounds if we have markers
- const markerList = Object.values(mapMarkers);
- if (markerList.length > 0) {
- const group = L.featureGroup(markerList);
- mapInstance.fitBounds(group.getBounds(), { padding: [40, 40] });
+ // Fit bounds to venue markers only (not external ones)
+ const venueMarkers = Object.values(mapMarkers);
+ if (venueMarkers.length > 0) {
+ const group = L.featureGroup(venueMarkers);
+ mapInstance.fitBounds(group.getBounds(), { padding: [40, 40], maxZoom: 14 });
}
}
- function mapFilterCat(catId) {
- mapActiveCategory = catId;
- document.querySelectorAll('.map-cat-btn').forEach(b => b.classList.remove('active'));
- if (catId === null) {
- document.querySelector('.map-cat-btn[onclick="mapFilterCat(null)"]').classList.add('active');
+ // ββ Category filter toggles βββββββββββββββββββββββββββββββ
+ const ALL_CATS = ['hotel', 'golf', 'nightlife', 'excursion', 'itinerary'];
+
+ function mapToggleCat(catId) {
+ if (mapHiddenCats.has(catId)) {
+ mapHiddenCats.delete(catId);
} else {
- document.querySelector(`.map-cat-btn[onclick="mapFilterCat('${catId}')"]`).classList.add('active');
+ mapHiddenCats.add(catId);
}
+ mapSyncCatButtons();
mapRefreshMarkers();
}
+ function mapClearAllCats() {
+ ALL_CATS.forEach(c => mapHiddenCats.add(c));
+ mapSyncCatButtons();
+ mapRefreshMarkers();
+ }
+
+ function mapRestoreAllCats() {
+ mapHiddenCats.clear();
+ mapSyncCatButtons();
+ mapRefreshMarkers();
+ }
+
+ function mapSyncCatButtons() {
+ ALL_CATS.forEach(catId => {
+ const btn = document.getElementById('cat-btn-' + catId);
+ if (btn) {
+ if (mapHiddenCats.has(catId)) {
+ btn.classList.add('hidden-cat');
+ btn.classList.remove('active');
+ } else {
+ btn.classList.remove('hidden-cat');
+ btn.classList.add('active');
+ }
+ }
+ });
+ }
+
+ // ββ External / Yelp search ββββββββββββββββββββββββββββββββ
+ function mapClearExtMarkers() {
+ mapExtMarkers.forEach(m => mapInstance.removeLayer(m));
+ mapExtMarkers = [];
+ }
+
+ 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.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.
';
+ } 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}
+
+
`;
+ }).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
+
+ const emoji = 'π΄';
+ const icon = mapMakeExtIcon('#ff6b35', emoji, 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 });
+ }
+ }
+ } catch(err) {
+ console.error('Yelp search error:', err);
+ results.innerHTML = '
Yelp search failed. Try again.
';
+ } finally {
+ btn.classList.remove('searching');
+ btn.innerHTML = `
Yelp`;
+ }
+ }
+
+ function mapShowExtResult(index) {
+ const businesses = window._mapYelpResults || [];
+ const b = businesses[index];
+ if (!b || !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();
+ }
+ }
+
+ // ββ Local venue search (dropdown) ββββββββββββββββββββββββ
async function mapDoSearch(q) {
const results = document.getElementById('map-search-results');
results.innerHTML = '
Searchingβ¦
';
@@ -1408,6 +1692,7 @@
// 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);
@@ -1415,6 +1700,7 @@
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 `
@@ -1428,7 +1714,7 @@
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) {
- if (html) html += '
OSM results
';
+ html += '
Web results
';
res.forEach(r => {
html += `
π ${r.display_name.split(',')[0]}
@@ -1437,7 +1723,7 @@
}
} catch(e) {}
- if (!html) html = '
No results found
';
+ if (!html) html = '
No results β try the Yelp button for full search
';
results.innerHTML = html;
}
@@ -1454,24 +1740,14 @@
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 });
- // Add temporary OSM marker
const osmMarker = L.marker([lat, lon], {
- icon: mapMakeIcon('#fbbf24', 'π', 32)
- }).addTo(mapInstance).bindPopup(``).openPopup();
- setTimeout(() => mapInstance.removeLayer(osmMarker), 8000);
- }
-
- function mapDoOSMSearch(q) {
- mapFlyToOSM = (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 });
- };
- mapDoSearch(q);
+ icon: mapMakeExtIcon('#fbbf24', 'π', 28)
+ }).addTo(mapInstance).bindPopup(``).openPopup();
+ // Track as external so it doesn't clutter venue markers
+ mapExtMarkers.push(osmMarker);
}
// ββ WebSocket vote update β refresh map markers ββββββββββββββ
- const _originalWsHandler = null; // will be overridden in wsMessage handler
// ββ Kick off ββββββββββββββββββββββββββββββββββββββββββββββ
init();