Make search look nicer

This commit is contained in:
Eugene Burmakin 2025-09-02 21:21:22 +02:00
parent 5eb3eb0024
commit 4f402a0c2a
12 changed files with 446 additions and 198 deletions

View file

@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
# [UNRELEASED]
The Search release
In this release we're introducing a new search feature that allows users to search for places and see when they visited them. On the map page, click on Search icon, enter a place name (e.g. "Alexanderplatz"), wait for suggestions to load, and click on the suggestion you want to search for. You then will see a list of years you visited that place. Click on the year to unfold list of visits for that year. Then click on the visit you want to see on the map and you will be moved to that visit on the map. From the opened visit popup you can create a new visit to save it in the database.
Important: This feature relies on reverse geocoding. Without reverse geocoding, the search feature will not work.
## Added
- User can now search for places and see when they visited them.
## Fixed
- Default value for `points_count` attribute is now set to 0 in the User model.

File diff suppressed because one or more lines are too long

View file

@ -10,6 +10,9 @@ class LocationSearch {
this.suggestionsVisible = false;
this.currentSuggestionIndex = -1;
// Make instance globally accessible for popup buttons
window.locationSearchInstance = this;
this.initializeSearchBar();
}
@ -64,12 +67,13 @@ class LocationSearch {
createInlineSearchBar() {
// Create inline search bar that appears next to the search button
const searchBar = document.createElement('div');
searchBar.className = 'location-search-bar absolute bg-white border border-gray-300 rounded-lg shadow-lg';
searchBar.id = 'location-search-bar';
searchBar.className = 'location-search-bar absolute bg-white border border-gray-300 rounded-lg shadow-lg hidden';
searchBar.id = 'location-search-container'; // Use container ID for test compatibility
searchBar.style.width = '400px'; // Increased width for better usability
searchBar.style.maxHeight = '600px'; // Set max height for the entire search bar
searchBar.style.padding = '12px'; // Increased padding
searchBar.style.display = 'none'; // Start hidden with inline style instead of class
searchBar.style.zIndex = '9999'; // Very high z-index to ensure visibility
searchBar.style.overflow = 'visible'; // Allow content to overflow but results area will scroll
searchBar.innerHTML = `
<div class="flex items-center space-x-2">
@ -94,15 +98,15 @@ class LocationSearch {
</div>
<!-- Suggestions dropdown -->
<div id="location-search-suggestions-panel" class="hidden mt-2">
<div id="location-search-suggestions-panel" class="hidden mt-2 border-t border-gray-200">
<div class="bg-gray-50 px-3 py-2 border-b text-xs font-medium text-gray-700">Suggestions</div>
<div id="location-search-suggestions" class="max-h-48 overflow-y-auto"></div>
<div id="location-search-suggestions" class="max-h-48 overflow-y-auto border border-gray-200 rounded-b"></div>
</div>
<!-- Results dropdown -->
<div id="location-search-results-panel" class="hidden mt-2">
<div id="location-search-results-panel" class="hidden mt-2 border-t border-gray-200">
<div class="bg-gray-50 px-3 py-2 border-b text-xs font-medium text-gray-700">Results</div>
<div id="location-search-results" class="max-h-64 overflow-y-auto"></div>
<div id="location-search-results" class="border border-gray-200 rounded-b"></div>
</div>
`;
@ -119,11 +123,68 @@ class LocationSearch {
this.resultsContainer = document.getElementById('location-search-results');
this.resultsPanel = document.getElementById('location-search-results-panel');
// Set scrolling properties immediately for results container with !important
this.resultsContainer.style.setProperty('max-height', '400px', 'important');
this.resultsContainer.style.setProperty('overflow-y', 'scroll', 'important');
this.resultsContainer.style.setProperty('overflow-x', 'hidden', 'important');
this.resultsContainer.style.setProperty('min-height', '0', 'important');
this.resultsContainer.style.setProperty('display', 'block', 'important');
// Set scrolling properties for suggestions container with !important
this.suggestionsContainer.style.setProperty('max-height', '200px', 'important');
this.suggestionsContainer.style.setProperty('overflow-y', 'scroll', 'important');
this.suggestionsContainer.style.setProperty('overflow-x', 'hidden', 'important');
this.suggestionsContainer.style.setProperty('min-height', '0', 'important');
this.suggestionsContainer.style.setProperty('display', 'block', 'important');
console.log('LocationSearch: Set scrolling properties on containers');
// Prevent map scroll events when scrolling inside the search containers
this.preventMapScrollOnContainers();
// No clear button or default panel in inline mode
this.clearButton = null;
this.defaultPanel = null;
}
preventMapScrollOnContainers() {
// Prevent scroll events from bubbling to the map when scrolling inside search containers
const containers = [this.resultsContainer, this.suggestionsContainer, this.searchBar];
containers.forEach(container => {
if (container) {
// Prevent wheel events (scroll) from reaching the map
container.addEventListener('wheel', (e) => {
e.stopPropagation();
}, { passive: false });
// Prevent touch scroll events from reaching the map
container.addEventListener('touchstart', (e) => {
e.stopPropagation();
}, { passive: false });
container.addEventListener('touchmove', (e) => {
e.stopPropagation();
}, { passive: false });
container.addEventListener('touchend', (e) => {
e.stopPropagation();
}, { passive: false });
// Also prevent mousewheel for older browsers
container.addEventListener('mousewheel', (e) => {
e.stopPropagation();
}, { passive: false });
// Prevent DOMMouseScroll for Firefox
container.addEventListener('DOMMouseScroll', (e) => {
e.stopPropagation();
}, { passive: false });
console.log('LocationSearch: Added scroll prevention to container', container.id || 'search-bar');
}
});
}
bindSearchEvents() {
// Toggle search bar visibility
@ -199,7 +260,7 @@ class LocationSearch {
// Close search bar when clicking outside (but not on map interactions)
document.addEventListener('click', (e) => {
if (this.searchVisible &&
!e.target.closest('.location-search-bar') &&
!e.target.closest('#location-search-container') &&
!e.target.closest('#location-search-toggle') &&
!e.target.closest('.leaflet-container')) { // Don't close on map interactions
this.hideSearchBar();
@ -311,16 +372,12 @@ class LocationSearch {
this.resultsContainer.innerHTML = resultsHtml;
// Add markers to map
this.addSearchMarkersToMap(data.locations);
// Bind result interaction events
this.bindResultEvents();
}
buildLocationResultHtml(location, index) {
const firstVisit = location.visits[0];
const lastVisit = location.visits[location.visits.length - 1];
const firstVisit = location.visits[location.visits.length - 1];
const lastVisit = location.visits[0];
// Group visits by year
const visitsByYear = this.groupVisitsByYear(location.visits);
@ -349,20 +406,17 @@ class LocationSearch {
<span class="year-arrow text-gray-400 transition-transform"></span>
</div>
<div class="year-visits hidden" id="year-${index}-${year}">
${yearVisits.map((visit, visitIndex) => `
${yearVisits.map((visit) => `
<div class="visit-item text-xs text-gray-700 py-2 px-4 border-b border-gray-100 hover:bg-blue-50 cursor-pointer"
data-location-index="${index}" data-visit-index="${location.visits.indexOf(visit)}">
<div class="flex justify-between items-start">
<div>
📍 ${this.formatDateTime(visit.date)}
<div class="text-xs text-gray-500 mt-1">
</div>
<div class="text-xs text-gray-500">
${visit.duration_estimate}
</div>
</div>
<div class="text-xs text-gray-400">
${visit.distance_meters}m
</div>
</div>
</div>
`).join('')}
</div>
@ -444,43 +498,6 @@ class LocationSearch {
}
}
addSearchMarkersToMap(locations) {
if (this.searchMarkersLayer) {
this.map.removeLayer(this.searchMarkersLayer);
}
this.searchMarkersLayer = L.layerGroup();
locations.forEach(location => {
const [lat, lon] = location.coordinates;
// Create custom search result marker
const marker = L.circleMarker([lat, lon], {
radius: 8,
fillColor: '#ff6b35',
color: '#ffffff',
weight: 2,
opacity: 1,
fillOpacity: 0.8
});
// Add popup with location info
const popupContent = `
<div class="text-sm">
<div class="font-semibold">${this.escapeHtml(location.place_name)}</div>
<div class="text-gray-600 mt-1">${this.escapeHtml(location.address || '')}</div>
<div class="mt-2">
<span class="text-blue-600">${location.total_visits} visit(s)</span>
</div>
</div>
`;
marker.bindPopup(popupContent);
this.searchMarkersLayer.addLayer(marker);
});
this.searchMarkersLayer.addTo(this.map);
}
focusOnLocation(location) {
const [lat, lon] = location.coordinates;
@ -529,9 +546,6 @@ class LocationSearch {
// Create a special marker for the specific visit
this.addVisitMarker(lat, lon, visit, location);
// Show visit details in a popup or notification
this.showVisitDetails(visit, location);
// DON'T hide results - keep sidebar open
// this.hideResults();
}
@ -561,9 +575,13 @@ class LocationSearch {
<div class="text-sm">${this.formatDateTime(visit.date)}</div>
<div class="text-xs text-gray-500">Duration: ${visit.duration_estimate}</div>
</div>
<div class="mt-3 pt-2 border-t border-gray-200">
<div class="mt-3 pt-2 border-t border-gray-200 flex gap-2">
<button onclick="window.locationSearchInstance?.createVisitAt?.(${lat}, ${lon}, '${this.escapeHtml(location.place_name)}', '${visit.date}', '${visit.duration_estimate}')"
class="text-xs bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700 flex-1">
Create Visit
</button>
<button onclick="this.getRootNode().host?.closePopup?.() || this.closest('.leaflet-popup').querySelector('.leaflet-popup-close-button')?.click()"
class="text-xs text-blue-600 hover:text-blue-800">
class="text-xs text-blue-600 hover:text-blue-800 px-2">
Close
</button>
</div>
@ -592,50 +610,6 @@ class LocationSearch {
this.currentVisitMarker = this.visitMarker;
}
showVisitDetails(visit, location) {
// Remove any existing notification
const existingNotification = document.querySelector('.visit-navigation-notification');
if (existingNotification) {
existingNotification.remove();
}
// Create a persistent notification showing visit details
const notification = document.createElement('div');
notification.className = 'visit-navigation-notification fixed top-4 right-4 z-40 bg-green-50 border border-green-200 rounded-lg p-4 shadow-lg max-w-sm';
notification.innerHTML = `
<div class="flex items-start">
<div class="flex-shrink-0">
<div class="w-6 h-6 bg-green-100 rounded-full flex items-center justify-center">
📍
</div>
</div>
<div class="ml-3 flex-1">
<div class="text-sm font-medium text-green-800">
Viewing visit
</div>
<div class="text-sm text-green-600 mt-1">
${this.escapeHtml(location.place_name)}
</div>
<div class="text-xs text-green-500 mt-1">
${this.formatDateTime(visit.date)} ${visit.duration_estimate}
</div>
</div>
<button class="flex-shrink-0 ml-3 text-green-400 hover:text-green-500" onclick="this.parentElement.parentElement.remove()">
</button>
</div>
`;
document.body.appendChild(notification);
// Auto-remove notification after 10 seconds (longer duration)
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 10000);
}
clearSearch() {
this.searchInput.value = '';
this.hideResults();
@ -686,6 +660,7 @@ class LocationSearch {
this.searchBar.style.position = 'absolute'; // Position relative to map container
// Show the search bar
this.searchBar.classList.remove('hidden');
this.searchBar.style.setProperty('display', 'block', 'important');
this.searchBar.style.visibility = 'visible';
this.searchBar.style.opacity = '1';
@ -720,6 +695,7 @@ class LocationSearch {
}
hideSearchBar() {
this.searchBar.classList.add('hidden');
this.searchBar.style.display = 'none';
this.searchVisible = false;
this.clearSearch();
@ -734,6 +710,8 @@ class LocationSearch {
}
clearSearchMarkers() {
// Note: No longer using search markers, but keeping method for compatibility
// Only clear visit markers if they exist
if (this.searchMarkersLayer) {
this.map.removeLayer(this.searchMarkersLayer);
this.searchMarkersLayer = null;
@ -822,7 +800,6 @@ class LocationSearch {
data-suggestion-index="${index}">
<div class="font-medium text-sm">${this.escapeHtml(suggestion.name)}</div>
<div class="text-xs text-gray-500 mt-1">${this.escapeHtml(suggestion.address || '')}</div>
<div class="text-xs text-gray-400 mt-1">${suggestion.type}</div>
</div>
`;
});
@ -948,6 +925,262 @@ class LocationSearch {
}
}
createVisitAt(lat, lon, placeName, visitDate, durationEstimate) {
console.log(`Creating visit at ${lat}, ${lon} for ${placeName} at ${visitDate} (duration: ${durationEstimate})`);
// Close the current visit popup
if (this.visitMarker) {
this.visitMarker.closePopup();
}
// Calculate start and end times from the original visit
const { startTime, endTime } = this.calculateVisitTimes(visitDate, durationEstimate);
this.showBasicVisitForm(lat, lon, placeName, startTime, endTime);
}
showBasicVisitForm(lat, lon, placeName, presetStartTime, presetEndTime) {
// Close any existing visit form popups first
const existingPopups = document.querySelectorAll('.basic-visit-form-popup');
existingPopups.forEach(popup => {
const leafletPopup = popup.closest('.leaflet-popup');
if (leafletPopup) {
const closeButton = leafletPopup.querySelector('.leaflet-popup-close-button');
if (closeButton) closeButton.click();
}
});
// Use preset times if available, otherwise use current time defaults
let startTime, endTime;
if (presetStartTime && presetEndTime) {
startTime = presetStartTime;
endTime = presetEndTime;
console.log('Using preset times:', { startTime, endTime });
} else {
console.log('No preset times provided, using defaults');
// Get current date/time for default values
const now = new Date();
const oneHourLater = new Date(now.getTime() + (60 * 60 * 1000));
// Format dates for datetime-local input
const formatDateTime = (date) => {
return date.toISOString().slice(0, 16);
};
startTime = formatDateTime(now);
endTime = formatDateTime(oneHourLater);
}
// Create form HTML
const formHTML = `
<div class="visit-form" style="min-width: 280px;">
<h3 style="margin-top: 0; margin-bottom: 15px; font-size: 16px; color: #333;">Add New Visit</h3>
<form id="basic-add-visit-form" style="display: flex; flex-direction: column; gap: 10px;">
<div>
<label for="basic-visit-name" style="display: block; margin-bottom: 5px; font-weight: bold; font-size: 14px;">Name:</label>
<input type="text" id="basic-visit-name" name="name" required value="${this.escapeHtml(placeName)}"
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px;"
placeholder="Enter visit name">
</div>
<div>
<label for="basic-visit-start" style="display: block; margin-bottom: 5px; font-weight: bold; font-size: 14px;">Start Time:</label>
<input type="datetime-local" id="basic-visit-start" name="started_at" required value="${startTime}"
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px;">
</div>
<div>
<label for="basic-visit-end" style="display: block; margin-bottom: 5px; font-weight: bold; font-size: 14px;">End Time:</label>
<input type="datetime-local" id="basic-visit-end" name="ended_at" required value="${endTime}"
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px;">
</div>
<input type="hidden" name="latitude" value="${lat}">
<input type="hidden" name="longitude" value="${lon}">
<div style="display: flex; gap: 10px; margin-top: 15px;">
<button type="submit" style="flex: 1; background: #28a745; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer; font-weight: bold;">
Create Visit
</button>
<button type="button" id="basic-cancel-visit" style="flex: 1; background: #dc3545; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer; font-weight: bold;">
Cancel
</button>
</div>
</form>
</div>
`;
// Create popup at the location
const basicVisitPopup = L.popup({
closeOnClick: false,
autoClose: false,
maxWidth: 300,
className: 'basic-visit-form-popup'
})
.setLatLng([lat, lon])
.setContent(formHTML)
.openOn(this.map);
// Add event listeners after the popup is added to DOM
setTimeout(() => {
const form = document.getElementById('basic-add-visit-form');
const cancelButton = document.getElementById('basic-cancel-visit');
const nameInput = document.getElementById('basic-visit-name');
if (form) {
form.addEventListener('submit', (e) => this.handleBasicFormSubmit(e, basicVisitPopup));
}
if (cancelButton) {
cancelButton.addEventListener('click', () => {
this.map.closePopup(basicVisitPopup);
});
}
// Focus and select the name input
if (nameInput) {
nameInput.focus();
nameInput.select();
}
}, 100);
}
async handleBasicFormSubmit(event, popup) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
// Get form values
const visitData = {
visit: {
name: formData.get('name'),
started_at: formData.get('started_at'),
ended_at: formData.get('ended_at'),
latitude: formData.get('latitude'),
longitude: formData.get('longitude')
}
};
// Validate that end time is after start time
const startTime = new Date(visitData.visit.started_at);
const endTime = new Date(visitData.visit.ended_at);
if (endTime <= startTime) {
alert('End time must be after start time');
return;
}
// Disable form while submitting
const submitButton = form.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.disabled = true;
submitButton.textContent = 'Creating...';
try {
const response = await fetch(`/api/v1/visits`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify(visitData)
});
const data = await response.json();
if (response.ok) {
alert(`Visit "${visitData.visit.name}" created successfully!`);
this.map.closePopup(popup);
// Try to refresh visits layer if available
this.refreshVisitsIfAvailable();
} else {
const errorMessage = data.error || data.message || 'Failed to create visit';
alert(errorMessage);
}
} catch (error) {
console.error('Error creating visit:', error);
alert('Network error: Failed to create visit');
} finally {
// Re-enable form
submitButton.disabled = false;
submitButton.textContent = originalText;
}
}
refreshVisitsIfAvailable() {
// Try to refresh visits layer if available
const mapsController = document.querySelector('[data-controller*="maps"]');
if (mapsController) {
const stimulusApp = window.Stimulus || window.stimulus;
if (stimulusApp) {
const controller = stimulusApp.getControllerForElementAndIdentifier(mapsController, 'maps');
if (controller && controller.visitsManager && controller.visitsManager.fetchAndDisplayVisits) {
console.log('Refreshing visits layer after creating visit');
controller.visitsManager.fetchAndDisplayVisits();
}
}
}
}
calculateVisitTimes(visitDate, durationEstimate) {
if (!visitDate) {
return { startTime: null, endTime: null };
}
try {
// Parse the visit date (e.g., "2022-12-27T18:01:00.000Z")
const visitDateTime = new Date(visitDate);
// Parse duration estimate (e.g., "~15m", "~1h 44m", "~2h 30m")
let durationMinutes = 15; // Default to 15 minutes if parsing fails
if (durationEstimate) {
const durationStr = durationEstimate.replace('~', '').trim();
// Match patterns like "15m", "1h 44m", "2h", etc.
const hoursMatch = durationStr.match(/(\d+)h/);
const minutesMatch = durationStr.match(/(\d+)m/);
let hours = 0;
let minutes = 0;
if (hoursMatch) {
hours = parseInt(hoursMatch[1]);
}
if (minutesMatch) {
minutes = parseInt(minutesMatch[1]);
}
durationMinutes = (hours * 60) + minutes;
// If no matches found, try to parse as pure minutes
if (durationMinutes === 0) {
const pureMinutes = parseInt(durationStr);
if (!isNaN(pureMinutes)) {
durationMinutes = pureMinutes;
}
}
}
// Calculate start time (visit time) and end time (visit time + duration)
const startTime = visitDateTime.toISOString().slice(0, 16); // Format for datetime-local
const endDateTime = new Date(visitDateTime.getTime() + (durationMinutes * 60 * 1000));
const endTime = endDateTime.toISOString().slice(0, 16);
console.log(`Calculated visit times: ${startTime} to ${endTime} (duration: ${durationMinutes} minutes)`);
return { startTime, endTime };
} catch (error) {
console.error('Error calculating visit times:', error);
return { startTime: null, endTime: null };
}
}
// Utility methods
escapeHtml(text) {
const map = {

View file

@ -2,6 +2,8 @@
module LocationSearch
class ResultAggregator
include ActionView::Helpers::TextHelper
# Time threshold for grouping consecutive points into visits (minutes)
VISIT_TIME_THRESHOLD = 30
@ -76,15 +78,15 @@ module LocationSearch
end
def format_duration(minutes)
return "~#{minutes}m" if minutes < 60
return "~#{pluralize(minutes, 'minute')}" if minutes < 60
hours = minutes / 60
remaining_minutes = minutes % 60
if remaining_minutes == 0
"~#{hours}h"
"~#{pluralize(hours, 'hour')}"
else
"~#{hours}h #{remaining_minutes}m"
"~#{pluralize(hours, 'hour')} #{pluralize(remaining_minutes, 'minute')}"
end
end

View file

@ -36,3 +36,7 @@ MANAGER_URL = SELF_HOSTED ? nil : ENV.fetch('MANAGER_URL', nil)
METRICS_USERNAME = ENV.fetch('METRICS_USERNAME', 'prometheus')
METRICS_PASSWORD = ENV.fetch('METRICS_PASSWORD', 'prometheus')
# /Prometheus metrics
FEATURES = {
location_search: !!ENV.fetch('PHOTON_API_HOST', nil)
}

View file

@ -297,7 +297,7 @@ RSpec.describe Api::V1::LocationsController, type: :request do
)
end
it 'limits suggestions to 5 results' do
it 'limits suggestions to 10 results' do
large_suggestions = Array.new(10) do |i|
{
lat: 52.5000 + i * 0.001,
@ -314,7 +314,7 @@ RSpec.describe Api::V1::LocationsController, type: :request do
get '/api/v1/locations/suggestions', params: { q: 'test' }, headers: headers
json_response = JSON.parse(response.body)
expect(json_response['suggestions'].length).to eq(5)
expect(json_response['suggestions'].length).to eq(10)
end
end

View file

@ -62,7 +62,7 @@ RSpec.describe LocationSearch::GeocodingService do
end
it 'limits results to MAX_RESULTS' do
expect(Geocoder).to receive(:search).with(query, limit: 5)
expect(Geocoder).to receive(:search).with(query, limit: 10)
service.search(query)
end

View file

@ -42,7 +42,7 @@ RSpec.describe LocationSearch::ResultAggregator do
result = service.group_points_into_visits([single_point])
visit = result.first
expect(visit[:duration_estimate]).to eq('~15m')
expect(visit[:duration_estimate]).to eq('~15 minutes')
expect(visit[:visit_details][:duration_minutes]).to eq(15)
end
end
@ -95,7 +95,7 @@ RSpec.describe LocationSearch::ResultAggregator do
result = service.group_points_into_visits(consecutive_points)
visit = result.first
expect(visit[:duration_estimate]).to eq('~45m')
expect(visit[:duration_estimate]).to eq('~45 minutes')
expect(visit[:visit_details][:duration_minutes]).to eq(45)
end
@ -196,14 +196,14 @@ RSpec.describe LocationSearch::ResultAggregator do
short_visit_points = points_with_various_durations.take(2)
result = service.group_points_into_visits(short_visit_points)
expect(result.first[:duration_estimate]).to eq('~25m')
expect(result.first[:duration_estimate]).to eq('~25 minutes')
end
it 'formats duration correctly for hours and minutes' do
long_visit_points = points_with_various_durations.drop(2)
result = service.group_points_into_visits(long_visit_points)
expect(result.first[:duration_estimate]).to eq('~2h 15m')
expect(result.first[:duration_estimate]).to eq('~2 hours 15 minutes')
end
it 'formats duration correctly for hours only' do
@ -215,7 +215,7 @@ RSpec.describe LocationSearch::ResultAggregator do
result = service.group_points_into_visits(exact_hour_points)
expect(result.first[:duration_estimate]).to eq('~2h')
expect(result.first[:duration_estimate]).to eq('~2 hours')
end
end

View file

@ -224,7 +224,7 @@ RSpec.describe 'Location Search Feature', type: :system, js: true do
find('#location-search-toggle').click
end
it 'adds search markers to the map' do
it 'completes search and shows results without location markers' do
fill_in 'location-search-input', with: 'Kaufland'
within('#location-search-container') do
click_button '🔍'
@ -233,8 +233,7 @@ RSpec.describe 'Location Search Feature', type: :system, js: true do
# Wait for search to complete
expect(page).to have_content('Kaufland Mitte')
# Check that markers are added (this would require inspecting the map object)
# For now, we'll verify the search completed successfully
# Verify search results are displayed (no location markers are added to keep map clean)
expect(page).to have_content('Found 1 location(s)')
end