Filters Module
The Filters module manages the restaurant filter buttons that allow users to quickly filter reviews by restaurant. It dynamically generates filter buttons based on available restaurants and coordinates with the UI module to update the displayed reviews.
Overview
The Filters module is responsible for:
- Generating filter buttons for each unique restaurant
- Tracking the currently active filter
- Applying visual styles to the active filter button
- Triggering UI updates when filter changes
Source: src/js/modules/filters.js
Public Properties
currentFilter
Tracks the currently active filter selection.
Type: string
Default: 'all'
Values:
'all' - Show all reviews (no filtering)
'Restaurant Name' - Show only reviews from this restaurant
Usage:
// Check current filter
if (Filters.currentFilter === 'all') {
console.log('Showing all reviews');
} else {
console.log(`Filtering by: ${Filters.currentFilter}`);
}
Public Methods
init()
Initializes the filters module and generates initial filter buttons.
This method takes no parameters
Returns: void
Usage:
// From src/js/app.js or ui.js
import { Filters } from './modules/filters.js';
Filters.init();
Behavior:
- Calls
update() to generate filter buttons
- Should be called after DataStore is initialized
update()
Regenerates filter buttons based on the current list of restaurants in DataStore.
Returns: void
Usage:
// From src/js/modules/filters.js:12
update() {
const restaurants = DataStore.getRestaurants();
const filtersContainer = document.getElementById('restaurantFilters');
if (!filtersContainer) return;
filtersContainer.innerHTML = restaurants.map(restaurant =>
`<button data-filter="${Utils.escapeHtml(restaurant)}" class="filter-btn px-4 py-2 rounded-full text-sm font-medium transition-all duration-200">
${Utils.escapeHtml(restaurant)}
</button>`
).join('');
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', (e) => this.filterByRestaurant(e.target.dataset.filter));
});
}
Behavior:
- Gets unique restaurant names from DataStore
- Generates HTML button for each restaurant
- Escapes restaurant names to prevent XSS
- Attaches click event listeners to all filter buttons
- Should be called when reviews are added/updated/deleted
When to call:
- During initialization
- After adding a new review with a new restaurant
- After deleting all reviews from a restaurant
- When reviews update from Firebase real-time listener
filterByRestaurant(restaurant)
Filters reviews by the specified restaurant and updates the UI.
The restaurant name to filter by, or ‘all’ to show all reviews
Returns: void
Usage:
// From src/js/modules/filters.js:29
filterByRestaurant(restaurant) {
this.currentFilter = restaurant;
// Update button styles
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
const activeBtn = document.querySelector(`[data-filter="${restaurant}"]`);
if (activeBtn) {
activeBtn.classList.add('active');
}
// Trigger UI update
UI.renderReviews();
}
Behavior:
- Updates
currentFilter property
- Removes
active class from all filter buttons
- Adds
active class to the clicked button
- Calls
UI.renderReviews() to update the review grid
Example:
// Filter to show only "El Emperador" reviews
Filters.filterByRestaurant('El Emperador');
// Show all reviews
Filters.filterByRestaurant('all');
HTML Structure
The filters module requires this HTML structure:
<div class="flex flex-wrap gap-2">
<!-- "All" button is always present in the HTML -->
<button data-filter="all" class="filter-btn active px-4 py-2 rounded-full text-sm font-medium transition-all duration-200">
Todos
</button>
<!-- Dynamic restaurant filters generated here -->
<div id="restaurantFilters" class="flex flex-wrap gap-2">
<!-- Generated by Filters.update() -->
</div>
</div>
Each filter button is generated as:
<button
data-filter="Restaurant Name"
class="filter-btn px-4 py-2 rounded-full text-sm font-medium transition-all duration-200"
>
Restaurant Name
</button>
When active, the active class is added:
<button
data-filter="El Emperador"
class="filter-btn active px-4 py-2 rounded-full text-sm font-medium transition-all duration-200"
>
El Emperador
</button>
Styling
Filter buttons use Tailwind CSS classes:
filter-btn - Custom class for targeting all filter buttons
active - Applied to currently selected filter
px-4 py-2 - Padding
rounded-full - Fully rounded corners (pill shape)
text-sm font-medium - Typography
transition-all duration-200 - Smooth transitions
Define .filter-btn and .filter-btn.active styles in your CSS file to customize the appearance of filter buttons.
Integration with UI Module
The Filters module works closely with the UI module:
// In UI.renderReviews() - src/js/modules/ui.js:48
if (Filters.currentFilter !== 'all') {
reviews = reviews.filter(r => r.restaurant === Filters.currentFilter);
}
The UI module checks Filters.currentFilter and filters the reviews array before rendering.
Security
All restaurant names are properly escaped before rendering:
import { Utils } from './utils.js';
// Escaping prevents XSS attacks
data-filter="${Utils.escapeHtml(restaurant)}"
${Utils.escapeHtml(restaurant)}
Never render user input directly into HTML without escaping. Always use Utils.escapeHtml().
Dependencies
The Filters module depends on:
- DataStore: To get unique restaurant names (
DataStore.getRestaurants())
- Utils: For HTML escaping (
Utils.escapeHtml())
- UI: To trigger review grid updates (
UI.renderReviews())
Dynamic Updates
The filter buttons automatically update when restaurants change:
// In src/js/modules/ui.js:17-21
document.addEventListener('reviewsUpdated', () => {
this.renderReviews();
Filters.update(); // Regenerate filter buttons
Stats.update();
});
When reviews are added/deleted via Firebase real-time sync, the reviewsUpdated event triggers Filters.update() to refresh the button list.
Edge Cases
No Reviews
When no reviews exist, DataStore.getRestaurants() returns an empty array, and no filter buttons are generated (only the “Todos” button remains).
Single Restaurant
If all reviews are from one restaurant, only one filter button is generated.
Special Characters
Restaurant names with special characters are properly escaped:
// Restaurant name: "O'Reilly's Pub"
// Generated HTML (safely escaped):
<button data-filter="O'Reilly's Pub">O'Reilly's Pub</button>
Usage Example
import { Filters } from './modules/filters.js';
import { DataStore } from './modules/datastore.js';
import { UI } from './modules/ui.js';
// Initialize DataStore first
await DataStore.init();
// Initialize Filters
Filters.init(); // Generates filter buttons
// User clicks a filter button
// Automatically calls Filters.filterByRestaurant('Restaurant Name')
// Which updates currentFilter and calls UI.renderReviews()
// Reset to show all
Filters.filterByRestaurant('all');
Combining with Search
Filters work alongside the Search module. Both can be active simultaneously:
// In UI.renderReviews() - src/js/modules/ui.js:44-51
let reviews = DataStore.reviews;
// Apply search first
if (Search.currentQuery) {
reviews = DataStore.searchReviews(Search.currentQuery);
}
// Then apply restaurant filter
if (Filters.currentFilter !== 'all') {
reviews = reviews.filter(r => r.restaurant === Filters.currentFilter);
}
Search and filter are applied sequentially. The UI shows reviews that match both the search query AND the selected restaurant filter.
- DataStore - Provides restaurant list
- UI - Renders filtered reviews
- Search - Complementary filtering by text
- Utils - HTML escaping utility