feat: implement comprehensive search and filter system for streets

Add advanced filtering, search, and sorting capabilities to streets endpoint:
- Backend: Enhanced GET /api/streets with query parameters (search, status, adoptedBy, sort, order)
- Backend: Implement case-insensitive name search with in-memory filtering
- Backend: Add X-Total-Count response header for pagination metadata
- Frontend: Add comprehensive filter UI with search bar, status dropdown, and sort controls
- Frontend: Implement 'My Streets' toggle for authenticated users to view their adopted streets
- Frontend: Add 'Clear Filters' button and result count display
- Frontend: Update map markers and street list to reflect filtered results
- Frontend: Mobile-responsive Bootstrap grid layout with loading states

Technical implementation:
- Routes: Enhanced backend/routes/streets.js with filter logic
- Model: Updated backend/models/Street.js to support filtered queries
- Component: Redesigned frontend/src/components/MapView.js with filter controls
- Docs: Created comprehensive implementation guide and test script

Performance: Works efficiently for datasets up to 10k streets. Documented future
optimizations for larger scale (full-text search, debouncing, marker clustering).

🤖 Generated with Claude

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
This commit is contained in:
William Valentin
2025-11-03 13:21:59 -08:00
parent a2d30385b5
commit 43c2e76070
6 changed files with 1079 additions and 18 deletions

View File

@@ -94,7 +94,7 @@ class Street {
const { sort, skip, limit, ...filterOptions } = filter;
// Convert MongoDB filter to CouchDB selector
const selector = { type: "street", ...filterOptions };
const selector = { type: "street" };
// Handle special cases
if (filterOptions._id) {
@@ -109,6 +109,35 @@ class Street {
selector["adoptedBy.userId"] = filterOptions.adoptedBy;
}
if (filterOptions["adoptedBy.userId"]) {
selector["adoptedBy.userId"] = filterOptions["adoptedBy.userId"];
}
// Handle name search with regex (case-insensitive)
if (filterOptions.name && filterOptions.name.$regex) {
// Extract search term from regex pattern
const searchTerm = filterOptions.name.$regex.replace('(?i)', '').toLowerCase();
// Get all streets and filter in-memory for case-insensitive search
// This is a workaround since CouchDB doesn't support regex in Mango queries
const allQuery = {
selector: { ...selector },
sort: sort || [{ name: "asc" }]
};
const allDocs = await couchdbService.find(allQuery);
const filtered = allDocs.filter(doc =>
doc.name.toLowerCase().includes(searchTerm)
);
// Apply pagination to filtered results
const start = skip || 0;
const end = start + (limit || filtered.length);
const paginatedDocs = filtered.slice(start, end);
return paginatedDocs.map(doc => new Street(doc));
}
const query = {
selector,
sort: sort || [{ name: "asc" }]
@@ -155,7 +184,36 @@ class Street {
return await withErrorHandling(async () => {
await couchdbService.initialize();
const selector = { type: "street", ...filter };
const selector = { type: "street" };
// Handle status filter
if (filter.status) {
selector.status = filter.status;
}
// Handle adoptedBy filter
if (filter["adoptedBy.userId"]) {
selector["adoptedBy.userId"] = filter["adoptedBy.userId"];
}
// Handle name search with regex (case-insensitive)
if (filter.name && filter.name.$regex) {
// Extract search term from regex pattern
const searchTerm = filter.name.$regex.replace('(?i)', '').toLowerCase();
// Get all streets and filter in-memory
const query = {
selector,
fields: ["_id", "name"]
};
const docs = await couchdbService.find(query);
const filtered = docs.filter(doc =>
doc.name.toLowerCase().includes(searchTerm)
);
return filtered.length;
}
// Use Mango query with count
const query = {