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>
This commit is contained in:
parent
6c90430ff6
commit
a0fffcf4f0
13 changed files with 2170 additions and 184 deletions
266
public/test-refactored.html
Normal file
266
public/test-refactored.html
Normal file
|
@ -0,0 +1,266 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Refactored Maps</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>
|
||||
.test-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.test-section {
|
||||
margin-bottom: 40px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
.test-section h2 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
#map {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.map-controls {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
.btn.active {
|
||||
background: #0056b3;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.form-group input, .form-group textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.search-container {
|
||||
position: relative;
|
||||
}
|
||||
#autocomplete {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
#autocomplete.show {
|
||||
display: block;
|
||||
}
|
||||
.autocomplete-item {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.autocomplete-item:hover,
|
||||
.autocomplete-item.selected {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.message {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
.message.show {
|
||||
display: block;
|
||||
}
|
||||
.message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.location-stats {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-container">
|
||||
<h1>Refactored Map Components Test</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Map Display</h2>
|
||||
<div class="map-controls">
|
||||
<button id="map-view-btn" class="btn active">Map View</button>
|
||||
<button id="table-view-btn" class="btn">Table View</button>
|
||||
<button id="theme-toggle" class="btn">Toggle Theme</button>
|
||||
<button id="refresh-button" class="btn">Refresh</button>
|
||||
</div>
|
||||
|
||||
<div id="map-view">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
|
||||
<div id="table-view" style="display: none;">
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="background: #f8f9fa;">
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">Location</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">Details</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">Reported</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">Expires</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="reports-tbody">
|
||||
<tr><td colspan="4" style="padding: 20px; text-align: center;">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="location-stats">
|
||||
<span id="location-count">Loading...</span> |
|
||||
<span id="last-update">Initializing...</span> |
|
||||
<span id="table-location-count">0 reports</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Location Submission Form</h2>
|
||||
<form id="location-form">
|
||||
<div class="form-group">
|
||||
<label for="address">Address:</label>
|
||||
<div class="search-container">
|
||||
<input type="text" id="address" placeholder="Enter address or intersection..." required>
|
||||
<div id="autocomplete"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description (optional):</label>
|
||||
<textarea id="description" placeholder="Describe road conditions..." rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-btn btn">
|
||||
<i class="fas fa-flag"></i> Report Ice
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="message" class="message"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="utils.js"></script>
|
||||
<script src="map-base.js"></script>
|
||||
<script>
|
||||
// Test both implementations
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Create a simple test implementation
|
||||
class TestMapApp extends MapBase {
|
||||
constructor() {
|
||||
super();
|
||||
this.initializeMap();
|
||||
this.initializeTestFeatures();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
initializeTestFeatures() {
|
||||
// View toggle
|
||||
const mapViewBtn = document.getElementById('map-view-btn');
|
||||
const tableViewBtn = document.getElementById('table-view-btn');
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
|
||||
if (mapViewBtn) mapViewBtn.addEventListener('click', () => this.switchView('map'));
|
||||
if (tableViewBtn) tableViewBtn.addEventListener('click', () => this.switchView('table'));
|
||||
if (themeToggle) themeToggle.addEventListener('click', () => this.toggleTheme());
|
||||
}
|
||||
|
||||
switchView(viewType) {
|
||||
const mapView = document.getElementById('map-view');
|
||||
const tableView = document.getElementById('table-view');
|
||||
const mapViewBtn = document.getElementById('map-view-btn');
|
||||
const tableViewBtn = document.getElementById('table-view-btn');
|
||||
|
||||
if (viewType === 'map') {
|
||||
mapView.style.display = 'block';
|
||||
tableView.style.display = 'none';
|
||||
mapViewBtn.classList.add('active');
|
||||
tableViewBtn.classList.remove('active');
|
||||
setTimeout(() => this.map.invalidateSize(), 100);
|
||||
} else {
|
||||
mapView.style.display = 'none';
|
||||
tableView.style.display = 'block';
|
||||
mapViewBtn.classList.remove('active');
|
||||
tableViewBtn.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
toggleTheme() {
|
||||
const currentTheme = document.body.dataset.theme || 'light';
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
document.body.dataset.theme = newTheme;
|
||||
}
|
||||
|
||||
createMarker(location) {
|
||||
const isPersistent = !!location.persistent;
|
||||
const timeAgo = window.getTimeAgo ? window.getTimeAgo(location.created_at) : '';
|
||||
|
||||
const marker = L.marker([location.latitude, location.longitude])
|
||||
.addTo(this.map)
|
||||
.bindPopup(`<strong>${location.address}</strong><br>${location.description || 'No details'}<br><small>${timeAgo}</small>`);
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
async searchAddress(query) {
|
||||
try {
|
||||
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&limit=5&q=${encodeURIComponent(query + ', Michigan')}`);
|
||||
const data = await response.json();
|
||||
this.showAutocomplete(data.slice(0, 5));
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
this.showAutocomplete([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new TestMapApp();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Add a link
Reference in a new issue