ice/public/app-refactored.js
Claude Code a0fffcf4f0 Refactor architecture: Add models/services layer and refactor frontend
Major architectural improvements:
- Created models/services layer for better separation of concerns
  - Location model with async methods for database operations
  - ProfanityWord model for content moderation
  - DatabaseService for centralized database management
  - ProfanityFilterService refactored to use models
- Refactored frontend map implementations to share common code
  - MapBase class extracts 60-70% of duplicate functionality
  - Refactored implementations extend MapBase for specific features
  - Maintained unique geocoding capabilities per implementation
- Updated server.js to use new service architecture
- All routes now use async/await with models instead of raw queries
- Enhanced error handling and maintainability

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 19:21:51 -04:00

221 lines
No EOL
8.2 KiB
JavaScript

/**
* Nominatim-only implementation using MapBase
*/
class NominatimMapApp extends MapBase {
constructor() {
super();
this.initializeMap();
}
/**
* Initialize the Leaflet map
*/
initializeMap() {
this.map = L.map('map').setView([42.9634, -85.6681], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
}).addTo(this.map);
}
/**
* Create custom marker for location
*/
createMarker(location) {
const isPersistent = !!location.persistent;
const timeAgo = window.getTimeAgo ? window.getTimeAgo(location.created_at) : '';
const persistentText = isPersistent ? '<br><small><strong>📌 Persistent Report</strong></small>' : '';
const customIcon = this.createCustomIcon(isPersistent);
const marker = L.marker([location.latitude, location.longitude], {
icon: customIcon
})
.addTo(this.map)
.bindPopup(`<strong>${location.address}</strong><br>${location.description || 'No additional details'}<br><small>Reported ${timeAgo}</small>${persistentText}`);
return marker;
}
/**
* Create custom icon for markers
*/
createCustomIcon(isPersistent = false) {
const iconColor = isPersistent ? '#28a745' : '#dc3545'; // Green for persistent, red for temporary
const iconSymbol = isPersistent ? '🔒' : '⚠️'; // Lock for persistent, warning for temporary
return L.divIcon({
className: 'custom-marker',
html: `
<div style="
background-color: ${iconColor};
width: 30px;
height: 30px;
border-radius: 50%;
border: 3px solid white;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
cursor: pointer;
">${iconSymbol}</div>
`,
iconSize: [30, 30],
iconAnchor: [15, 15],
popupAnchor: [0, -15]
});
}
/**
* Search for addresses using Nominatim
*/
async searchAddress(query) {
try {
// Generate multiple search variations for better results
const searches = [
query, // Original query
query.replace(' & ', ' and '), // Replace & with 'and'
query.replace(' and ', ' & '), // Replace 'and' with &
query.replace(' at ', ' & '), // Replace 'at' with &
`${query}, Michigan`, // Add Michigan if not present
`${query}, MI`, // Add MI if not present
];
// Add street type variations
const streetVariations = [];
searches.forEach(search => {
streetVariations.push(search);
streetVariations.push(search.replace(' St ', ' Street '));
streetVariations.push(search.replace(' Street ', ' St '));
streetVariations.push(search.replace(' Ave ', ' Avenue '));
streetVariations.push(search.replace(' Avenue ', ' Ave '));
streetVariations.push(search.replace(' Rd ', ' Road '));
streetVariations.push(search.replace(' Road ', ' Rd '));
streetVariations.push(search.replace(' Blvd ', ' Boulevard '));
streetVariations.push(search.replace(' Boulevard ', ' Blvd '));
});
// Remove duplicates
const uniqueSearches = [...new Set(streetVariations)];
// Try each search variation until we get results
for (const searchQuery of uniqueSearches) {
try {
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&extratags=1&limit=5&q=${encodeURIComponent(searchQuery)}`);
const data = await response.json();
if (data && data.length > 0) {
// Prefer results that are in Michigan/Grand Rapids
const michiganResults = data.filter(item =>
item.display_name.toLowerCase().includes('michigan') ||
item.display_name.toLowerCase().includes('grand rapids') ||
item.display_name.toLowerCase().includes(', mi')
);
const results = michiganResults.length > 0 ? michiganResults : data;
this.showAutocomplete(results.slice(0, 5));
return;
}
} catch (error) {
console.warn(`Geocoding failed for: "${searchQuery}"`, error);
}
}
// If no results found, show empty autocomplete
this.showAutocomplete([]);
} catch (error) {
console.error('Error searching addresses:', error);
this.showAutocomplete([]);
}
}
/**
* Handle form submission with enhanced geocoding
*/
async handleSubmit() {
const address = this.addressInput.value.trim();
const description = this.descriptionInput.value.trim();
if (!address) {
this.showMessage('Please enter an address', 'error');
return;
}
// If coordinates are not set, try to geocode the address
let lat = parseFloat(this.addressInput.dataset.lat);
let lon = parseFloat(this.addressInput.dataset.lon);
if (!lat || !lon || isNaN(lat) || isNaN(lon)) {
// Try to geocode the current address value
const geocodeResult = await this.geocodeAddress(address);
if (geocodeResult) {
lat = parseFloat(geocodeResult.lat);
lon = parseFloat(geocodeResult.lon);
} else {
this.showMessage('Please select a valid address from the suggestions', 'error');
return;
}
}
// Disable submit button
this.submitButton.disabled = true;
this.submitButton.innerHTML = '<span class="loading-spinner"></span> Submitting...';
try {
const response = await fetch('/api/locations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
address,
latitude: lat,
longitude: lon,
description
})
});
const data = await response.json();
if (response.ok) {
this.showMessage('Location reported successfully!', 'success');
this.resetForm();
this.refreshLocations();
} else {
this.handleSubmitError(data);
}
} catch (error) {
console.error('Error submitting location:', error);
this.showMessage('Error submitting location. Please try again.', 'error');
} finally {
this.submitButton.disabled = false;
this.submitButton.innerHTML = '<i class="fas fa-flag"></i> Report Ice';
}
}
/**
* Geocode an address using Nominatim
*/
async geocodeAddress(address) {
try {
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent(address + ', Michigan')}`);
const data = await response.json();
if (data && data.length > 0) {
return data[0];
}
return null;
} catch (error) {
console.error('Error geocoding address:', error);
return null;
}
}
}
// Initialize the app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new NominatimMapApp();
});